kateviewspace.cpp 28.2 KB
Newer Older
1
/* This file is part of the KDE project
2
3
4
   SPDX-FileCopyrightText: 2001 Christoph Cullmann <cullmann@kde.org>
   SPDX-FileCopyrightText: 2001 Joseph Wenninger <jowenn@kde.org>
   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
12
#include "katedebug.h"
#include "katedocmanager.h"
13
#include "katefileactions.h"
14
#include "katemainwindow.h"
15
#include "katesessionmanager.h"
Christoph Cullmann's avatar
Christoph Cullmann committed
16
#include "kateupdatedisabler.h"
17
#include "kateviewmanager.h"
Christoph Cullmann's avatar
Christoph Cullmann committed
18
#include <KActionCollection>
19

20
#include <KAcceleratorManager>
Dominik Haumann's avatar
Dominik Haumann committed
21
22
#include <KConfigGroup>
#include <KLocalizedString>
23

24
25
#include <QApplication>
#include <QClipboard>
Dominik Haumann's avatar
Dominik Haumann committed
26
#include <QHelpEvent>
27
#include <QMenu>
28
#include <QMessageBox>
Dominik Haumann's avatar
Dominik Haumann committed
29
30
#include <QStackedWidget>
#include <QToolButton>
Dominik Haumann's avatar
Dominik Haumann committed
31
#include <QToolTip>
32
#include <QWhatsThis>
33

34
35
#include <KTextEditor/Editor>

36
37
// BEGIN KateViewSpace
KateViewSpace::KateViewSpace(KateViewManager *viewManager, QWidget *parent, const char *name)
Dominik Haumann's avatar
Dominik Haumann committed
38
39
    : QWidget(parent)
    , m_viewManager(viewManager)
40
    , m_isActiveSpace(false)
41
{
42
43
44
    setObjectName(QString::fromLatin1(name));
    QVBoxLayout *layout = new QVBoxLayout(this);
    layout->setSpacing(0);
Laurent Montel's avatar
Laurent Montel committed
45
    layout->setContentsMargins(0, 0, 0, 0);
46

47
    // BEGIN tab bar
48
49
    QHBoxLayout *hLayout = new QHBoxLayout();
    hLayout->setSpacing(0);
Laurent Montel's avatar
Laurent Montel committed
50
    hLayout->setContentsMargins(0, 0, 0, 0);
51

52
    // add left <-> right history buttons
53
    m_historyBack = new QToolButton(this);
54
55
    m_historyBack->setToolTip(i18n("Go Back"));
    m_historyBack->setIcon(QIcon::fromTheme(QStringLiteral("arrow-left")));
56
57
58
59
    m_historyBack->setAutoRaise(true);
    KAcceleratorManager::setNoAccel(m_historyBack);
    m_historyBack->installEventFilter(this); // on click, active this view space
    hLayout->addWidget(m_historyBack);
60
61
62
63
    connect(m_historyBack, &QToolButton::clicked, this, [this] {
        goBack();
    });
    m_historyBack->setEnabled(false);
64
65

    m_historyForward = new QToolButton(this);
66
67
    m_historyForward->setIcon(QIcon::fromTheme(QStringLiteral("arrow-right")));
    m_historyForward->setToolTip(i18n("Go Forward"));
68
69
70
71
    m_historyForward->setAutoRaise(true);
    KAcceleratorManager::setNoAccel(m_historyForward);
    m_historyForward->installEventFilter(this); // on click, active this view space
    hLayout->addWidget(m_historyForward);
72
73
74
75
    connect(m_historyForward, &QToolButton::clicked, this, [this] {
        goForward();
    });
    m_historyForward->setEnabled(false);
76

77
78
79
    // add tab bar
    m_tabBar = new KateTabBar(this);
    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
85
    hLayout->addWidget(m_tabBar);

86
87
88
    // add quick open
    m_quickOpen = new QToolButton(this);
    m_quickOpen->setAutoRaise(true);
89
    KAcceleratorManager::setNoAccel(m_quickOpen);
90
91
92
    m_quickOpen->installEventFilter(this); // on click, active this view space
    hLayout->addWidget(m_quickOpen);

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
98
99
    Q_ASSERT(quickOpen);
    bridge->setToolTip(quickOpen->toolTip());
    bridge->setWhatsThis(i18n("Click here to switch to the Quick Open view."));
Laurent Montel's avatar
Laurent Montel committed
100
    connect(bridge, &QAction::triggered, quickOpen, &QAction::trigger);
101

102
    // add vertical split view space
Christoph Cullmann's avatar
Christoph Cullmann committed
103
104
105
106
107
108
109
110
111
112
113
114
    m_split = new QToolButton(this);
    m_split->setAutoRaise(true);
    m_split->setPopupMode(QToolButton::InstantPopup);
    m_split->setIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right")));
    m_split->addAction(m_viewManager->mainWindow()->actionCollection()->action(QStringLiteral("view_split_vert")));
    m_split->addAction(m_viewManager->mainWindow()->actionCollection()->action(QStringLiteral("view_split_horiz")));
    m_split->addAction(m_viewManager->mainWindow()->actionCollection()->action(QStringLiteral("view_close_current_space")));
    m_split->addAction(m_viewManager->mainWindow()->actionCollection()->action(QStringLiteral("view_close_others")));
    m_split->addAction(m_viewManager->mainWindow()->actionCollection()->action(QStringLiteral("view_hide_others")));
    m_split->setWhatsThis(i18n("Control view space splitting"));
    m_split->installEventFilter(this); // on click, active this view space
    hLayout->addWidget(m_split);
115
116

    layout->addLayout(hLayout);
117
    // END tab bar
118
119
120

    stack = new QStackedWidget(this);
    stack->setFocus();
Dominik Haumann's avatar
Dominik Haumann committed
121
    stack->setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding));
122
123
124
125
126
    layout->addWidget(stack);

    m_group.clear();

    // connect signal to hide/show statusbar
Laurent Montel's avatar
Laurent Montel committed
127
128
    connect(m_viewManager->mainWindow(), &KateMainWindow::statusBarToggled, this, &KateViewSpace::statusBarToggled);
    connect(m_viewManager->mainWindow(), &KateMainWindow::tabBarToggled, this, &KateViewSpace::tabBarToggled);
129

Christoph Cullmann's avatar
Christoph Cullmann committed
130
    // init the bars...
131
    statusBarToggled();
Christoph Cullmann's avatar
Christoph Cullmann committed
132
    tabBarToggled();
133
134
}

135
136
bool KateViewSpace::eventFilter(QObject *obj, QEvent *event)
{
137
138
    QToolButton *button = qobject_cast<QToolButton *>(obj);

139
140
    // quick open button: show tool tip with shortcut
    if (button == m_quickOpen && event->type() == QEvent::ToolTip) {
141
        QHelpEvent *e = static_cast<QHelpEvent *>(event);
142
143
        QAction *quickOpen = m_viewManager->mainWindow()->actionCollection()->action(QStringLiteral("view_quick_open"));
        Q_ASSERT(quickOpen);
144
        QToolTip::showText(e->globalPos(), button->toolTip() + QStringLiteral(" (%1)").arg(quickOpen->shortcut().toString()), button);
145
146
147
148
149
150
151
        return true;
    }

    // quick open button: What's This
    if (button == m_quickOpen && event->type() == QEvent::WhatsThis) {
        QHelpEvent *e = static_cast<QHelpEvent *>(event);
        const int hiddenDocs = hiddenDocuments();
Alexander Lohnau's avatar
Alexander Lohnau committed
152
153
154
155
156
        QString helpText = (hiddenDocs == 0)
            ? i18n("Click here to switch to the Quick Open view.")
            : i18np("Currently, there is one more document open. To see all open documents, switch to the Quick Open view by clicking here.",
                    "Currently, there are %1 more documents open. To see all open documents, switch to the Quick Open view by clicking here.",
                    hiddenDocs);
157
158
        QWhatsThis::showText(e->globalPos(), helpText, m_quickOpen);
        return true;
Dominik Haumann's avatar
Dominik Haumann committed
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
165
166
        if (currentView()) {
            m_viewManager->activateView(currentView()->document());
        }
167
168
    }
    return false;
169
170
}

171
void KateViewSpace::statusBarToggled()
172
{
173
    KateUpdateDisabler updatesDisabled(m_viewManager->mainWindow());
Waqar Ahmed's avatar
Waqar Ahmed committed
174
175
    for (const auto &[_, view] : m_docToView) {
        Q_UNUSED(_)
Christoph Cullmann's avatar
Christoph Cullmann committed
176
        view->setStatusBarEnabled(m_viewManager->mainWindow()->showStatusBar());
177
178
179
    }
}

Christoph Cullmann's avatar
Christoph Cullmann committed
180
181
void KateViewSpace::tabBarToggled()
{
182
    KateUpdateDisabler updatesDisabled(m_viewManager->mainWindow());
183
184
    m_historyBack->setVisible(m_viewManager->mainWindow()->showTabBar());
    m_historyForward->setVisible(m_viewManager->mainWindow()->showTabBar());
Christoph Cullmann's avatar
Christoph Cullmann committed
185
186
187
188
189
    m_tabBar->setVisible(m_viewManager->mainWindow()->showTabBar());
    m_split->setVisible(m_viewManager->mainWindow()->showTabBar());
    m_quickOpen->setVisible(m_viewManager->mainWindow()->showTabBar());
}

190
KTextEditor::View *KateViewSpace::createView(KTextEditor::Document *doc)
191
{
192
    // should only be called if a view does not yet exist
193
194
195
196
    {
        auto it = m_docToView.find(doc);
        Q_ASSERT(it == m_docToView.end());
    }
197

198
199
200
201
202
203
204
205
206
207
208
    /**
     * Create a fresh view
     */
    KTextEditor::View *v = doc->createView(stack, m_viewManager->mainWindow()->wrapper());

    // set status bar to right state
    v->setStatusBarEnabled(m_viewManager->mainWindow()->showStatusBar());

    // restore the config of this view if possible
    if (!m_group.isEmpty()) {
        QString fn = v->document()->url().toString();
209
        if (!fn.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
210
            QString vgroup = QStringLiteral("%1 %2").arg(m_group, fn);
211

Christoph Cullmann's avatar
Christoph Cullmann committed
212
            KateSession::Ptr as = KateApp::self()->sessionManager()->activeSession();
213
214
            if (as->config() && as->config()->hasGroup(vgroup)) {
                KConfigGroup cg(as->config(), vgroup);
215
                v->readSessionConfig(cg);
216
217
            }
        }
218
219
    }

