File indexing completed on 2024-04-28 05:49:33

0001 /*
0002     SPDX-FileCopyrightText: 2014 Dominik Haumann <dhaumann@kde.org>
0003     SPDX-FileCopyrightText: 2020 Christoph Cullmann <cullmann@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "katetabbar.h"
0009 #include "kateapp.h"
0010 #include "ktexteditor_utils.h"
0011 #include "tabmimedata.h"
0012 
0013 #include <QApplication>
0014 #include <QDataStream>
0015 #include <QDrag>
0016 #include <QPixmap>
0017 #include <QStyleOptionTab>
0018 #include <QStylePainter>
0019 #include <QWheelEvent>
0020 
0021 #include <KAcceleratorManager>
0022 #include <KConfigGroup>
0023 #include <KSharedConfig>
0024 
0025 #include <KTextEditor/Document>
0026 
0027 /**
0028  * Creates a new tab bar with the given \a parent.
0029  */
0030 KateTabBar::KateTabBar(QWidget *parent)
0031     : QTabBar(parent)
0032 {
0033     // we want no auto-accelerators here
0034     KAcceleratorManager::setNoAccel(this);
0035 
0036     // enable document mode, docs tell this will trigger:
0037     // On macOS this will look similar to the tabs in Safari or Sierra's Terminal.app.
0038     // this seems reasonable for our document tabs
0039     setDocumentMode(true);
0040 
0041     // we want drag and drop
0042     setAcceptDrops(true);
0043 
0044     // allow users to re-arrange the tabs
0045     setMovable(true);
0046 
0047     // enforce configured limit
0048     readConfig();
0049 
0050     // handle config changes
0051     connect(KateApp::self(), &KateApp::configurationChanged, this, &KateTabBar::readConfig);
0052 }
0053 
0054 void KateTabBar::readConfig()
0055 {
0056     KSharedConfig::Ptr config = KSharedConfig::openConfig();
0057     KConfigGroup cgGeneral = KConfigGroup(config, QStringLiteral("General"));
0058 
0059     // 0 == unlimited, normalized other inputs
0060     const int tabCountLimit = std::max(cgGeneral.readEntry("Tabbar Tab Limit", 0), 0);
0061     if (m_tabCountLimit != tabCountLimit) {
0062         m_tabCountLimit = tabCountLimit;
0063         const QList<DocOrWidget> docList = documentList();
0064         if (m_tabCountLimit > 0 && docList.count() > m_tabCountLimit) {
0065             // close N least used tabs
0066             std::map<quint64, DocOrWidget> lruDocs;
0067             for (const DocOrWidget &doc : docList) {
0068                 lruDocs[m_docToLruCounterAndHasTab[doc].first] = doc;
0069             }
0070             int toRemove = docList.count() - m_tabCountLimit;
0071             for (const auto &[_, doc] : lruDocs) {
0072                 if (toRemove-- == 0) {
0073                     break;
0074                 }
0075                 removeTab(documentIdx(doc));
0076             }
0077         } else if (m_docToLruCounterAndHasTab.size() > (size_t)docList.size()) {
0078             // populate N recently user documents
0079             std::map<quint64, DocOrWidget, std::greater<quint64>> mruDocs;
0080             for (const auto &i : m_docToLruCounterAndHasTab) {
0081                 DocOrWidget doc = i.first;
0082                 if (!docList.contains(doc)) {
0083                     mruDocs[i.second.first] = doc;
0084                 }
0085             }
0086             int toAdd = m_tabCountLimit - docList.count();
0087             for (const auto &i : mruDocs) {
0088                 if (toAdd-- == 0) {
0089                     break;
0090                 }
0091                 DocOrWidget doc = i.second;
0092                 const auto idx = addTab(doc.doc() ? doc.doc()->documentName() : doc.widget()->windowTitle());
0093                 setTabDocument(idx, doc);
0094             }
0095         }
0096     }
0097 
0098     // use scroll buttons if we have no limit
0099     setUsesScrollButtons(m_tabCountLimit == 0 || cgGeneral.readEntry("Allow Tab Scrolling", true));
0100 
0101     // elide if requested, this is independent of the limit, just honor the users wish
0102     setElideMode(cgGeneral.readEntry("Elide Tab Text", false) ? Qt::ElideMiddle : Qt::ElideNone);
0103 
0104     // handle tab close button and expansion
0105     setExpanding(cgGeneral.readEntry("Expand Tabs", false));
0106     setTabsClosable(cgGeneral.readEntry("Show Tabs Close Button", true));
0107 
0108     // get mouse click rules
0109     m_doubleClickNewDocument = cgGeneral.readEntry("Tab Double Click New Document", true);
0110     m_middleClickCloseDocument = cgGeneral.readEntry("Tab Middle Click Close Document", true);
0111     m_openNewTabInFrontOfCurrent = cgGeneral.readEntry("Open New Tab To The Right Of Current", false);
0112 }
0113 
0114 void KateTabBar::setActive(bool active)
0115 {
0116     if (active == m_isActive) {
0117         return;
0118     }
0119     m_isActive = active;
0120     update();
0121 }
0122 
0123 bool KateTabBar::isActive() const
0124 {
0125     return m_isActive;
0126 }
0127 
0128 int KateTabBar::prevTab() const
0129 {
0130     return currentIndex() == 0 ? 0 // first index, keep it here.
0131                                : currentIndex() - 1;
0132 }
0133 
0134 int KateTabBar::nextTab() const
0135 {
0136     return currentIndex() == count() - 1 ? count() - 1 // last index, keep it here.
0137                                          : currentIndex() + 1;
0138 }
0139 
0140 bool KateTabBar::containsTab(int index) const
0141 {
0142     return index >= 0 && index < count();
0143 }
0144 
0145 QVariant KateTabBar::ensureValidTabData(int idx)
0146 {
0147     if (!tabData(idx).isValid()) {
0148         DocOrWidget v(static_cast<KTextEditor::Document *>(nullptr));
0149         setTabData(idx, QVariant::fromValue(v));
0150     }
0151     return tabData(idx);
0152 }
0153 
0154 void KateTabBar::mouseDoubleClickEvent(QMouseEvent *event)
0155 {
0156     event->accept();
0157 
0158     if (m_doubleClickNewDocument && event->button() == Qt::LeftButton) {
0159         Q_EMIT newTabRequested();
0160     }
0161 }
0162 
0163 void KateTabBar::mousePressEvent(QMouseEvent *event)
0164 {
0165     if (!isActive()) {
0166         Q_EMIT activateViewSpaceRequested();
0167     }
0168 
0169     int tab = tabAt(event->pos());
0170     if (event->button() == Qt::LeftButton && tab != -1 && count() > 1) {
0171         dragStartPos = event->pos();
0172         auto r = tabRect(tab);
0173         dragHotspotPos = {dragStartPos.x() - r.x(), dragStartPos.y() - r.y()};
0174     } else {
0175         dragStartPos = {};
0176     }
0177 
0178     QTabBar::mousePressEvent(event);
0179 
0180     // handle close for middle mouse button
0181     if (m_middleClickCloseDocument && event->button() == Qt::MiddleButton) {
0182         int id = tabAt(event->pos());
0183         if (id >= 0) {
0184             Q_EMIT tabCloseRequested(id);
0185         }
0186     }
0187 }
0188 
0189 void KateTabBar::mouseMoveEvent(QMouseEvent *event)
0190 {
0191     if (dragStartPos.isNull()) {
0192         QTabBar::mouseMoveEvent(event);
0193         return;
0194     }
0195 
0196     // We start our drag once the cursor leaves "current view space"
0197     // Starting drag before that is a bit useless
0198     // One disadvantage of the current approach is that the user
0199     // might not know that kate's tabs can be dragged to other
0200     // places, unless they drag it really far away
0201     auto viewSpace = qobject_cast<KateViewSpace *>(parent());
0202     const auto viewspaceRect = viewSpace->rect();
0203     QRect viewspaceRectTopArea = viewspaceRect;
0204     viewspaceRectTopArea.setTop(viewspaceRect.top() - 40);
0205     viewspaceRectTopArea.setBottom(viewspaceRect.height() / 2);
0206     if (!viewSpace || viewspaceRectTopArea.contains(event->pos())) {
0207         QTabBar::mouseMoveEvent(event);
0208         return;
0209     }
0210 
0211     if ((event->pos() - dragStartPos).manhattanLength() < QApplication::startDragDistance()) {
0212         QTabBar::mouseMoveEvent(event);
0213         return;
0214     }
0215 
0216     if (rect().contains(event->pos())) {
0217         return QTabBar::mouseMoveEvent(event);
0218     }
0219 
0220     int tab = currentIndex();
0221     if (tab < 0) {
0222         return;
0223     }
0224 
0225     QRect rect = tabRect(tab);
0226     const auto tabData = this->tabData(tab).value<DocOrWidget>();
0227     auto *tabObject = tabData.qobject();
0228     if (!tabObject) {
0229         return;
0230     }
0231 
0232     QPixmap p(rect.size() * this->devicePixelRatioF());
0233     p.setDevicePixelRatio(this->devicePixelRatioF());
0234     p.fill(Qt::transparent);
0235 
0236     // For some reason initStyleOption with tabIdx directly
0237     // wasn't working, so manually set some stuff
0238     QStyleOptionTab opt;
0239     opt.text = tabText(tab);
0240     opt.icon = tabIcon(tab);
0241     opt.iconSize = iconSize();
0242     opt.state = QStyle::State_Enabled | QStyle::State_Selected | QStyle::State_Raised;
0243     opt.tabIndex = tab;
0244     opt.position = QStyleOptionTab::OnlyOneTab;
0245     opt.features = QStyleOptionTab::TabFeature::HasFrame;
0246     opt.rect = rect;
0247     // adjust the rect so that it starts at(0,0)
0248     opt.rect.adjust(-opt.rect.x(), 0, -opt.rect.x(), 0);
0249 
0250     QStylePainter paint(&p, this);
0251     paint.drawControl(QStyle::CE_TabBarTab, opt);
0252     paint.end();
0253 
0254     QByteArray data;
0255     auto view = viewSpace->currentView();
0256     if (view) {
0257         KTextEditor::Cursor cp = view->cursorPosition();
0258         QDataStream ds(&data, QIODevice::WriteOnly);
0259         ds << cp.line();
0260         ds << cp.column();
0261         ds << view->document()->url();
0262     } else if (!viewSpace->currentWidget()) {
0263         qWarning() << "No view or widget, why?";
0264         return;
0265     }
0266 
0267     auto mime = new TabMimeData(viewSpace, tabData);
0268     mime->setData(QStringLiteral("application/kate.tab.mimedata"), data);
0269 
0270     QDrag *drag = new QDrag(this);
0271     drag->setMimeData(mime);
0272     drag->setPixmap(p);
0273     drag->setHotSpot(dragHotspotPos);
0274 
0275     auto posCopy = dragStartPos;
0276     dragStartPos = {};
0277     dragHotspotPos = {};
0278     drag->exec(Qt::CopyAction);
0279 
0280     // On drag end we check whether the drag was a success
0281     // i.e., if the widget/doc in tabData still exists in
0282     // this tabbar, it failed or there was a copy operation
0283     // in which case there might be the "movable tab" hanging
0284     // in between which we reset
0285     auto onDragEnd = [this, tabObject, posCopy]() {
0286         bool found = false;
0287         int tabIdx = 0;
0288         for (; tabIdx < count(); ++tabIdx) {
0289             auto d = this->tabData(tabIdx);
0290             // We only expect doc, no dnd support for widgets
0291             if (d.value<DocOrWidget>().qobject() == tabObject) {
0292                 found = true;
0293                 break;
0294             }
0295         }
0296 
0297         if (found) {
0298             // We send this even to ensure the "moveable tab" is properly reset and we have no dislocated tabs
0299             auto e = QMouseEvent(QEvent::MouseButtonPress, posCopy, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
0300             qApp->sendEvent(this, &e);
0301             if (currentIndex() != tabIdx) {
0302                 setCurrentIndex(tabIdx);
0303             }
0304         }
0305     };
0306 
0307     connect(drag, &QDrag::destroyed, this, onDragEnd);
0308 }
0309 
0310 void KateTabBar::contextMenuEvent(QContextMenuEvent *ev)
0311 {
0312     int id = tabAt(ev->pos());
0313     if (id >= 0) {
0314         Q_EMIT contextMenuRequest(id, ev->globalPos());
0315     }
0316 }
0317 
0318 void KateTabBar::setTabDocument(int idx, DocOrWidget d)
0319 {
0320     setTabData(idx, QVariant::fromValue(d));
0321 
0322     auto *doc = d.doc();
0323     // BUG: 441340 We need to escape the & because it is used for accelerators/shortcut mnemonic by default
0324     QString tabName = d.doc() ? d.doc()->documentName() : d.widget()->windowTitle();
0325     tabName.replace(QLatin1Char('&'), QLatin1String("&&"));
0326     setTabText(idx, tabName);
0327     if (d.doc()) {
0328         setTabToolTip(idx, doc->url().isValid() ? doc->url().toDisplayString(QUrl::PreferLocalFile) : doc->documentName());
0329         setModifiedStateIcon(idx, doc);
0330     } else {
0331         setTabIcon(idx, d.widget()->windowIcon());
0332     }
0333 }
0334 
0335 void KateTabBar::setModifiedStateIcon(int idx, KTextEditor::Document *doc)
0336 {
0337     // use common method, as e.g. in filetree plugin, too
0338     setTabIcon(idx, Utils::iconForDocument(doc));
0339 }
0340 
0341 void KateTabBar::setCurrentDocument(DocOrWidget docOrWidget)
0342 {
0343     // in any case: update lru counter for this document, might add new element to hash
0344     // we have a tab after this call, too!
0345     m_docToLruCounterAndHasTab[docOrWidget] = std::make_pair(++m_lruCounter, true);
0346 
0347     // do we have a tab for this document?
0348     // if yes => just set as current one
0349     const int existingIndex = documentIdx(docOrWidget);
0350     if (existingIndex != -1) {
0351         setCurrentIndex(existingIndex);
0352         return;
0353     }
0354 
0355     // else: if we are still inside the allowed number of tabs or have no limit
0356     // => create new tab and be done
0357     if ((m_tabCountLimit == 0) || (count() < m_tabCountLimit)) {
0358         m_beingAdded = docOrWidget;
0359         int index = m_openNewTabInFrontOfCurrent && currentIndex() != -1 ? currentIndex() + 1 : -1;
0360         insertTab(index, docOrWidget.doc() ? docOrWidget.doc()->documentName() : docOrWidget.widget()->windowTitle());
0361         return;
0362     }
0363 
0364     // ok, we have already the limit of tabs reached:
0365     // replace the tab with the lowest lru counter => the least recently used
0366 
0367     // search for the right tab
0368     quint64 minCounter = static_cast<quint64>(-1);
0369     int indexToReplace = 0;
0370     DocOrWidget docToReplace = DocOrWidget::null();
0371     for (int idx = 0; idx < count(); idx++) {
0372         QVariant data = tabData(idx);
0373         DocOrWidget doc = data.value<DocOrWidget>();
0374         const quint64 currentCounter = m_docToLruCounterAndHasTab[doc].first;
0375         if (currentCounter <= minCounter) {
0376             minCounter = currentCounter;
0377             indexToReplace = idx;
0378             docToReplace = doc;
0379         }
0380     }
0381 
0382     // mark the replace doc as "has no tab"
0383     m_docToLruCounterAndHasTab[docToReplace].second = false;
0384 
0385     const auto oldCurrentIdx = currentIndex();
0386 
0387     // replace it's data + set it as active
0388     setTabDocument(indexToReplace, docOrWidget);
0389     setCurrentIndex(indexToReplace);
0390 
0391     const auto newCurrentIdx = currentIndex();
0392     if (m_openNewTabInFrontOfCurrent && oldCurrentIdx != -1 && newCurrentIdx != oldCurrentIdx + 1) {
0393         moveTab(newCurrentIdx, oldCurrentIdx + 1);
0394     }
0395 }
0396 
0397 void KateTabBar::removeDocument(DocOrWidget doc)
0398 {
0399     // purge LRU storage, must work
0400     auto erased = (m_docToLruCounterAndHasTab.erase(doc) == 1);
0401     if (!erased) {
0402         qWarning() << Q_FUNC_INFO << "Failed to erase";
0403     }
0404 
0405     // remove document if needed, we might have no tab for it, if tab count is limited!
0406     const int idx = documentIdx(doc);
0407     if (idx == -1) {
0408         return;
0409     }
0410 
0411     // if we have some tab limit, replace the removed tab with the next best document that has none!
0412     if (m_tabCountLimit > 0) {
0413         quint64 maxCounter = 0;
0414         DocOrWidget docToReplace = DocOrWidget::null();
0415         for (const auto &lru : m_docToLruCounterAndHasTab) {
0416             // ignore stuff with tabs
0417             if (lru.second.second) {
0418                 continue;
0419             }
0420 
0421             // search most recently used one
0422             if (lru.second.first >= maxCounter) {
0423                 maxCounter = lru.second.first;
0424                 docToReplace = lru.first;
0425             }
0426         }
0427 
0428         // any document found? replace the tab we want to close and be done
0429         if (!docToReplace.isNull()) {
0430             // mark the replace doc as "has a tab"
0431             m_docToLruCounterAndHasTab[docToReplace].second = true;
0432 
0433             // replace info for the tab
0434             setTabDocument(idx, docToReplace);
0435             setCurrentIndex(idx);
0436             Q_EMIT currentChanged(idx);
0437             return;
0438         }
0439     }
0440 
0441     // if we arrive here, we just need to purge the tab
0442     // this happens if we have no limit or no document to replace the current one
0443     removeTab(idx);
0444 }
0445 
0446 int KateTabBar::documentIdx(DocOrWidget doc)
0447 {
0448     for (int idx = 0; idx < count(); idx++) {
0449         const QVariant data = tabData(idx);
0450         if (data.value<DocOrWidget>().qobject() == doc.qobject()) {
0451             return idx;
0452         }
0453     }
0454     return -1;
0455 }
0456 
0457 DocOrWidget KateTabBar::tabDocument(int idx)
0458 {
0459     QVariant data = ensureValidTabData(idx);
0460     DocOrWidget buttonData = data.value<DocOrWidget>();
0461 
0462     auto doc = DocOrWidget::null();
0463     // The tab got activated before the correct finalization,
0464     // we need to plug the document before returning.
0465     if (buttonData.isNull() && !m_beingAdded.isNull()) {
0466         setTabDocument(idx, m_beingAdded);
0467         doc = m_beingAdded;
0468         m_beingAdded.clear();
0469     } else {
0470         doc = buttonData;
0471     }
0472 
0473     return doc;
0474 }
0475 
0476 void KateTabBar::tabInserted(int idx)
0477 {
0478     if (m_beingAdded.qobject()) {
0479         setTabDocument(idx, m_beingAdded);
0480     }
0481     m_beingAdded.clear();
0482 }
0483 
0484 QList<DocOrWidget> KateTabBar::documentList() const
0485 {
0486     QList<DocOrWidget> result;
0487     result.reserve(count());
0488     for (int idx = 0; idx < count(); idx++) {
0489         QVariant data = tabData(idx);
0490         result.append(data.value<DocOrWidget>());
0491     }
0492     return result;
0493 }
0494 
0495 #include "moc_katetabbar.cpp"