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 ¤tLoc = 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