220
221
    connect(v, &KTextEditor::View::cursorPositionChanged, this, [this](KTextEditor::View *view, const KTextEditor::Cursor &newPosition) {
        if (view && view->document())
222
            addPositionToHistory(view->document()->url(), newPosition);
223
224
    });

225
    // register document, it is shown below through showView() then
Christoph Cullmann's avatar
Christoph Cullmann committed
226
    registerDocument(doc);
227

228
    // view shall still be not registered
229
    Q_ASSERT(m_docToView.find(doc) == m_docToView.end());
230

231
232
    // insert View into stack
    stack->addWidget(v);
233
    m_docToView[doc] = v;
234
    showView(v);
235

236
    return v;
237
238
}

239
void KateViewSpace::removeView(KTextEditor::View *v)
240
{
241
    // remove view mappings
242
243
244
    auto it = m_docToView.find(v->document());
    Q_ASSERT(it != m_docToView.end());
    m_docToView.erase(it);
245

246
    // ...and now: remove from view space
247
    stack->removeWidget(v);
248
249
250

    // switch to most recently used rather than letting stack choose one
    // (last element could well be v->document() being removed here)
251
252
253
254
    for (auto rit = m_registeredDocuments.rbegin(); rit != m_registeredDocuments.rend(); ++rit) {
        auto it = m_docToView.find(*rit);
        if (it != m_docToView.end()) {
            showView(*rit);
255
256
257
            break;
        }
    }
258
259
}

