File indexing completed on 2024-04-28 04:37:19

0001 /*
0002     SPDX-FileCopyrightText: 2002 Falk Brettschneider <falkbr@kdevelop.org>
0003     SPDX-FileCopyrightText: 2003 John Firebaugh <jfirebaugh@kde.org>
0004     SPDX-FileCopyrightText: 2006 Adam Treat <treat@kde.org>
0005     SPDX-FileCopyrightText: 2006, 2007 Alexander Dymo <adymo@kdevelop.org>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "mainwindow.h"
0011 #include "mainwindow_p.h"
0012 
0013 #include <QDBusConnection>
0014 #include <QDomElement>
0015 #include <QDragEnterEvent>
0016 #include <QDropEvent>
0017 #include <QMenu>
0018 #include <QMenuBar>
0019 #include <QMimeData>
0020 #include <QUrl>
0021 
0022 #include <KActionCollection>
0023 #include <KLocalizedString>
0024 #include <KShortcutsDialog>
0025 #include <KTextEditor/Document>
0026 #include <KTextEditor/View>
0027 #include <KWindowSystem>
0028 #include <KXMLGUIFactory>
0029 
0030 #include <sublime/area.h>
0031 #include "shellextension.h"
0032 #include "partcontroller.h"
0033 #include "plugincontroller.h"
0034 #include "projectcontroller.h"
0035 #include "uicontroller.h"
0036 #include "documentcontroller.h"
0037 #include "workingsetcontroller.h"
0038 #include "sessioncontroller.h"
0039 #include "sourceformattercontroller.h"
0040 #include "areadisplay.h"
0041 #include "project.h"
0042 #include "debug.h"
0043 #include "uiconfig.h"
0044 #include "ktexteditorpluginintegration.h"
0045 
0046 #include <interfaces/isession.h>
0047 #include <interfaces/iprojectcontroller.h>
0048 #include <sublime/view.h>
0049 #include <sublime/document.h>
0050 #include <sublime/urldocument.h>
0051 #include <sublime/container.h>
0052 #include <util/path.h>
0053 #include <util/widgetcolorizer.h>
0054 
0055 using namespace KDevelop;
0056 
0057 namespace {
0058 
0059 QColor defaultColor(const QPalette& palette)
0060 {
0061     return palette.windowText().color();
0062 }
0063 
0064 QColor colorForDocument(const QUrl& url, const QPalette& palette, const QColor& defaultColor)
0065 {
0066     auto project = Core::self()->projectController()->findProjectForUrl(url);
0067     if (!project)
0068         return defaultColor;
0069 
0070     return WidgetColorizer::colorForId(qHash(project->path()), palette);
0071 }
0072 
0073 }
0074 
0075 void MainWindow::applyMainWindowSettings(const KConfigGroup& config)
0076 {
0077     Q_D(MainWindow);
0078 
0079     if(!d->changingActiveView())
0080         KXmlGuiWindow::applyMainWindowSettings(config);
0081 }
0082 
0083 void MainWindow::createGUI(KParts::Part* part)
0084 {
0085     Sublime::MainWindow::setWindowTitleHandling(false);
0086     Sublime::MainWindow::createGUI(part);
0087 }
0088 
0089 void MainWindow::initializeCorners()
0090 {
0091     const KConfigGroup cg = KSharedConfig::openConfig()->group( "UiSettings" );
0092     const int bottomleft = cg.readEntry( "BottomLeftCornerOwner", 0 );
0093     const int bottomright = cg.readEntry( "BottomRightCornerOwner", 0 );
0094     qCDebug(SHELL) << "Bottom Left:" << bottomleft;
0095     qCDebug(SHELL) << "Bottom Right:" << bottomright;
0096 
0097     // 0 means vertical dock (left, right), 1 means horizontal dock( top, bottom )
0098     if( bottomleft == 0 )
0099         setCorner( Qt::BottomLeftCorner, Qt::LeftDockWidgetArea );
0100     else if( bottomleft == 1 )
0101         setCorner( Qt::BottomLeftCorner, Qt::BottomDockWidgetArea );
0102 
0103     if( bottomright == 0 )
0104         setCorner( Qt::BottomRightCorner, Qt::RightDockWidgetArea );
0105     else if( bottomright == 1 )
0106         setCorner( Qt::BottomRightCorner, Qt::BottomDockWidgetArea );
0107 }
0108 
0109 MainWindow::MainWindow( Sublime::Controller *parent, Qt::WindowFlags flags )
0110         : Sublime::MainWindow( parent, flags )
0111 {
0112     QDBusConnection::sessionBus().registerObject( QStringLiteral("/kdevelop/MainWindow"),
0113         this, QDBusConnection::ExportScriptableSlots );
0114 
0115     setAcceptDrops( true );
0116     initializeCorners();
0117 
0118     setObjectName( QStringLiteral("MainWindow") );
0119     d_ptr = new MainWindowPrivate(this);
0120 
0121     Q_D(MainWindow);
0122 
0123     setStandardToolBarMenuEnabled( true );
0124     d->setupActions();
0125 
0126     if( !ShellExtension::getInstance()->xmlFile().isEmpty() )
0127     {
0128         setXMLFile( ShellExtension::getInstance() ->xmlFile() );
0129     }
0130 
0131     menuBar()->setCornerWidget(new AreaDisplay(this), Qt::TopRightCorner);
0132 }
0133 
0134 MainWindow::~ MainWindow()
0135 {
0136     if (memberList().count() == 1) {
0137         // We're closing down...
0138         Core::self()->shutdown();
0139     }
0140 
0141     delete d_ptr;
0142 }
0143 
0144 KTextEditorIntegration::MainWindow *MainWindow::kateWrapper() const
0145 {
0146     Q_D(const MainWindow);
0147 
0148     return d->kateWrapper();
0149 }
0150 
0151 void MainWindow::split(Qt::Orientation orientation)
0152 {
0153     Q_D(MainWindow);
0154 
0155     d->split(orientation);
0156 }
0157 
0158 void MainWindow::ensureVisible()
0159 {
0160     if (isMinimized()) {
0161         if (isMaximized()) {
0162             showMaximized();
0163         } else {
0164             showNormal();
0165         }
0166     }
0167     KWindowSystem::forceActiveWindow(winId());
0168 }
0169 
0170 QAction* MainWindow::createCustomElement(QWidget* parent, int index, const QDomElement& element)
0171 {
0172     QAction* before = nullptr;
0173     if (index > 0 && index < parent->actions().count())
0174         before = parent->actions().at(index);
0175 
0176     //KDevelop needs to ensure that separators defined as <Separator style="visible" />
0177     //are always shown in the menubar. For those, we create special disabled actions
0178     //instead of calling QMenuBar::addSeparator() because menubar separators are ignored
0179     if (element.tagName().compare(QLatin1String("separator"), Qt::CaseInsensitive) == 0
0180             && element.attribute(QStringLiteral("style")) == QLatin1String("visible")) {
0181         if ( auto* bar = qobject_cast<QMenuBar*>( parent ) ) {
0182             auto* separatorAction = new QAction(QStringLiteral("|"), this);
0183             bar->insertAction( before, separatorAction );
0184             separatorAction->setDisabled(true);
0185             return separatorAction;
0186         }
0187     }
0188 
0189     return KXMLGUIBuilder::createCustomElement(parent, index, element);
0190 }
0191 
0192 bool KDevelop::MainWindow::event( QEvent* ev )
0193 {
0194     if ( ev->type() == QEvent::PaletteChange )
0195         updateAllTabColors();
0196     return Sublime::MainWindow::event(ev);
0197 }
0198 
0199 void MainWindow::dragEnterEvent( QDragEnterEvent* ev )
0200 {
0201     const QMimeData* mimeData = ev->mimeData();
0202     if (mimeData->hasUrls()) {
0203         ev->acceptProposedAction();
0204     } else if (mimeData->hasText()) {
0205         // also take text which contains a URL
0206         const QUrl url = QUrl::fromUserInput(mimeData->text());
0207         if (url.isValid()) {
0208             ev->acceptProposedAction();
0209         }
0210     }
0211 }
0212 
0213 void MainWindow::dropEvent( QDropEvent* ev )
0214 {
0215     Sublime::View* dropToView = viewForPosition(mapToGlobal(ev->pos()));
0216     if(dropToView)
0217         activateView(dropToView);
0218 
0219     QList<QUrl> urls;
0220 
0221     const QMimeData* mimeData = ev->mimeData();
0222     if (mimeData->hasUrls()) {
0223         urls = mimeData->urls();
0224     } else if (mimeData->hasText()) {
0225         const QUrl url = QUrl::fromUserInput(mimeData->text());
0226         if (url.isValid()) {
0227             urls << url;
0228         }
0229     }
0230 
0231     bool eventUsed = false;
0232     if (urls.size() == 1) {
0233         eventUsed = Core::self()->projectControllerInternal()->fetchProjectFromUrl(urls.at(0), ProjectController::NoFetchFlags);
0234     }
0235 
0236     if (!eventUsed) {
0237         for(const auto& url : qAsConst(urls)) {
0238             Core::self()->documentController()->openDocument(url);
0239         }
0240     }
0241 
0242     ev->acceptProposedAction();
0243 }
0244 
0245 void MainWindow::loadSettings()
0246 {
0247     qCDebug(SHELL) << "Loading Settings";
0248     initializeCorners();
0249 
0250     updateAllTabColors();
0251 
0252     Sublime::MainWindow::loadSettings();
0253 }
0254 
0255 void MainWindow::configureShortcuts()
0256 {
0257     ///Workaround for a problem with the actions: Always start the shortcut-configuration in the first mainwindow, then propagate the updated
0258     ///settings into the other windows
0259 
0260 
0261 // We need to bring up the shortcut dialog ourself instead of
0262 //      Core::self()->uiControllerInternal()->mainWindows()[0]->guiFactory()->configureShortcuts();
0263 // so we can connect to the saved() signal to propagate changes in the editor shortcuts
0264 
0265    KShortcutsDialog dlg(KShortcutsEditor::AllActions, KShortcutsEditor::LetterShortcutsAllowed, this);
0266 
0267     const auto firstMainWindowClientsBefore = Core::self()->uiControllerInternal()->mainWindows()[0]->guiFactory()->clients();
0268     for (KXMLGUIClient* client : firstMainWindowClientsBefore) {
0269         if(client && !client->xmlFile().isEmpty())
0270             dlg.addCollection( client->actionCollection() );
0271     }
0272 
0273     connect(&dlg, &KShortcutsDialog::saved, this, &MainWindow::shortcutsChanged);
0274     dlg.configure(true);
0275 
0276     QMap<QString, QKeySequence> shortcuts;
0277     // querying again just in case something changed behind our back
0278     const auto firstMainWindowClientsAfter = Core::self()->uiControllerInternal()->mainWindows()[0]->guiFactory()->clients();
0279     for (KXMLGUIClient* client : firstMainWindowClientsAfter) {
0280         const auto actions = client->actionCollection()->actions();
0281         for (QAction* action : actions) {
0282             if(!action->objectName().isEmpty()) {
0283                 shortcuts[action->objectName()] = action->shortcut();
0284             }
0285         }
0286     }
0287 
0288     for(int a = 1; a < Core::self()->uiControllerInternal()->mainWindows().size(); ++a) {
0289         const auto clients = Core::self()->uiControllerInternal()->mainWindows()[a]->guiFactory()->clients();
0290         for (KXMLGUIClient* client : clients) {
0291             const auto actions = client->actionCollection()->actions();
0292             for (QAction* action : actions) {
0293                 qCDebug(SHELL) << "transferring setting shortcut for" << action->objectName();
0294                 const auto shortcutIt = shortcuts.constFind(action->objectName());
0295                 if (shortcutIt != shortcuts.constEnd()) {
0296                     action->setShortcut(*shortcutIt);
0297                 }
0298             }
0299         }
0300     }
0301 
0302 }
0303 
0304 void MainWindow::shortcutsChanged()
0305 {
0306     KTextEditor::View *activeClient = Core::self()->documentController()->activeTextDocumentView();
0307     if(!activeClient)
0308         return;
0309 
0310     const auto documents = Core::self()->documentController()->openDocuments();
0311     for (IDocument* doc : documents) {
0312         KTextEditor::Document *textDocument = doc->textDocument();
0313         if (textDocument) {
0314             const auto views = textDocument->views();
0315             for (KTextEditor::View* client : views) {
0316                 if (client != activeClient) {
0317                     client->reloadXML();
0318                 }
0319             }
0320         }
0321     }
0322 }
0323 
0324 
0325 void MainWindow::initialize()
0326 {
0327     Q_D(MainWindow);
0328 
0329     KStandardAction::keyBindings(this, SLOT(configureShortcuts()), actionCollection());
0330     setupGUI(ToolBar | Save);
0331     createGUI(nullptr);
0332 
0333     Core::self()->partController()->addManagedTopLevelWidget(this);
0334     qCDebug(SHELL) << "Adding plugin-added connection";
0335 
0336     connect( Core::self()->pluginController(), &IPluginController::pluginLoaded,
0337              d, &MainWindowPrivate::addPlugin);
0338     connect( Core::self()->pluginController(), &IPluginController::pluginUnloaded,
0339              d, &MainWindowPrivate::removePlugin);
0340     connect( Core::self()->partController(), &IPartController::activePartChanged,
0341         d, &MainWindowPrivate::activePartChanged);
0342     connect( this, &MainWindow::activeViewChanged,
0343         d, &MainWindowPrivate::changeActiveView);
0344     connect(Core::self()->sourceFormatterControllerInternal(), &SourceFormatterController::hasFormattersChanged,
0345              d, &MainWindowPrivate::updateSourceFormatterGuiClient);
0346 
0347     const auto plugins = Core::self()->pluginController()->loadedPlugins();
0348     for (IPlugin* plugin : plugins) {
0349         d->addPlugin(plugin);
0350     }
0351 
0352     guiFactory()->addClient(Core::self()->sessionController());
0353     if (Core::self()->sourceFormatterControllerInternal()->hasFormatters()) {
0354         guiFactory()->addClient(Core::self()->sourceFormatterControllerInternal());
0355     }
0356 
0357     // Needed to re-plug the actions from the sessioncontroller as xmlguiclients don't
0358     // seem to remember which actions where plugged in.
0359     Core::self()->sessionController()->updateXmlGuiActionList();
0360 
0361     d->setupGui();
0362 
0363     qRegisterMetaType<QPointer<KTextEditor::Document>>();
0364 
0365     //Queued so we process it with some delay, to make sure the rest of the UI has already adapted
0366     connect(Core::self()->documentController(), &IDocumentController::documentActivated,
0367             // Use a queued connection, because otherwise the view is not yet fully set up
0368             // but wrap the document in a smart pointer to guard against crashes when it
0369             // gets deleted in the meantime
0370             this, [this](IDocument *doc) {
0371                 const auto textDocument = QPointer<KTextEditor::Document>(doc->textDocument());
0372                 QMetaObject::invokeMethod(this, "documentActivated", Qt::QueuedConnection,
0373                                           Q_ARG(QPointer<KTextEditor::Document>, textDocument));
0374             });
0375 
0376     connect(Core::self()->documentController(), &IDocumentController::documentClosed, this, &MainWindow::updateCaption, Qt::QueuedConnection);
0377     connect(Core::self()->documentController(), &IDocumentController::documentUrlChanged, this, &MainWindow::updateCaption, Qt::QueuedConnection);
0378     connect(Core::self()->documentController(), &IDocumentController::documentStateChanged, this, &MainWindow::updateCaption, Qt::QueuedConnection);
0379     connect(Core::self()->sessionController()->activeSession(), &ISession::sessionUpdated, this, &MainWindow::updateCaption);
0380     // if currently viewed document is part of project, trigger update of full path to project prefixed one
0381     connect(Core::self()->projectController(), &ProjectController::projectOpened, this, &MainWindow::updateCaption, Qt::QueuedConnection);
0382 
0383     connect(Core::self()->documentController(), &IDocumentController::documentOpened, this, &MainWindow::updateTabColor);
0384     connect(Core::self()->documentController(), &IDocumentController::documentUrlChanged, this, &MainWindow::updateTabColor);
0385     connect(this, &Sublime::MainWindow::viewAdded, this, &MainWindow::updateAllTabColors);
0386     connect(Core::self()->projectController(), &ProjectController::projectOpened, this, &MainWindow::updateAllTabColors, Qt::QueuedConnection);
0387 
0388     updateCaption();
0389 }
0390 
0391 void MainWindow::cleanup()
0392 {
0393 }
0394 
0395 void MainWindow::setVisible( bool visible )
0396 {
0397     KXmlGuiWindow::setVisible( visible );
0398     emit finishedLoading();
0399 }
0400 
0401 bool MainWindow::queryClose()
0402 {
0403     if (!Core::self()->documentControllerInternal()->saveAllDocumentsForWindow(this, IDocument::Default))
0404         return false;
0405 
0406     return Sublime::MainWindow::queryClose();
0407 }
0408 
0409 void MainWindow::documentActivated(const QPointer<KTextEditor::Document>& textDocument)
0410 {
0411     Q_D(MainWindow);
0412 
0413     updateCaption();
0414 
0415     // update active document connection
0416     disconnect(d->activeDocumentReadWriteConnection);
0417     if (textDocument) {
0418         d->activeDocumentReadWriteConnection = connect(textDocument, &KTextEditor::Document::readWriteChanged,
0419                                                     this, &MainWindow::updateCaption);
0420     }
0421 }
0422 
0423 void MainWindow::updateCaption()
0424 {
0425     QString title;
0426     QString localFilePath;
0427     bool isDocumentModified = false;
0428 
0429     if(area()->activeView())
0430     {
0431         Sublime::Document* doc = area()->activeView()->document();
0432         auto* urlDoc = qobject_cast<Sublime::UrlDocument*>(doc);
0433         if(urlDoc)
0434         {
0435             if (urlDoc->url().isLocalFile()) {
0436                 localFilePath = urlDoc->url().toLocalFile();
0437             }
0438             title += Core::self()->projectController()->prettyFileName(urlDoc->url(), KDevelop::IProjectController::FormatPlain);
0439         }
0440         else
0441             title += doc->title();
0442 
0443         auto iDoc = qobject_cast<IDocument*>(doc);
0444         if (iDoc && iDoc->textDocument() && !iDoc->textDocument()->isReadWrite()) {
0445             title += i18n(" (read only)");
0446         }
0447 
0448         title += QLatin1String(" [*]"); // [*] is placeholder for modified state, cmp. QWidget::windowModified
0449 
0450         isDocumentModified = iDoc && (iDoc->state() != IDocument::Clean);
0451     }
0452 
0453     const auto activeSession = Core::self()->sessionController()->activeSession();
0454     const QString sessionTitle = activeSession ? activeSession->description() : QString();
0455     if (!sessionTitle.isEmpty()) {
0456         if (title.isEmpty()) {
0457             title = sessionTitle;
0458         } else {
0459             title = sessionTitle + QLatin1String(" - [ ") + title + QLatin1Char(']');
0460         }
0461     }
0462 
0463     // Workaround for a bug observed on macOS with Qt 5.9.8 (TODO: test with newer Qt, report bug):
0464     // Ensure to call setCaption() (thus implicitly setWindowTitle()) before
0465     // setWindowModified() & setWindowFilePath().
0466     // Otherwise, if the state will change "modified" from true to false as well change the title string,
0467     // calling setWindowTitle() last results in the "modified" indicator==asterisk becoming part of the
0468     // displayed window title somehow.
0469     // Other platforms so far not known to be affected, any order of calls seems fine.
0470     setCaption(title);
0471     setWindowModified(isDocumentModified);
0472     setWindowFilePath(localFilePath);
0473 }
0474 
0475 void MainWindow::updateAllTabColors()
0476 {
0477     auto documentController = Core::self()->documentController();
0478     if (!documentController)
0479         return;
0480 
0481     const auto defaultColor = ::defaultColor(palette());
0482     if (UiConfig::colorizeByProject()) {
0483         QHash<const Sublime::View*, QColor> viewColors;
0484         const auto containers = this->containers();
0485         for (auto* container : containers) {
0486             const auto views = container->views();
0487             viewColors.reserve(views.size());
0488             viewColors.clear();
0489             for (auto view : views) {
0490                 const auto urlDoc = qobject_cast<Sublime::UrlDocument*>(view->document());
0491                 if (urlDoc) {
0492                     viewColors[view] = colorForDocument(urlDoc->url(), palette(), defaultColor);
0493                 }
0494             }
0495             container->setTabColors(viewColors);
0496         }
0497     } else {
0498         const auto containers = this->containers();
0499         for (auto* container : containers) {
0500             container->resetTabColors(defaultColor);
0501         }
0502     }
0503 }
0504 
0505 void MainWindow::updateTabColor(IDocument* doc)
0506 {
0507     if (!UiConfig::self()->colorizeByProject())
0508         return;
0509 
0510     const auto color = colorForDocument(doc->url(), palette(), defaultColor(palette()));
0511     const auto containers = this->containers();
0512     for (auto* container : containers) {
0513         const auto views = container->views();
0514         for (auto* view : views) {
0515             const auto urlDoc = qobject_cast<Sublime::UrlDocument*>(view->document());
0516             if (urlDoc && urlDoc->url() == doc->url()) {
0517                 container->setTabColor(view, color);
0518             }
0519         }
0520     }
0521 }
0522 
0523 void MainWindow::registerStatus(QObject* status)
0524 {
0525     Q_D(MainWindow);
0526 
0527     d->registerStatus(status);
0528 }
0529 
0530 void MainWindow::initializeStatusBar()
0531 {
0532     Q_D(MainWindow);
0533 
0534     d->setupStatusBar();
0535 }
0536 
0537 void MainWindow::showErrorMessage(const QString& message, int timeout)
0538 {
0539     Q_D(MainWindow);
0540 
0541     d->showErrorMessage(message, timeout);
0542 }
0543 
0544 void MainWindow::tabContextMenuRequested(Sublime::View* view, QMenu* menu)
0545 {
0546     Q_D(MainWindow);
0547 
0548     Sublime::MainWindow::tabContextMenuRequested(view, menu);
0549     d->tabContextMenuRequested(view, menu);
0550 }
0551 
0552 void MainWindow::tabToolTipRequested(Sublime::View* view, Sublime::Container* container, int tab)
0553 {
0554     Q_D(MainWindow);
0555 
0556     d->tabToolTipRequested(view, container, tab);
0557 }
0558 
0559 void MainWindow::dockBarContextMenuRequested(Qt::DockWidgetArea area, const QPoint& position)
0560 {
0561     Q_D(MainWindow);
0562 
0563     d->dockBarContextMenuRequested(area, position);
0564 }
0565 
0566 void MainWindow::newTabRequested()
0567 {
0568     Q_D(MainWindow);
0569 
0570     Sublime::MainWindow::newTabRequested();
0571 
0572     d->fileNew();
0573 }
0574 
0575 #include "moc_mainwindow.cpp"