1 /* This file is part of the KDE project
2    SPDX-FileCopyrightText: 2001 Christoph Cullmann <cullmann@kde.org>
3    SPDX-FileCopyrightText: 2001 Joseph Wenninger <jowenn@kde.org>
4    SPDX-FileCopyrightText: 2001, 2005 Anders Lund <anders.lund@lund.tdcadsl.dk>
5 
6    SPDX-License-Identifier: LGPL-2.0-only
7 */
8 #include "kateviewspace.h"
9 
10 #include "kateapp.h"
11 #include "katedebug.h"
12 #include "katedocmanager.h"
13 #include "katefileactions.h"
14 #include "katemainwindow.h"
15 #include "katesessionmanager.h"
16 #include "kateupdatedisabler.h"
17 #include "kateviewmanager.h"
18 #include <KActionCollection>
19 
20 #include <KAcceleratorManager>
21 #include <KConfigGroup>
22 #include <KLocalizedString>
23 
24 #include <QApplication>
25 #include <QClipboard>
26 #include <QHelpEvent>
27 #include <QMenu>
28 #include <QMessageBox>
29 #include <QStackedWidget>
30 #include <QToolButton>
31 #include <QToolTip>
32 #include <QWhatsThis>
33 
34 #include <KTextEditor/Editor>
35 
36 // BEGIN KateViewSpace
KateViewSpace(KateViewManager * viewManager,QWidget * parent,const char * name)37 KateViewSpace::KateViewSpace(KateViewManager *viewManager, QWidget *parent, const char *name)
38     : QWidget(parent)
39     , m_viewManager(viewManager)
40     , m_isActiveSpace(false)
41 {
42     setObjectName(QString::fromLatin1(name));
43     QVBoxLayout *layout = new QVBoxLayout(this);
44     layout->setSpacing(0);
45     layout->setContentsMargins(0, 0, 0, 0);
46 
47     // BEGIN tab bar
48     QHBoxLayout *hLayout = new QHBoxLayout();
49     hLayout->setSpacing(0);
50     hLayout->setContentsMargins(0, 0, 0, 0);
51 
52     // add left <-> right history buttons
53     m_historyBack = new QToolButton(this);
54     m_historyBack->setToolTip(i18n("Go Back"));
55     m_historyBack->setIcon(QIcon::fromTheme(QStringLiteral("arrow-left")));
56     m_historyBack->setAutoRaise(true);
57     KAcceleratorManager::setNoAccel(m_historyBack);
58     m_historyBack->installEventFilter(this); // on click, active this view space
59     hLayout->addWidget(m_historyBack);
60     connect(m_historyBack, &QToolButton::clicked, this, [this] {
61         goBack();
62     });
63     m_historyBack->setEnabled(false);
64 
65     m_historyForward = new QToolButton(this);
66     m_historyForward->setIcon(QIcon::fromTheme(QStringLiteral("arrow-right")));
67     m_historyForward->setToolTip(i18n("Go Forward"));
68     m_historyForward->setAutoRaise(true);
69     KAcceleratorManager::setNoAccel(m_historyForward);
70     m_historyForward->installEventFilter(this); // on click, active this view space
71     hLayout->addWidget(m_historyForward);
72     connect(m_historyForward, &QToolButton::clicked, this, [this] {
73         goForward();
74     });
75     m_historyForward->setEnabled(false);
76 
77     // add tab bar
78     m_tabBar = new KateTabBar(this);
79     connect(m_tabBar, &KateTabBar::currentChanged, this, &KateViewSpace::changeView);
80     connect(m_tabBar, &KateTabBar::tabCloseRequested, this, &KateViewSpace::closeTabRequest, Qt::QueuedConnection);
81     connect(m_tabBar, &KateTabBar::contextMenuRequest, this, &KateViewSpace::showContextMenu, Qt::QueuedConnection);
82     connect(m_tabBar, &KateTabBar::newTabRequested, this, &KateViewSpace::createNewDocument);
83     connect(m_tabBar, SIGNAL(activateViewSpaceRequested()), this, SLOT(makeActive()));
84     hLayout->addWidget(m_tabBar);
85 
86     // add quick open
87     m_quickOpen = new QToolButton(this);
88     m_quickOpen->setAutoRaise(true);
89     KAcceleratorManager::setNoAccel(m_quickOpen);
90     m_quickOpen->installEventFilter(this); // on click, active this view space
91     hLayout->addWidget(m_quickOpen);
92 
93     // forward tab bar quick open action to global quick open action
94     QAction *bridge = new QAction(QIcon::fromTheme(QStringLiteral("tab-duplicate")), i18nc("indicator for more documents", "+%1", 100), this);
95     m_quickOpen->setDefaultAction(bridge);
96     QAction *quickOpen = m_viewManager->mainWindow()->actionCollection()->action(QStringLiteral("view_quick_open"));
97     Q_ASSERT(quickOpen);
98     bridge->setToolTip(quickOpen->toolTip());
99     bridge->setWhatsThis(i18n("Click here to switch to the Quick Open view."));
100     connect(bridge, &QAction::triggered, quickOpen, &QAction::trigger);
101 
102     // add vertical split view space
103     m_split = new QToolButton(this);
104     m_split->setAutoRaise(true);
105     m_split->setPopupMode(QToolButton::InstantPopup);
106     m_split->setIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right")));
107     m_split->addAction(m_viewManager->mainWindow()->actionCollection()->action(QStringLiteral("view_split_vert")));
108     m_split->addAction(m_viewManager->mainWindow()->actionCollection()->action(QStringLiteral("view_split_horiz")));
109     m_split->addAction(m_viewManager->mainWindow()->actionCollection()->action(QStringLiteral("view_close_current_space")));
110     m_split->addAction(m_viewManager->mainWindow()->actionCollection()->action(QStringLiteral("view_close_others")));
111     m_split->addAction(m_viewManager->mainWindow()->actionCollection()->action(QStringLiteral("view_hide_others")));
112     m_split->setWhatsThis(i18n("Control view space splitting"));
113     m_split->installEventFilter(this); // on click, active this view space
114     hLayout->addWidget(m_split);
115 
116     layout->addLayout(hLayout);
117     // END tab bar
118 
119     stack = new QStackedWidget(this);
120     stack->setFocus();
121     stack->setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding));
122     layout->addWidget(stack);
123 
124     m_group.clear();
125 
126     // connect signal to hide/show statusbar
127     connect(m_viewManager->mainWindow(), &KateMainWindow::statusBarToggled, this, &KateViewSpace::statusBarToggled);
128     connect(m_viewManager->mainWindow(), &KateMainWindow::tabBarToggled, this, &KateViewSpace::tabBarToggled);
129 
130     // init the bars...
131     statusBarToggled();
132     tabBarToggled();
133 }
134 
eventFilter(QObject * obj,QEvent * event)135 bool KateViewSpace::eventFilter(QObject *obj, QEvent *event)
136 {
137     QToolButton *button = qobject_cast<QToolButton *>(obj);
138 
139     // quick open button: show tool tip with shortcut
140     if (button == m_quickOpen && event->type() == QEvent::ToolTip) {
141         QHelpEvent *e = static_cast<QHelpEvent *>(event);
142         QAction *quickOpen = m_viewManager->mainWindow()->actionCollection()->action(QStringLiteral("view_quick_open"));
143         Q_ASSERT(quickOpen);
144         QToolTip::showText(e->globalPos(), button->toolTip() + QStringLiteral(" (%1)").arg(quickOpen->shortcut().toString()), button);
145         return true;
146     }
147 
148     // quick open button: What's This
149     if (button == m_quickOpen && event->type() == QEvent::WhatsThis) {
150         QHelpEvent *e = static_cast<QHelpEvent *>(event);
151         const int hiddenDocs = hiddenDocuments();
152         QString helpText = (hiddenDocs == 0)
153             ? i18n("Click here to switch to the Quick Open view.")
154             : i18np("Currently, there is one more document open. To see all open documents, switch to the Quick Open view by clicking here.",
155                     "Currently, there are %1 more documents open. To see all open documents, switch to the Quick Open view by clicking here.",
156                     hiddenDocs);
157         QWhatsThis::showText(e->globalPos(), helpText, m_quickOpen);
158         return true;
159     }
160 
161     // on mouse press on view space bar tool buttons: activate this space
162     if (button && !isActiveSpace() && event->type() == QEvent::MouseButtonPress) {
163         m_viewManager->setActiveSpace(this);
164         if (currentView()) {
165             m_viewManager->activateView(currentView()->document());
166         }
167     }
168     return false;
169 }
170 
statusBarToggled()171 void KateViewSpace::statusBarToggled()
172 {
173     KateUpdateDisabler updatesDisabled(m_viewManager->mainWindow());
174     for (const auto &[_, view] : m_docToView) {
175         Q_UNUSED(_)
176         view->setStatusBarEnabled(m_viewManager->mainWindow()->showStatusBar());
177     }
178 }
179 
tabBarToggled()180 void KateViewSpace::tabBarToggled()
181 {
182     KateUpdateDisabler updatesDisabled(m_viewManager->mainWindow());
183     m_historyBack->setVisible(m_viewManager->mainWindow()->showTabBar());
184     m_historyForward->setVisible(m_viewManager->mainWindow()->showTabBar());
185     m_tabBar->setVisible(m_viewManager->mainWindow()->showTabBar());
186     m_split->setVisible(m_viewManager->mainWindow()->showTabBar());
187     m_quickOpen->setVisible(m_viewManager->mainWindow()->showTabBar());
188 }
189 
createView(KTextEditor::Document * doc)190 KTextEditor::View *KateViewSpace::createView(KTextEditor::Document *doc)
191 {
192     // should only be called if a view does not yet exist
193     Q_ASSERT(m_docToView.find(doc) == m_docToView.end());
194 
195     /**
196      * Create a fresh view
197      */
198     KTextEditor::View *v = doc->createView(stack, m_viewManager->mainWindow()->wrapper());
199 
200     // set status bar to right state
201     v->setStatusBarEnabled(m_viewManager->mainWindow()->showStatusBar());
202 
203     // restore the config of this view if possible
204     if (!m_group.isEmpty()) {
205         QString fn = v->document()->url().toString();
206         if (!fn.isEmpty()) {
207             QString vgroup = QStringLiteral("%1 %2").arg(m_group, fn);
208 
209             KateSession::Ptr as = KateApp::self()->sessionManager()->activeSession();
210             if (as->config() && as->config()->hasGroup(vgroup)) {
211                 KConfigGroup cg(as->config(), vgroup);
212                 v->readSessionConfig(cg);
213             }
214         }
215     }
216 
217     connect(v, &KTextEditor::View::cursorPositionChanged, this, [this](KTextEditor::View *view, const KTextEditor::Cursor &newPosition) {
218         if (view && view->document())
219             addPositionToHistory(view->document()->url(), newPosition);
220     });
221 
222     // register document, it is shown below through showView() then
223     registerDocument(doc);
224 
225     // view shall still be not registered
226     Q_ASSERT(m_docToView.find(doc) == m_docToView.end());
227 
228     // insert View into stack
229     stack->addWidget(v);
230     m_docToView[doc] = v;
231     showView(v);
232 
233     return v;
234 }
235 
removeView(KTextEditor::View * v)236 void KateViewSpace::removeView(KTextEditor::View *v)
237 {
238     // remove view mappings
239     auto it = m_docToView.find(v->document());
240     Q_ASSERT(it != m_docToView.end());
241     m_docToView.erase(it);
242 
243     // ...and now: remove from view space
244     stack->removeWidget(v);
245 
246     // switch to most recently used rather than letting stack choose one
247     // (last element could well be v->document() being removed here)
248     for (auto rit = m_registeredDocuments.rbegin(); rit != m_registeredDocuments.rend(); ++rit) {
249         auto it = m_docToView.find(*rit);
250         if (it != m_docToView.end()) {
251             showView(*rit);
252             break;
253         }
254     }
255 }
256 
showView(KTextEditor::Document * document)257 bool KateViewSpace::showView(KTextEditor::Document *document)
258 {
259     /**
260      * nothing can be done if we have now view ready here
261      */
262     auto it = m_docToView.find(document);
263     if (it == m_docToView.end()) {
264         return false;
265     }
266 
267     /**
268      * update mru list order
269      */
270     const int index = m_registeredDocuments.lastIndexOf(document);
271     // move view to end of list
272     if (index < 0) {
273         return false;
274     }
275     // move view to end of list
276     m_registeredDocuments.removeAt(index);
277     m_registeredDocuments.append(document);
278 
279     /**
280      * show the wanted view
281      */
282     KTextEditor::View *kv = it->second;
283     stack->setCurrentWidget(kv);
284     kv->show();
285 
286     /**
287      * we need to avoid that below's index changes will mess with current view
288      */
289     disconnect(m_tabBar, &KateTabBar::currentChanged, this, &KateViewSpace::changeView);
290 
291     /**
292      * follow current view
293      */
294     m_tabBar->setCurrentDocument(document);
295 
296     // track tab changes again
297     connect(m_tabBar, &KateTabBar::currentChanged, this, &KateViewSpace::changeView);
298     return true;
299 }
300 
changeView(int idx)301 void KateViewSpace::changeView(int idx)
302 {
303     if (idx == -1) {
304         return;
305     }
306 
307     // make sure we open the view in this view space
308     if (!isActiveSpace()) {
309         m_viewManager->setActiveSpace(this);
310     }
311 
312     KTextEditor::Document *doc = m_tabBar->tabDocument(idx);
313     if (!doc) {
314         auto w = m_tabBar->tabData(idx).value<QWidget *>();
315         if (!w) {
316             Q_ASSERT(false);
317             return;
318         }
319         stack->setCurrentWidget(w);
320         return;
321     }
322 
323     // tell the view manager to show the view
324     m_viewManager->activateView(doc);
325 }
326 
currentView()327 KTextEditor::View *KateViewSpace::currentView()
328 {
329     // might be 0 if the stack contains no view
330     return qobject_cast<KTextEditor::View *>(stack->currentWidget());
331 }
332 
isActiveSpace()333 bool KateViewSpace::isActiveSpace()
334 {
335     return m_isActiveSpace;
336 }
337 
setActive(bool active)338 void KateViewSpace::setActive(bool active)
339 {
340     m_isActiveSpace = active;
341     m_tabBar->setActive(active);
342 }
343 
makeActive(bool focusCurrentView)344 void KateViewSpace::makeActive(bool focusCurrentView)
345 {
346     if (!isActiveSpace()) {
347         m_viewManager->setActiveSpace(this);
348         if (focusCurrentView && currentView()) {
349             m_viewManager->activateView(currentView()->document());
350         }
351     }
352     Q_ASSERT(isActiveSpace());
353 }
354 
registerDocument(KTextEditor::Document * doc)355 void KateViewSpace::registerDocument(KTextEditor::Document *doc)
356 {
357     /**
358      * ignore request to register something that is already known
359      */
360     if (m_registeredDocuments.contains(doc)) {
361         return;
362     }
363 
364     /**
365      * remember our new document
366      */
367     m_registeredDocuments.insert(0, doc);
368 
369     /**
370      * ensure we cleanup after document is deleted, e.g. we remove the tab bar button
371      */
372     connect(doc, &QObject::destroyed, this, &KateViewSpace::documentDestroyed);
373 
374     /**
375      * register document is used in places that don't like view creation
376      * therefore we must ensure the currentChanged doesn't trigger that
377      */
378     disconnect(m_tabBar, &KateTabBar::currentChanged, this, &KateViewSpace::changeView);
379 
380     /**
381      * create the tab for this document, if necessary
382      */
383     m_tabBar->setCurrentDocument(doc);
384 
385     /**
386      * handle later document state changes
387      */
388     connect(doc, &KTextEditor::Document::documentNameChanged, this, &KateViewSpace::updateDocumentName);
389     connect(doc, &KTextEditor::Document::documentUrlChanged, this, &KateViewSpace::updateDocumentUrl);
390     connect(doc, &KTextEditor::Document::modifiedChanged, this, &KateViewSpace::updateDocumentState);
391 
392     /**
393      * allow signals again, now that the tab is there
394      */
395     connect(m_tabBar, &KateTabBar::currentChanged, this, &KateViewSpace::changeView);
396 }
397 
documentDestroyed(QObject * doc)398 void KateViewSpace::documentDestroyed(QObject *doc)
399 {
400     /**
401      * WARNING: this pointer is half destroyed
402      * only good enough to check pointer value e.g. for hashs
403      */
404     KTextEditor::Document *invalidDoc = static_cast<KTextEditor::Document *>(doc);
405     Q_ASSERT(m_registeredDocuments.contains(invalidDoc));
406     m_registeredDocuments.removeAll(invalidDoc);
407 
408     /**
409      * we shall have no views for this document at this point in time!
410      */
411     Q_ASSERT(m_docToView.find(invalidDoc) == m_docToView.end());
412 
413     // disconnect entirely
414     disconnect(doc, nullptr, this, nullptr);
415 
416     /**
417      * remove the tab for this document, if existing
418      */
419     m_tabBar->removeDocument(invalidDoc);
420 }
421 
updateDocumentName(KTextEditor::Document * doc)422 void KateViewSpace::updateDocumentName(KTextEditor::Document *doc)
423 {
424     // update tab button if available, might not be the case for tab limit set!
425     const int buttonId = m_tabBar->documentIdx(doc);
426     if (buttonId >= 0) {
427         // BUG: 441278 We need to escape the & because it is used for accelerators/shortcut mnemonic by default
428         QString tabName = doc->documentName();
429         tabName.replace(QLatin1Char('&'), QLatin1String("&&"));
430         m_tabBar->setTabText(buttonId, tabName);
431     }
432 }
433 
updateDocumentUrl(KTextEditor::Document * doc)434 void KateViewSpace::updateDocumentUrl(KTextEditor::Document *doc)
435 {
436     // update tab button if available, might not be the case for tab limit set!
437     const int buttonId = m_tabBar->documentIdx(doc);
438     if (buttonId >= 0) {
439         m_tabBar->setTabToolTip(buttonId, doc->url().toDisplayString());
440     }
441 }
442 
updateDocumentState(KTextEditor::Document * doc)443 void KateViewSpace::updateDocumentState(KTextEditor::Document *doc)
444 {
445     QIcon icon;
446     if (doc->isModified()) {
447         icon = QIcon::fromTheme(QStringLiteral("document-save"));
448     }
449 
450     // update tab button if available, might not be the case for tab limit set!
451     const int buttonId = m_tabBar->documentIdx(doc);
452     if (buttonId >= 0) {
453         m_tabBar->setTabIcon(buttonId, icon);
454     }
455 }
456 
closeTabRequest(int idx)457 void KateViewSpace::closeTabRequest(int idx)
458 {
459     auto *doc = m_tabBar->tabDocument(idx);
460     if (!doc) {
461         auto widget = m_tabBar->tabData(idx).value<QWidget *>();
462         if (!widget) {
463             Q_ASSERT(false);
464             return;
465         }
466 
467         bool shouldClose = true;
468         QMetaObject::invokeMethod(widget, "shouldClose", Q_RETURN_ARG(bool, shouldClose));
469         if (shouldClose) {
470             stack->removeWidget(widget);
471             m_tabBar->removeTab(idx);
472         }
473         return;
474     }
475 
476     m_viewManager->slotDocumentClose(doc);
477 }
478 
createNewDocument()479 void KateViewSpace::createNewDocument()
480 {
481     // make sure we open the view in this view space
482     if (!isActiveSpace()) {
483         m_viewManager->setActiveSpace(this);
484     }
485 
486     // create document
487     KTextEditor::Document *doc = KateApp::self()->documentManager()->createDoc();
488 
489     // tell the view manager to show the document
490     m_viewManager->activateView(doc);
491 }
492 
focusPrevTab()493 void KateViewSpace::focusPrevTab()
494 {
495     const int id = m_tabBar->prevTab();
496     if (id >= 0) {
497         changeView(id);
498     }
499 }
500 
focusNextTab()501 void KateViewSpace::focusNextTab()
502 {
503     const int id = m_tabBar->nextTab();
504     if (id >= 0) {
505         changeView(id);
506     }
507 }
508 
addWidgetAsTab(QWidget * widget)509 void KateViewSpace::addWidgetAsTab(QWidget *widget)
510 {
511     stack->addWidget(widget);
512     m_tabBar->setCurrentWidget(widget);
513     stack->setCurrentWidget(widget);
514 }
515 
hasWidgets() const516 bool KateViewSpace::hasWidgets() const
517 {
518     return stack->count() > (int)m_docToView.size();
519 }
520 
currentWidget()521 QWidget *KateViewSpace::currentWidget()
522 {
523     if (auto w = stack->currentWidget()) {
524         return qobject_cast<KTextEditor::View *>(w) ? nullptr : w;
525     }
526     return nullptr;
527 }
528 
closeTabWithWidget(QWidget * widget)529 void KateViewSpace::closeTabWithWidget(QWidget *widget)
530 {
531     if (!widget) {
532         return;
533     }
534 
535     for (int i = 0; i < m_tabBar->count(); ++i) {
536         if (m_tabBar->tabData(i).value<QWidget *>() == widget) {
537             closeTabRequest(i);
538             break;
539         }
540     }
541 }
542 
addPositionToHistory(const QUrl & url,KTextEditor::Cursor c,bool calledExternally)543 void KateViewSpace::addPositionToHistory(const QUrl &url, KTextEditor::Cursor c, bool calledExternally)
544 {
545     // We don't care about invalid urls (Fixed Diff View / Untitled docs)
546     if (!url.isValid()) {
547         return;
548     }
549 
550     // if same line, remove last entry
551     // If new pos is same as "current pos", replace it with new one
552     bool currPosIsInSameLine = false;
553     if (currentLocation < m_locations.size()) {
554         const auto &currentLoc = m_locations.at(currentLocation);
555         currPosIsInSameLine = currentLoc.url == url && currentLoc.cursor.line() == c.line();
556     }
557 
558     // Check if the location is at least "viewLineCount" away from the "current" position in m_locations
559     if (!calledExternally && currentLocation < m_locations.size() && m_locations.at(currentLocation).url == url) {
560         const int currentLine = m_locations.at(currentLocation).cursor.line();
561         const int newPosLine = c.line();
562 
563         const auto view = m_viewManager->activeView();
564         const int viewLineCount = view->lastDisplayedLine() - view->firstDisplayedLine();
565         const int lowerBound = currentLine - viewLineCount;
566         const int upperBound = currentLine + viewLineCount;
567         if (lowerBound <= newPosLine && newPosLine <= upperBound) {
568             if (currPosIsInSameLine) {
569                 m_locations[currentLocation].cursor = c;
570             }
571             return;
572         }
573     }
574 
575     if (currPosIsInSameLine) {
576         m_locations[currentLocation].cursor.setColumn(c.column());
577         return;
578     }
579 
580     // we are in the middle of jumps somewhere?
581     if (!m_locations.empty() && currentLocation + 1 < m_locations.size()) {
582         // erase all forward history
583         m_locations.erase(m_locations.begin() + currentLocation + 1, m_locations.end());
584     }
585 
586     /** this is our new forward **/
587 
588     m_locations.push_back({url, c});
589     // set currentLocation as last
590     currentLocation = m_locations.size() - 1;
591     // disable forward button as we are at the end now
592     m_historyForward->setEnabled(false);
593     Q_EMIT m_viewManager->historyForwardEnabled(false);
594 
595     // renable back
596     if (currentLocation > 0) {
597         m_historyBack->setEnabled(true);
598         Q_EMIT m_viewManager->historyBackEnabled(true);
599     }
600 
601     // limit size to 50, remove first 10
602     int toErase = (int)m_locations.size() - 50;
603     if (toErase > 0) {
604         m_locations.erase(m_locations.begin(), m_locations.begin() + toErase);
605         currentLocation -= toErase;
606     }
607 }
608 
hiddenDocuments() const609 int KateViewSpace::hiddenDocuments() const
610 {
611     const auto hiddenDocs = KateApp::self()->documentManager()->documentList().size() - m_tabBar->count();
612     Q_ASSERT(hiddenDocs >= 0);
613     return hiddenDocs;
614 }
615 
showContextMenu(int idx,const QPoint & globalPos)616 void KateViewSpace::showContextMenu(int idx, const QPoint &globalPos)
617 {
618     // right now, show no context menu on empty tab bar space
619     if (idx < 0) {
620         return;
621     }
622 
623     auto *doc = m_tabBar->tabDocument(idx);
624     if (!doc) {
625         // This tab is holding some other widget
626         // Show only "close tab" for now
627         // maybe later allow adding context menu entries from the widgets
628         // if needed
629         QMenu menu(this);
630         auto aCloseTab = menu.addAction(QIcon::fromTheme(QStringLiteral("tab-close")), i18n("Close Tab"));
631         auto choice = menu.exec(globalPos);
632         if (choice == aCloseTab) {
633             closeTabRequest(idx);
634         }
635         return;
636     }
637 
638     auto addActionFromCollection = [this](QMenu *menu, const char *action_name) {
639         QAction *action = m_viewManager->mainWindow()->action(action_name);
640         return menu->addAction(action->icon(), action->text());
641     };
642 
643     QMenu menu(this);
644     QAction *aCloseTab = menu.addAction(QIcon::fromTheme(QStringLiteral("tab-close")), i18n("&Close Document"));
645     QAction *aCloseOthers = menu.addAction(QIcon::fromTheme(QStringLiteral("tab-close-other")), i18n("Close Other &Documents"));
646     menu.addSeparator();
647     QAction *aCopyPath = addActionFromCollection(&menu, "file_copy_filepath");
648     QAction *aOpenFolder = addActionFromCollection(&menu, "file_open_containing_folder");
649     QAction *aFileProperties = addActionFromCollection(&menu, "file_properties");
650     menu.addSeparator();
651     QAction *aRenameFile = addActionFromCollection(&menu, "file_rename");
652     QAction *aDeleteFile = addActionFromCollection(&menu, "file_delete");
653     menu.addSeparator();
654     QMenu *mCompareWithActive = new QMenu(i18n("Compare with active document"), &menu);
655     mCompareWithActive->setIcon(QIcon::fromTheme(QStringLiteral("kompare")));
656     menu.addMenu(mCompareWithActive);
657 
658     if (KateApp::self()->documentManager()->documentList().size() < 2) {
659         aCloseOthers->setEnabled(false);
660     }
661 
662     if (doc->url().isEmpty()) {
663         aCopyPath->setEnabled(false);
664         aOpenFolder->setEnabled(false);
665         aRenameFile->setEnabled(false);
666         aDeleteFile->setEnabled(false);
667         aFileProperties->setEnabled(false);
668         mCompareWithActive->setEnabled(false);
669     }
670 
671     auto activeDocument =
672         KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView()->document(); // used for mCompareWithActive which is used with another
673                                                                                                       // tab which is not active
674     // both documents must have urls and must not be the same to have the compare feature enabled
675     if (activeDocument->url().isEmpty() || activeDocument == doc) {
676         mCompareWithActive->setEnabled(false);
677     }
678 
679     if (mCompareWithActive->isEnabled()) {
680         for (auto &&diffTool : KateFileActions::supportedDiffTools()) {
681             QAction *compareAction = mCompareWithActive->addAction(diffTool.first);
682 
683             // we use the full path to safely execute the tool, disable action if no full path => tool not found
684             compareAction->setData(diffTool.second);
685             compareAction->setEnabled(!diffTool.second.isEmpty());
686         }
687     }
688 
689     QAction *choice = menu.exec(globalPos);
690 
691     if (!choice) {
692         return;
693     }
694 
695     if (choice == aCloseTab) {
696         closeTabRequest(idx);
697     } else if (choice == aCloseOthers) {
698         KateApp::self()->documentManager()->closeOtherDocuments(doc);
699     } else if (choice == aCopyPath) {
700         KateFileActions::copyFilePathToClipboard(doc);
701     } else if (choice == aOpenFolder) {
702         KateFileActions::openContainingFolder(doc);
703     } else if (choice == aFileProperties) {
704         KateFileActions::openFilePropertiesDialog(doc);
705     } else if (choice == aRenameFile) {
706         KateFileActions::renameDocumentFile(this, doc);
707     } else if (choice == aDeleteFile) {
708         KateFileActions::deleteDocumentFile(this, doc);
709     } else if (choice->parent() == mCompareWithActive) {
710         QString actionData = choice->data().toString(); // name of the executable of the diff program
711         if (!KateFileActions::compareWithExternalProgram(activeDocument, doc, actionData)) {
712             QMessageBox::information(this,
713                                      i18n("Could not start program"),
714                                      i18n("The selected program could not be started. Maybe it is not installed."),
715                                      QMessageBox::StandardButton::Ok);
716         }
717     }
718 }
719 
saveConfig(KConfigBase * config,int myIndex,const QString & viewConfGrp)720 void KateViewSpace::saveConfig(KConfigBase *config, int myIndex, const QString &viewConfGrp)
721 {
722     //   qCDebug(LOG_KATE)<<"KateViewSpace::saveConfig("<<myIndex<<", "<<viewConfGrp<<") - currentView: "<<currentView()<<")";
723     QString groupname = QString(viewConfGrp + QStringLiteral("-ViewSpace %1")).arg(myIndex);
724 
725     // aggregate all views in view space (LRU ordered)
726     std::vector<KTextEditor::View *> views;
727     QStringList lruList;
728     const auto docList = documentList();
729     for (KTextEditor::Document *doc : docList) {
730         lruList << doc->url().toString();
731         auto it = m_docToView.find(doc);
732         if (it != m_docToView.end()) {
733             views.push_back(it->second);
734         }
735     }
736 
737     KConfigGroup group(config, groupname);
738     group.writeEntry("Documents", lruList);
739     group.writeEntry("Count", static_cast<int>(views.size()));
740 
741     if (currentView()) {
742         group.writeEntry("Active View", currentView()->document()->url().toString());
743     }
744 
745     // Save file list, including cursor position in this instance.
746     int idx = 0;
747     for (auto view : views) {
748         const auto url = view->document()->url();
749         if (!url.isEmpty()) {
750             group.writeEntry(QStringLiteral("View %1").arg(idx), url.toString());
751 
752             // view config, group: "ViewSpace <n> url"
753             QString vgroup = QStringLiteral("%1 %2").arg(groupname, url.toString());
754             KConfigGroup viewGroup(config, vgroup);
755             view->writeSessionConfig(viewGroup);
756         }
757 
758         ++idx;
759     }
760 }
761 
restoreConfig(KateViewManager * viewMan,const KConfigBase * config,const QString & groupname)762 void KateViewSpace::restoreConfig(KateViewManager *viewMan, const KConfigBase *config, const QString &groupname)
763 {
764     KConfigGroup group(config, groupname);
765 
766     // workaround for the weird bug where the tabbar sometimes becomes invisible after opening a session via the session chooser dialog or the --start cmd
767     // option
768     // TODO: Debug the actual reason for the bug. See https://invent.kde.org/utilities/kate/-/merge_requests/189
769     m_tabBar->hide();
770     m_tabBar->show();
771 
772     // set back bar status to configured variant
773     tabBarToggled();
774 
775     // restore Document lru list so that all tabs from the last session reappear
776     const QStringList lruList = group.readEntry("Documents", QStringList());
777     for (int i = 0; i < lruList.size(); ++i) {
778         // ignore non-existing documents
779         if (auto doc = KateApp::self()->documentManager()->findDocument(QUrl(lruList[i]))) {
780             registerDocument(doc);
781         }
782     }
783 
784     // restore active view properties
785     const QString fn = group.readEntry("Active View");
786     if (!fn.isEmpty()) {
787         KTextEditor::Document *doc = KateApp::self()->documentManager()->findDocument(QUrl(fn));
788 
789         if (doc) {
790             // view config, group: "ViewSpace <n> url"
791             QString vgroup = QStringLiteral("%1 %2").arg(groupname, fn);
792             KConfigGroup configGroup(config, vgroup);
793 
794             auto view = viewMan->createView(doc, this);
795             if (view) {
796                 // When a session is opened with a remote file being active, we need to wait
797                 // with applying saved session settings until the remote's temp file is initialised.
798                 if (!view->document()->url().isLocalFile()) {
799                     QSharedPointer<QMetaObject::Connection> conn(new QMetaObject::Connection());
800                     auto handler = [conn, view, configGroup](KTextEditor::Document *) {
801                         disconnect(*conn);
802                         view->readSessionConfig(configGroup);
803                     };
804                     *conn = connect(doc, &KTextEditor::Document::textChanged, view, handler);
805                 } else {
806                     view->readSessionConfig(configGroup);
807                 }
808                 m_tabBar->setCurrentDocument(doc);
809             }
810         }
811     }
812 
813     // avoid empty view space
814     if (m_docToView.empty()) {
815         auto *doc = KateApp::self()->documentManager()->documentList().first();
816         if (!fn.isEmpty()) {
817             QUrl url(fn);
818             KateApp::self()->documentManager()->documentInfo(doc)->doPostLoadOperations =
819                 !url.isLocalFile() && (KateApp::self()->hasCursorInArgs() || url.hasQuery());
820         }
821         viewMan->createView(doc, this);
822     }
823 
824     m_group = groupname; // used for restroing view configs later
825 }
826 
goBack()827 void KateViewSpace::goBack()
828 {
829     if (m_locations.empty() || currentLocation <= 0) {
830         currentLocation = 0;
831         return;
832     }
833 
834     const auto &location = m_locations.at(currentLocation - 1);
835     currentLocation--;
836 
837     if (currentLocation <= 0) {
838         m_historyBack->setEnabled(false);
839         Q_EMIT m_viewManager->historyBackEnabled(false);
840     }
841 
842     if (auto v = m_viewManager->activeView()) {
843         if (v->document() && v->document()->url() == location.url) {
844             const QSignalBlocker blocker(v);
845             v->setCursorPosition(location.cursor);
846             // enable forward
847             m_historyForward->setEnabled(true);
848             Q_EMIT m_viewManager->historyForwardEnabled(true);
849             return;
850         }
851     }
852 
853     auto v = m_viewManager->openUrlWithView(location.url, QString());
854     const QSignalBlocker blocker(v);
855     v->setCursorPosition(location.cursor);
856     // enable forward in viewspace + mainwindow
857     m_historyForward->setEnabled(true);
858     Q_EMIT m_viewManager->historyForwardEnabled(true);
859 }
860 
isHistoryBackEnabled() const861 bool KateViewSpace::isHistoryBackEnabled() const
862 {
863     return m_historyBack->isEnabled();
864 }
865 
isHistoryForwardEnabled() const866 bool KateViewSpace::isHistoryForwardEnabled() const
867 {
868     return m_historyForward->isEnabled();
869 }
870 
goForward()871 void KateViewSpace::goForward()
872 {
873     if (m_locations.empty()) {
874         return;
875     }
876 
877     // We are already at the last position
878     if (currentLocation >= m_locations.size() - 1) {
879         return;
880     }
881 
882     const auto &location = m_locations.at(currentLocation + 1);
883     currentLocation++;
884 
885     if (currentLocation + 1 >= m_locations.size()) {
886         Q_EMIT m_viewManager->historyForwardEnabled(false);
887         m_historyForward->setEnabled(false);
888     }
889 
890     if (!location.url.isValid() || !location.cursor.isValid()) {
891         m_locations.erase(m_locations.begin() + currentLocation);
892         return;
893     }
894 
895     m_historyBack->setEnabled(true);
896     Q_EMIT m_viewManager->historyBackEnabled(true);
897 
898     if (auto v = m_viewManager->activeView()) {
899         if (v->document() && v->document()->url() == location.url) {
900             const QSignalBlocker blocker(v);
901             v->setCursorPosition(location.cursor);
902             return;
903         }
904     }
905 
906     auto v = m_viewManager->openUrlWithView(location.url, QString());
907     const QSignalBlocker blocker(v);
908     v->setCursorPosition(location.cursor);
909 }
910 
911 // END KateViewSpace
912