260
261
bool KateViewSpace::showView(KTextEditor::Document *document)
{
262
263
264
    /**
     * nothing can be done if we have now view ready here
     */
265
266
    auto it = m_docToView.find(document);
    if (it == m_docToView.end()) {
267
268
269
        return false;
    }

270
271
272
273
274
275
276
277
278
279
280
281
    /**
     * update mru list order
     */
    const int index = m_registeredDocuments.lastIndexOf(document);
    // move view to end of list
    if (index < 0) {
        return false;
    }
    // move view to end of list
    m_registeredDocuments.removeAt(index);
    m_registeredDocuments.append(document);

282
283
284
    /**
     * show the wanted view
     */
285
    KTextEditor::View *kv = it->second;
286
287
288
    stack->setCurrentWidget(kv);
    kv->show();

289
290
291
292
293
    /**
     * we need to avoid that below's index changes will mess with current view
     */
    disconnect(m_tabBar, &KateTabBar::currentChanged, this, &KateViewSpace::changeView);

294
295
296
    /**
     * follow current view
     */
297
    m_tabBar->setCurrentDocument(document);
298

299
300
    // track tab changes again
    connect(m_tabBar, &KateTabBar::currentChanged, this, &KateViewSpace::changeView);
301
    return true;
302
303
}

