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"