File indexing completed on 2024-04-28 05:49:18
0001 /* This file is part of the KDE project 0002 0003 SPDX-FileCopyrightText: 2014-2019 Dominik Haumann <dhaumann@kde.org> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "tabswitcher.h" 0009 #include "tabswitcherfilesmodel.h" 0010 #include "tabswitchertreeview.h" 0011 0012 #include <KTextEditor/Application> 0013 #include <KTextEditor/Document> 0014 #include <KTextEditor/Editor> 0015 #include <KTextEditor/View> 0016 0017 #include <KActionCollection> 0018 #include <KLocalizedString> 0019 #include <KPluginFactory> 0020 #include <KXMLGUIFactory> 0021 0022 #include <QAction> 0023 #include <QScrollBar> 0024 0025 K_PLUGIN_FACTORY_WITH_JSON(TabSwitcherPluginFactory, "tabswitcherplugin.json", registerPlugin<TabSwitcherPlugin>();) 0026 0027 TabSwitcherPlugin::TabSwitcherPlugin(QObject *parent, const QVariantList &) 0028 : KTextEditor::Plugin(parent) 0029 { 0030 } 0031 0032 QObject *TabSwitcherPlugin::createView(KTextEditor::MainWindow *mainWindow) 0033 { 0034 return new TabSwitcherPluginView(this, mainWindow); 0035 } 0036 0037 TabSwitcherPluginView::TabSwitcherPluginView(TabSwitcherPlugin *plugin, KTextEditor::MainWindow *mainWindow) 0038 : QObject(mainWindow) 0039 , m_plugin(plugin) 0040 , m_mainWindow(mainWindow) 0041 { 0042 // register this view 0043 m_plugin->m_views.append(this); 0044 0045 m_model = new detail::TabswitcherFilesModel(this); 0046 m_treeView = new TabSwitcherTreeView(); 0047 m_treeView->setModel(m_model); 0048 0049 KXMLGUIClient::setComponentName(QStringLiteral("tabswitcher"), i18n("Document Switcher")); 0050 setXMLFile(QStringLiteral("ui.rc")); 0051 0052 // note: call after m_treeView is created 0053 setupActions(); 0054 0055 // fill the model 0056 setupModel(); 0057 0058 // register action in menu 0059 m_mainWindow->guiFactory()->addClient(this); 0060 0061 // popup connections 0062 connect(m_treeView, &TabSwitcherTreeView::pressed, this, &TabSwitcherPluginView::switchToClicked); 0063 connect(m_treeView, &TabSwitcherTreeView::itemActivated, this, &TabSwitcherPluginView::activateView); 0064 0065 // track existing documents 0066 connect(KTextEditor::Editor::instance()->application(), &KTextEditor::Application::documentCreated, this, &TabSwitcherPluginView::registerDocument); 0067 connect(KTextEditor::Editor::instance()->application(), &KTextEditor::Application::documentWillBeDeleted, this, &TabSwitcherPluginView::unregisterDocument); 0068 0069 auto mw = mainWindow->window(); 0070 // clang-format off 0071 connect(mw, SIGNAL(widgetAdded(QWidget*)), this, SLOT(onWidgetCreated(QWidget*))); 0072 connect(mw, SIGNAL(widgetRemoved(QWidget*)), this, SLOT(onWidgetRemoved(QWidget*))); 0073 // clang-format on 0074 0075 // track lru activation of views to raise the respective documents in the model 0076 connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &TabSwitcherPluginView::raiseView); 0077 } 0078 0079 TabSwitcherPluginView::~TabSwitcherPluginView() 0080 { 0081 // delete popup widget 0082 delete m_treeView; 0083 0084 // unregister action in menu 0085 m_mainWindow->guiFactory()->removeClient(this); 0086 0087 // unregister this view 0088 m_plugin->m_views.removeAll(this); 0089 } 0090 0091 void TabSwitcherPluginView::setupActions() 0092 { 0093 auto aNext = actionCollection()->addAction(QStringLiteral("view_lru_document_next")); 0094 aNext->setText(i18n("Last Used Views")); 0095 aNext->setIcon(QIcon::fromTheme(QStringLiteral("go-next-view-page"))); 0096 actionCollection()->setDefaultShortcut(aNext, Qt::CTRL | Qt::Key_Tab); 0097 aNext->setWhatsThis(i18n("Opens a list to walk through the list of last used views.")); 0098 aNext->setStatusTip(i18n("Walk through the list of last used views")); 0099 connect(aNext, &QAction::triggered, this, &TabSwitcherPluginView::walkForward); 0100 0101 auto aPrev = actionCollection()->addAction(QStringLiteral("view_lru_document_prev")); 0102 aPrev->setText(i18n("Last Used Views (Reverse)")); 0103 aPrev->setIcon(QIcon::fromTheme(QStringLiteral("go-previous-view-page"))); 0104 actionCollection()->setDefaultShortcut(aPrev, Qt::CTRL | Qt::SHIFT | Qt::Key_Tab); 0105 aPrev->setWhatsThis(i18n("Opens a list to walk through the list of last used views in reverse.")); 0106 aPrev->setStatusTip(i18n("Walk through the list of last used views")); 0107 connect(aPrev, &QAction::triggered, this, &TabSwitcherPluginView::walkBackward); 0108 0109 auto aClose = actionCollection()->addAction(QStringLiteral("view_lru_document_close")); 0110 aClose->setText(i18n("Close View")); 0111 aClose->setShortcutContext(Qt::WidgetShortcut); 0112 actionCollection()->setDefaultShortcut(aClose, Qt::CTRL | Qt::Key_W); 0113 aClose->setWhatsThis(i18n("Closes the selected view in the list of last used views.")); 0114 aClose->setStatusTip(i18n("Closes the selected view in the list of last used views.")); 0115 connect(aClose, &QAction::triggered, this, &TabSwitcherPluginView::closeView); 0116 0117 // make sure action work when the popup has focus 0118 m_treeView->addAction(aNext); 0119 m_treeView->addAction(aPrev); 0120 m_treeView->addAction(aClose); 0121 } 0122 0123 void TabSwitcherPluginView::setupModel() 0124 { 0125 const auto documents = KTextEditor::Editor::instance()->application()->documents(); 0126 // initial fill of model 0127 for (auto doc : documents) { 0128 registerDocument(doc); 0129 } 0130 } 0131 0132 void TabSwitcherPluginView::registerItem(DocOrWidget docOrWidget) 0133 { 0134 // insert into hash 0135 m_documents.insert(docOrWidget); 0136 0137 // add to model 0138 m_model->insertDocument(0, docOrWidget); 0139 } 0140 0141 void TabSwitcherPluginView::unregisterItem(DocOrWidget docOrWidget) 0142 { 0143 // remove from hash 0144 auto it = m_documents.find(docOrWidget); 0145 if (it == m_documents.end()) { 0146 return; 0147 } 0148 m_documents.erase(it); 0149 0150 // remove from model 0151 m_model->removeDocument(docOrWidget); 0152 } 0153 0154 void TabSwitcherPluginView::onWidgetCreated(QWidget *widget) 0155 { 0156 registerItem(widget); 0157 } 0158 0159 void TabSwitcherPluginView::onWidgetRemoved(QWidget *widget) 0160 { 0161 unregisterItem(widget); 0162 } 0163 0164 void TabSwitcherPluginView::registerDocument(KTextEditor::Document *document) 0165 { 0166 registerItem(document); 0167 connect(document, &KTextEditor::Document::documentNameChanged, this, &TabSwitcherPluginView::updateDocumentName); 0168 } 0169 0170 void TabSwitcherPluginView::unregisterDocument(KTextEditor::Document *document) 0171 { 0172 unregisterItem(document); 0173 // disconnect documentNameChanged() signal 0174 disconnect(document, nullptr, this, nullptr); 0175 } 0176 0177 void TabSwitcherPluginView::updateDocumentName(KTextEditor::Document *document) 0178 { 0179 if (m_documents.find(document) == m_documents.end()) { 0180 return; 0181 } 0182 0183 // update all items, since a document URL change menas we have to recalculate 0184 // common prefix path of all items. 0185 m_model->updateItems(); 0186 } 0187 0188 void TabSwitcherPluginView::raiseView(KTextEditor::View *view) 0189 { 0190 auto activeWidget = [this, view]() -> DocOrWidget { 0191 if (view && view->document()) { 0192 return view->document(); 0193 } 0194 QWidget *active = nullptr; 0195 QMetaObject::invokeMethod(m_mainWindow->window(), "activeWidget", Q_RETURN_ARG(QWidget *, active)); 0196 return active; 0197 }(); 0198 0199 if (activeWidget.isNull() || m_documents.find(activeWidget) == m_documents.end()) { 0200 return; 0201 } 0202 0203 m_model->raiseDocument(activeWidget); 0204 } 0205 0206 void TabSwitcherPluginView::walk(const int from, const int to) 0207 { 0208 if (m_model->rowCount() <= 1) { 0209 return; 0210 } 0211 0212 QModelIndex index; 0213 const int step = from < to ? 1 : -1; 0214 if (!m_treeView->isVisible()) { 0215 updateViewGeometry(); 0216 index = m_model->index(from + step, 0); 0217 if (!index.isValid()) { 0218 index = m_model->index(0, 0); 0219 } 0220 m_treeView->show(); 0221 m_treeView->setFocus(); 0222 } else { 0223 int newRow = m_treeView->selectionModel()->currentIndex().row() + step; 0224 if (newRow == to + step) { 0225 newRow = from; 0226 } 0227 index = m_model->index(newRow, 0); 0228 } 0229 0230 m_treeView->selectionModel()->select(index, QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect); 0231 m_treeView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); 0232 } 0233 0234 void TabSwitcherPluginView::walkForward() 0235 { 0236 walk(0, m_model->rowCount() - 1); 0237 } 0238 0239 void TabSwitcherPluginView::walkBackward() 0240 { 0241 walk(m_model->rowCount() - 1, 0); 0242 } 0243 0244 void TabSwitcherPluginView::updateViewGeometry() 0245 { 0246 QWidget *window = m_mainWindow->window(); 0247 const QSize centralSize = window->size(); 0248 0249 // Maximum size of the view is 3/4th of the central widget (the editor area) 0250 // so the view does not overlap the mainwindow since that looks awkward. 0251 const QSize viewMaxSize(centralSize.width() * 3 / 4, centralSize.height() * 3 / 4); 0252 0253 // The actual view size should be as big as the columns/rows need it, but 0254 // smaller than the max-size. This means the view will get quite high with 0255 // many open files but I think thats ok. Otherwise one can easily tweak the 0256 // max size to be only 1/2th of the central widget size 0257 const int rowHeight = m_treeView->sizeHintForRow(0); 0258 const int frameWidth = m_treeView->frameWidth(); 0259 // const QSize viewSize(std::min(m_treeView->sizeHintForColumn(0) + 2 * frameWidth + m_treeView->verticalScrollBar()->width(), viewMaxSize.width()), // ORIG 0260 // line, sizeHintForColumn was QListView but is protected for QTreeView so we introduced sizeHintWidth() 0261 const QSize viewSize(std::min(m_treeView->sizeHintWidth() + 2 * frameWidth + m_treeView->verticalScrollBar()->width(), viewMaxSize.width()), 0262 std::min(std::max(rowHeight * m_model->rowCount() + 2 * frameWidth, rowHeight * 6), viewMaxSize.height())); 0263 0264 // Position should be central over the editor area, so map to global from 0265 // parent of central widget since the view is positioned in global coords 0266 const QPoint centralWidgetPos = window->parent() ? window->mapToGlobal(window->pos()) : window->pos(); 0267 const int xPos = std::max(0, centralWidgetPos.x() + (centralSize.width() - viewSize.width()) / 2); 0268 const int yPos = std::max(0, centralWidgetPos.y() + (centralSize.height() - viewSize.height()) / 2); 0269 0270 m_treeView->setFixedSize(viewSize); 0271 m_treeView->move(xPos, yPos); 0272 } 0273 0274 void TabSwitcherPluginView::switchToClicked(const QModelIndex &index) 0275 { 0276 m_treeView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); 0277 activateView(index); 0278 } 0279 0280 void TabSwitcherPluginView::activateView(const QModelIndex &index) 0281 { 0282 Q_UNUSED(index) 0283 0284 // guard against empty selection 0285 if (m_treeView->selectionModel()->selectedRows().isEmpty()) { 0286 return; 0287 } 0288 0289 const int row = m_treeView->selectionModel()->selectedRows().first().row(); 0290 0291 auto doc = m_model->item(row); 0292 if (doc.doc()) { 0293 m_mainWindow->activateView(doc.doc()); 0294 } else if (doc.widget()) { 0295 auto mw = m_mainWindow->window(); 0296 QMetaObject::invokeMethod(mw, "activateWidget", Q_ARG(QWidget *, doc.widget())); 0297 } 0298 0299 m_treeView->hide(); 0300 } 0301 0302 void TabSwitcherPluginView::closeView() 0303 { 0304 if (m_treeView->selectionModel()->selectedRows().isEmpty()) { 0305 return; 0306 } 0307 0308 const int row = m_treeView->selectionModel()->selectedRows().first().row(); 0309 auto doc = m_model->item(row); 0310 if (doc.doc()) { 0311 KTextEditor::Editor::instance()->application()->closeDocument(doc.doc()); 0312 } else if (doc.widget()) { 0313 auto mw = m_mainWindow->window(); 0314 QMetaObject::invokeMethod(mw, "removeWidget", Q_ARG(QWidget *, doc.widget())); 0315 } 0316 } 0317 0318 // required for TabSwitcherPluginFactory vtable 0319 #include "tabswitcher.moc" 0320 0321 #include "moc_tabswitcher.cpp"