304
void KateViewSpace::changeView(int idx)
305
{
306
307
308
309
    if (idx == -1) {
        return;
    }

310
    KTextEditor::Document *doc = m_tabBar->tabDocument(idx);
311
    Q_ASSERT(doc);
312

313
    // make sure we open the view in this view space
314
    if (!isActiveSpace()) {
315
316
        m_viewManager->setActiveSpace(this);
    }
Dominik Haumann's avatar
Dominik Haumann committed
317

318
319
    // tell the view manager to show the view
    m_viewManager->activateView(doc);
320
}
321

322
KTextEditor::View *KateViewSpace::currentView()
323
{
324
    // might be 0 if the stack contains no view
325
    return static_cast<KTextEditor::View *>(stack->currentWidget());
326
327
328
329
}

bool KateViewSpace::isActiveSpace()
{
330
    return m_isActiveSpace;
331
332
}

333
void KateViewSpace::setActive(bool active)
334
{
335
    m_isActiveSpace = active;
336
    m_tabBar->setActive(active);
337
338
}

339
340
void KateViewSpace::makeActive(bool focusCurrentView)
{
341
    if (!isActiveSpace()) {
342
343
344
345
346
347
348
349
        m_viewManager->setActiveSpace(this);
        if (focusCurrentView && currentView()) {
            m_viewManager->activateView(currentView()->document());
        }
    }
    Q_ASSERT(isActiveSpace());
}

350
void KateViewSpace::registerDocument(KTextEditor::Document *doc)
351
{
352
353
354
355
    /**
     * ignore request to register something that is already known
     */
    if (m_registeredDocuments.contains(doc)) {
356
357
358
        return;
    }

359
360
361
    /**
     * remember our new document
     */
362
    m_registeredDocuments.insert(0, doc);
363
364
365
366

    /**
     * ensure we cleanup after document is deleted, e.g. we remove the tab bar button
     */
Laurent Montel's avatar
Laurent Montel committed
367
    connect(doc, &QObject::destroyed, this, &KateViewSpace::documentDestroyed);
368

369
370
371
372
373
    /**
     * register document is used in places that don't like view creation
     * therefore we must ensure the currentChanged doesn't trigger that
     */
    disconnect(m_tabBar, &KateTabBar::currentChanged, this, &KateViewSpace::changeView);
374
375

    /**
376
     * create the tab for this document, if necessary
377
     */
378
    m_tabBar->setCurrentDocument(doc);
379

Christoph Cullmann's avatar
Christoph Cullmann committed
380
381
382
    /**
     * handle later document state changes
     */
383
384
385
386
387
388
389
    connect(doc, &KTextEditor::Document::documentNameChanged, this, &KateViewSpace::updateDocumentName);
    connect(doc, &KTextEditor::Document::documentUrlChanged, this, &KateViewSpace::updateDocumentUrl);
    connect(doc, &KTextEditor::Document::modifiedChanged, this, &KateViewSpace::updateDocumentState);

    /**
     * allow signals again, now that the tab is there
     */
390
    connect(m_tabBar, &KateTabBar::currentChanged, this, &KateViewSpace::changeView);
391
392
}

393
void KateViewSpace::documentDestroyed(QObject *doc)
394
{
395
396
397
398
    /**
     * WARNING: this pointer is half destroyed
     * only good enough to check pointer value e.g. for hashs
     */
399
    KTextEditor::Document *invalidDoc = static_cast<KTextEditor::Document *>(doc);
400
    Q_ASSERT(m_registeredDocuments.contains(invalidDoc));
401
    m_registeredDocuments.removeAll(invalidDoc);
402

403
404
405
    /**
     * we shall have no views for this document at this point in time!
     */
406
    Q_ASSERT(m_docToView.find(invalidDoc) == m_docToView.end());
407

408
    // disconnect entirely
409
    disconnect(doc, nullptr, this, nullptr);
410

411
    /**
412
     * remove the tab for this document, if existing
413
     */
414
    m_tabBar->removeDocument(invalidDoc);
415
416
}

