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"