417
void KateViewSpace::updateDocumentName(KTextEditor::Document *doc)
418
{
419
    // update tab button if available, might not be the case for tab limit set!
420
    const int buttonId = m_tabBar->documentIdx(doc);
421
422
423
    if (buttonId >= 0) {
        m_tabBar->setTabText(buttonId, doc->documentName());
    }
424
425
}

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
426
427
void KateViewSpace::updateDocumentUrl(KTextEditor::Document *doc)
{
428
    // update tab button if available, might not be the case for tab limit set!
429
    const int buttonId = m_tabBar->documentIdx(doc);
430
    if (buttonId >= 0) {
Christoph Cullmann's avatar
Christoph Cullmann committed
431
        m_tabBar->setTabToolTip(buttonId, doc->url().toDisplayString());
432
    }
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
433
434
}

435
436
437
438
void KateViewSpace::updateDocumentState(KTextEditor::Document *doc)
{
    QIcon icon;
    if (doc->isModified()) {
Laurent Montel's avatar
Laurent Montel committed
439
        icon = QIcon::fromTheme(QStringLiteral("document-save"));
440
441
    }

442
    // update tab button if available, might not be the case for tab limit set!
443
    const int buttonId = m_tabBar->documentIdx(doc);
444
445
446
    if (buttonId >= 0) {
        m_tabBar->setTabIcon(buttonId, icon);
    }
447
448
}

449
void KateViewSpace::closeTabRequest(int idx)
450
{
451
    auto *doc = m_tabBar->tabDocument(idx);
452
    Q_ASSERT(doc);
453
    m_viewManager->slotDocumentClose(doc);
454
455
}

456
457
458
void KateViewSpace::createNewDocument()
{
    // make sure we open the view in this view space
459
    if (!isActiveSpace()) {
460
461
462
463
464
465
466
467
468
469
        m_viewManager->setActiveSpace(this);
    }

    // create document
    KTextEditor::Document *doc = KateApp::self()->documentManager()->createDoc();

    // tell the view manager to show the document
    m_viewManager->activateView(doc);
}

470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
void KateViewSpace::focusPrevTab()
{
    const int id = m_tabBar->prevTab();
    if (id >= 0) {
        changeView(id);
    }
}

void KateViewSpace::focusNextTab()
{
    const int id = m_tabBar->nextTab();
    if (id >= 0) {
        changeView(id);
    }
}

486
void KateViewSpace::addPositionToHistory(const QUrl &url, KTextEditor::Cursor c, bool calledExternally)
487
{
488
489
490
491
492
    // We don't care about invalid urls (Fixed Diff View / Untitled docs)
    if (!url.isValid()) {
        return;
    }

493
    // we are in the middle of jumps somewhere?
494
    if (!m_locations.empty() && currentLocation + 1 < m_locations.size()) {
495
496
497
498
499
        // erase all forward history
        m_locations.erase(m_locations.begin() + currentLocation + 1, m_locations.end());
    }

    // if same line, remove last entry
500
    if (!m_locations.empty() && m_locations.back().url == url && m_locations.back().cursor.line() == c.line()) {
501
502
503
        m_locations.pop_back();
    }

504
    // Check if the location is at least "viewLineCount" away
505
    if (!calledExternally && !m_locations.empty() && m_locations.back().url == url) {
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
        int line = c.line();
        int lastLocLine = m_locations.back().cursor.line();

        auto view = m_viewManager->activeView();

        int viewLineCount = view->lastDisplayedLine() - view->firstDisplayedLine();
        int lowerBound = lastLocLine - viewLineCount;
        int upperBound = lastLocLine + viewLineCount;
        if (lowerBound <= line && line <= upperBound) {
            return;
        }
    }

    // limit size to 50, remove first 10
    if (m_locations.size() >= 50) {
        m_locations.erase(m_locations.begin(), m_locations.begin() + 10);
522
523
    }

524
    /** this is our new forward **/
525
526

    m_locations.push_back({url, c});
527
    // set currentLocation as last
528
529
530
    currentLocation = m_locations.size() - 1;
    // disable forward button as we are at the end now
    m_historyForward->setEnabled(false);
531
    Q_EMIT m_viewManager->historyForwardEnabled(false);
532
533
534
535

    // renable back
    if (currentLocation > 0) {
        m_historyBack->setEnabled(true);
536
        Q_EMIT m_viewManager->historyBackEnabled(true);
537
538
    }
}
539
540
int KateViewSpace::hiddenDocuments() const
{
541
    const auto hiddenDocs = KateApp::self()->documentManager()->documentList().size() - m_tabBar->count();
542
543
544
545
    Q_ASSERT(hiddenDocs >= 0);
    return hiddenDocs;
}

546
void KateViewSpace::showContextMenu(int idx, const QPoint &globalPos)
547
548
{
    // right now, show no context menu on empty tab bar space
549
    if (idx < 0) {
550
551
552
        return;
    }

553
    auto *doc = m_tabBar->tabDocument(idx);
554
555
    Q_ASSERT(doc);

556
557
    auto addActionFromCollection = [this](QMenu *menu, const char *action_name) {
        QAction *action = m_viewManager->mainWindow()->action(action_name);
558
559
560
        return menu->addAction(action->icon(), action->text());
    };

561
    QMenu menu(this);
562
563
    QAction *aCloseTab = menu.addAction(QIcon::fromTheme(QStringLiteral("tab-close")), i18n("&Close Document"));
    QAction *aCloseOthers = menu.addAction(QIcon::fromTheme(QStringLiteral("tab-close-other")), i18n("Close Other &Documents"));
564
    menu.addSeparator();
565
566
567
568
569
570
571
572
573
574
    QAction *aCopyPath = addActionFromCollection(&menu, "file_copy_filepath");
    QAction *aOpenFolder = addActionFromCollection(&menu, "file_open_containing_folder");
    QAction *aFileProperties = addActionFromCollection(&menu, "file_properties");
    menu.addSeparator();
    QAction *aRenameFile = addActionFromCollection(&menu, "file_rename");
    QAction *aDeleteFile = addActionFromCollection(&menu, "file_delete");
    menu.addSeparator();
    QMenu *mCompareWithActive = new QMenu(i18n("Compare with active document"), &menu);
    mCompareWithActive->setIcon(QIcon::fromTheme(QStringLiteral("kompare")));
    menu.addMenu(mCompareWithActive);
575

576
    if (KateApp::self()->documentManager()->documentList().size() < 2) {
577
578
        aCloseOthers->setEnabled(false);
    }
579

580
581
582
    if (doc->url().isEmpty()) {
        aCopyPath->setEnabled(false);
        aOpenFolder->setEnabled(false);
583
584
585
586
587
588
        aRenameFile->setEnabled(false);
        aDeleteFile->setEnabled(false);
        aFileProperties->setEnabled(false);
        mCompareWithActive->setEnabled(false);
    }

Alexander Lohnau's avatar
Alexander Lohnau committed
589
590
591
    auto activeDocument =
        KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView()->document(); // used for mCompareWithActive which is used with another
                                                                                                      // tab which is not active
592
593
594
595
596
597
    // both documents must have urls and must not be the same to have the compare feature enabled
    if (activeDocument->url().isEmpty() || activeDocument == doc) {
        mCompareWithActive->setEnabled(false);
    }

    if (mCompareWithActive->isEnabled()) {
598
        for (auto &&diffTool : KateFileActions::supportedDiffTools()) {
599
600
601
            QAction *compareAction = mCompareWithActive->addAction(diffTool);
            compareAction->setData(diffTool);
        }
602
    }
603
604
605

    QAction *choice = menu.exec(globalPos);

606
607
608
609
    if (!choice) {
        return;
    }

610
    if (choice == aCloseTab) {
611
        closeTabRequest(idx);
612
613
    } else if (choice == aCloseOthers) {
        KateApp::self()->documentManager()->closeOtherDocuments(doc);
614
    } else if (choice == aCopyPath) {
615
        KateFileActions::copyFilePathToClipboard(doc);
616
    } else if (choice == aOpenFolder) {
617
618
619
620
621
622
623
624
625
626
        KateFileActions::openContainingFolder(doc);
    } else if (choice == aFileProperties) {
        KateFileActions::openFilePropertiesDialog(doc);
    } else if (choice == aRenameFile) {
        KateFileActions::renameDocumentFile(this, doc);
    } else if (choice == aDeleteFile) {
        KateFileActions::deleteDocumentFile(this, doc);
    } else if (choice->parent() == mCompareWithActive) {
        QString actionData = choice->data().toString(); // name of the executable of the diff program
        if (!KateFileActions::compareWithExternalProgram(activeDocument, doc, actionData)) {
Alexander Lohnau's avatar
Alexander Lohnau committed
627
628
629
630
            QMessageBox::information(this,
                                     i18n("Could not start program"),
                                     i18n("The selected program could not be started. Maybe it is not installed."),
                                     QMessageBox::StandardButton::Ok);
631
        }
632
633
634
    }
}

635
void KateViewSpace::saveConfig(KConfigBase *config, int myIndex, const QString &viewConfGrp)
636
{
637
    //   qCDebug(LOG_KATE)<<"KateViewSpace::saveConfig("<<myIndex<<", "<<viewConfGrp<<") - currentView: "<<currentView()<<")";
638
    QString groupname = QString(viewConfGrp + QStringLiteral("-ViewSpace %1")).arg(myIndex);
639

640
    // aggregate all views in view space (LRU ordered)
641
    std::vector<KTextEditor::View *> views;
642
    QStringList lruList;
643
644
    const auto docList = documentList();
    for (KTextEditor::Document *doc : docList) {
645
        lruList << doc->url().toString();
646
647
648
        auto it = m_docToView.find(doc);
        if (it != m_docToView.end()) {
            views.push_back(it->second);
649
650
651
        }
    }

652
    KConfigGroup group(config, groupname);
653
    group.writeEntry("Documents", lruList);
654
    group.writeEntry("Count", static_cast<int>(views.size()));
655

656
657
658
    if (currentView()) {
        group.writeEntry("Active View", currentView()->document()->url().toString());
    }
659

660
661
    // Save file list, including cursor position in this instance.
    int idx = 0;
662
663
664
665
    for (auto view : views) {
        const auto url = view->document()->url();
        if (!url.isEmpty()) {
            group.writeEntry(QStringLiteral("View %1").arg(idx), url.toString());
666

667
            // view config, group: "ViewSpace <n> url"
668
            QString vgroup = QStringLiteral("%1 %2").arg(groupname, url.toString());
669
            KConfigGroup viewGroup(config, vgroup);
670
            view->writeSessionConfig(viewGroup);
671
        }
672

673
674
        ++idx;
    }
675
676
}

677
void KateViewSpace::restoreConfig(KateViewManager *viewMan, const KConfigBase *config, const QString &groupname)
678
{
679
    KConfigGroup group(config, groupname);
680

Alexander Lohnau's avatar
Alexander Lohnau committed
681
682
    // workaround for the weird bug where the tabbar sometimes becomes invisible after opening a session via the session chooser dialog or the --start cmd
    // option
683
684
685
686
    // TODO: Debug the actual reason for the bug. See https://invent.kde.org/utilities/kate/-/merge_requests/189
    m_tabBar->hide();
    m_tabBar->show();

687
688
689
    // set back bar status to configured variant
    tabBarToggled();

690
691
692
    // restore Document lru list so that all tabs from the last session reappear
    const QStringList lruList = group.readEntry("Documents", QStringList());
    for (int i = 0; i < lruList.size(); ++i) {
693
694
        // ignore non-existing documents
        if (auto doc = KateApp::self()->documentManager()->findDocument(QUrl(lruList[i]))) {
695
            registerDocument(doc);
696
697
698
699
700
        }
    }

    // restore active view properties
    const QString fn = group.readEntry("Active View");
701
    if (!fn.isEmpty()) {
Christoph Cullmann's avatar
Christoph Cullmann committed
702
        KTextEditor::Document *doc = KateApp::self()->documentManager()->findDocument(QUrl(fn));
703

704
705
        if (doc) {
            // view config, group: "ViewSpace <n> url"
Laurent Montel's avatar
Laurent Montel committed
706
            QString vgroup = QStringLiteral("%1 %2").arg(groupname, fn);
707
            KConfigGroup configGroup(config, vgroup);
708

709
710
            auto view = viewMan->createView(doc, this);
            if (view) {
711
712
713
                // When a session is opened with a remote file being active, we need to wait
                // with applying saved session settings until the remote's temp file is initialised.
                if (!view->document()->url().isLocalFile()) {
714
                    QSharedPointer<QMetaObject::Connection> conn(new QMetaObject::Connection());
Christoph Cullmann's avatar
Christoph Cullmann committed
715
                    auto handler = [conn, view, configGroup](KTextEditor::Document *) {
716
                        disconnect(*conn);
717
                        view->readSessionConfig(configGroup);
718
719
                    };
                    *conn = connect(doc, &KTextEditor::Document::textChanged, view, handler);
720
721
722
                } else {
                    view->readSessionConfig(configGroup);
                }
723
                m_tabBar->setCurrentDocument(doc);
724
            }
725
        }
726
727
    }

728
    // avoid empty view space
729
730
    if (m_docToView.empty()) {
        auto *doc = KateApp::self()->documentManager()->documentList().first();
731
732
        if (!fn.isEmpty()) {
            QUrl url(fn);
Christoph Cullmann's avatar
Christoph Cullmann committed
733
734
            KateApp::self()->documentManager()->documentInfo(doc)->doPostLoadOperations =
                !url.isLocalFile() && (KateApp::self()->hasCursorInArgs() || url.hasQuery());
735
        }
736
        viewMan->createView(doc, this);
737
    }
738

739
    m_group = groupname; // used for restroing view configs later
740
}
741
742
743

void KateViewSpace::goBack()
{
744
    if (m_locations.empty() || currentLocation == 0) {
745
746
747
748
749
750
751
752
        return;
    }

    const auto &location = m_locations.at(currentLocation - 1);
    currentLocation--;

    if (currentLocation <= 0) {
        m_historyBack->setEnabled(false);
753
        Q_EMIT m_viewManager->historyBackEnabled(false);
754
755
756
757
    }

    if (auto v = m_viewManager->activeView()) {
        if (v->document() && v->document()->url() == location.url) {
758
759
            const QSignalBlocker blocker(v);
            v->setCursorPosition(location.cursor);
760
761
            // enable forward
            m_historyForward->setEnabled(true);
762
            Q_EMIT m_viewManager->historyForwardEnabled(true);
763
764
765
766
767
768
769
            return;
        }
    }

    auto v = m_viewManager->openUrlWithView(location.url, QString());
    const QSignalBlocker blocker(v);
    v->setCursorPosition(location.cursor);
770
    // enable forward in viewspace + mainwindow
771
    m_historyForward->setEnabled(true);
772
    Q_EMIT m_viewManager->historyForwardEnabled(true);
773
774
775
776
777
778
779
780
781
782
}

bool KateViewSpace::isHistoryBackEnabled() const
{
    return m_historyBack->isEnabled();
}

bool KateViewSpace::isHistoryForwardEnabled() const
{
    return m_historyForward->isEnabled();
783
784
785
786
}

void KateViewSpace::goForward()
{
787
    if (m_locations.empty()) {
788
789
790
791
792
793
794
795
796
797
        return;
    }
    if (currentLocation == m_locations.size() - 1) {
        return;
    }

    const auto &location = m_locations.at(currentLocation + 1);
    currentLocation++;

    if (currentLocation + 1 >= m_locations.size()) {
798
        Q_EMIT m_viewManager->historyForwardEnabled(false);
799
800
801
802
        m_historyForward->setEnabled(false);
    }

    if (!location.url.isValid() || !location.cursor.isValid()) {
803
        m_locations.erase(m_locations.begin() + currentLocation);
804
805
806
807
        return;
    }

    m_historyBack->setEnabled(true);
808
    Q_EMIT m_viewManager->historyBackEnabled(true);
809
810
811

    if (auto v = m_viewManager->activeView()) {
        if (v->document() && v->document()->url() == location.url) {
812
813
            const QSignalBlocker blocker(v);
            v->setCursorPosition(location.cursor);
814
815
816
817
818
819
820
821
822
            return;
        }
    }

    auto v = m_viewManager->openUrlWithView(location.url, QString());
    const QSignalBlocker blocker(v);
    v->setCursorPosition(location.cursor);
}

823
// END KateViewSpace