katetabbar.cpp 9.47 KB
Newer Older
1
2
/*  SPDX-License-Identifier: LGPL-2.0-or-later

3
4
5
6
    SPDX-FileCopyrightText: 2014 Dominik Haumann <dhaumann@kde.org>
    SPDX-FileCopyrightText: 2020 Christoph Cullmann <cullmann@kde.org>

    SPDX-License-Identifier: LGPL-2.0-or-later
7
*/
8
9

#include "katetabbar.h"
Christoph Cullmann's avatar
Christoph Cullmann committed
10
#include "kateapp.h"
11

Christoph Cullmann's avatar
Christoph Cullmann committed
12
#include <QIcon>
13
#include <QMimeData>
14
#include <QPainter>
Dominik Haumann's avatar
Dominik Haumann committed
15
#include <QResizeEvent>
16
#include <QStyleOptionTab>
17
#include <QWheelEvent>
Dominik Haumann's avatar
Dominik Haumann committed
18

19
#include <KConfigGroup>
Christoph Cullmann's avatar
Christoph Cullmann committed
20
#include <KSharedConfig>
21

22
23
24
25
26
27
#include <KTextEditor/Document>

struct KateTabButtonData {
    KTextEditor::Document *doc = nullptr;
};

28
Q_DECLARE_METATYPE(KateTabButtonData)
29

30
/**
31
 * Creates a new tab bar with the given \a parent.
32
33
 */
KateTabBar::KateTabBar(QWidget *parent)
34
    : QTabBar(parent)
35
{
36
37
38
    KSharedConfig::Ptr config = KSharedConfig::openConfig();
    KConfigGroup cgGeneral = KConfigGroup(config, "General");

39
40
41
42
43
44
    // enable document mode, docs tell this will trigger:
    // On macOS this will look similar to the tabs in Safari or Sierra's Terminal.app.
    // this seems reasonable for our document tabs
    setDocumentMode(true);

    // we want drag and drop
45
    setAcceptDrops(true);
46
47

    // use as much size as possible for each tab
48
    setExpanding(cgGeneral.readEntry("Expand Tabs", true));
49
50

    // document close function should be there
51
    setTabsClosable(cgGeneral.readEntry("Show Tabs Close Button", true));
52
53

    // allow users to re-arrange the tabs
54
    setMovable(true);
55
56
57
58
59
60

    // enforce configured limit
    readTabCountLimitConfig();

    // handle config changes
    connect(KateApp::self(), &KateApp::configurationChanged, this, &KateTabBar::readTabCountLimitConfig);
61
62
63
64
    connect(KateApp::self(), &KateApp::configurationChanged, this, [=]() {
        setExpanding(cgGeneral.readEntry("Expand Tabs", true));
        setTabsClosable(cgGeneral.readEntry("Show Tabs Close Button", true));
    });
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
}

void KateTabBar::readTabCountLimitConfig()
{
    KSharedConfig::Ptr config = KSharedConfig::openConfig();
    KConfigGroup cgGeneral = KConfigGroup(config, "General");

    // 0 == unlimited, normalized other inputs
    const int tabCountLimit = cgGeneral.readEntry("Tabbar Tab Limit", 0);
    m_tabCountLimit = (tabCountLimit <= 0) ? 0 : tabCountLimit;

    // use scroll buttons if we have no limit
    setUsesScrollButtons(m_tabCountLimit == 0);

    // elide if we have some limit
    setElideMode((m_tabCountLimit == 0) ? Qt::ElideNone : Qt::ElideMiddle);

    // if we enforce a limit: purge tabs that violate it
    if (m_tabCountLimit > 0 && (count() > m_tabCountLimit)) {
        // just purge last X tabs, this isn't that clever but happens only on config changes!
        while (count() > m_tabCountLimit) {
            removeTab(count() - 1);
        }
        setCurrentIndex(0);
    }
90
91
}

92
void KateTabBar::setActive(bool active)
93
{
94
    if (active == m_isActive) {
95
        return;
96
    }
97
    m_isActive = active;
98
    update();
99
100
}

101
bool KateTabBar::isActive() const
102
{
103
    return m_isActive;
104
105
}

106
107
int KateTabBar::prevTab() const
{
108
    return currentIndex() == 0 ? 0 // first index, keep it here.
Christoph Cullmann's avatar
Christoph Cullmann committed
109
                               : currentIndex() - 1;
110
111
112
113
}

int KateTabBar::nextTab() const
{
114
    return currentIndex() == count() - 1 ? count() - 1 // last index, keep it here.
Christoph Cullmann's avatar
Christoph Cullmann committed
115
                                         : currentIndex() + 1;
116
117
}

118
bool KateTabBar::containsTab(int index) const
119
{
120
    return index >= 0 && index < count();
121
122
}

123
QVariant KateTabBar::ensureValidTabData(int idx)
124
{
125
    if (!tabData(idx).isValid()) {
Christoph Cullmann's avatar
Christoph Cullmann committed
126
        setTabData(idx, QVariant::fromValue(KateTabButtonData {}));
127
    }
128
    return tabData(idx);
129
130
}

131
132
133
134
135
void KateTabBar::mouseDoubleClickEvent(QMouseEvent *event)
{
    event->accept();
    emit newTabRequested();
}
136
137
138

void KateTabBar::mousePressEvent(QMouseEvent *event)
{
139
    if (!isActive()) {
140
141
        emit activateViewSpaceRequested();
    }
142
    QTabBar::mousePressEvent(event);
143
144
145
146
147
148
149
150

    // handle close for middle mouse button
    if (event->button() == Qt::MidButton) {
        int id = tabAt(event->pos());
        if (id >= 0) {
            emit tabCloseRequested(id);
        }
    }
151
}
Dominik Haumann's avatar
Dominik Haumann committed
152

153
154
void KateTabBar::contextMenuEvent(QContextMenuEvent *ev)
{
155
    int id = tabAt(ev->pos());
156
157
158
    if (id >= 0) {
        emit contextMenuRequest(id, ev->globalPos());
    }
159
}
160

161
void KateTabBar::wheelEvent(QWheelEvent *event)
162
163
164
165
166
{
    event->accept();

    // cycle through the tabs
    const int delta = event->angleDelta().x() + event->angleDelta().y();
167
168
    const int idx = (delta > 0) ? prevTab() : nextTab();
    setCurrentIndex(idx);
169
}
170

Christoph Cullmann's avatar
Christoph Cullmann committed
171
void KateTabBar::setTabDocument(int idx, KTextEditor::Document *doc)
172
{
173
174
175
176
177
178
    // get right icon to use
    QIcon icon;
    if (doc->isModified()) {
        icon = QIcon::fromTheme(QStringLiteral("document-save"));
    }

179
180
181
182
    QVariant data = ensureValidTabData(idx);
    KateTabButtonData buttonData = data.value<KateTabButtonData>();
    buttonData.doc = doc;
    setTabData(idx, QVariant::fromValue(buttonData));
183
184
185
    setTabText(idx, doc->documentName());
    setTabToolTip(idx, doc->url().toDisplayString());
    setTabIcon(idx, icon);
186
}
187

188
189
void KateTabBar::setCurrentDocument(KTextEditor::Document *doc)
{
190
    // in any case: update lru counter for this document, might add new element to hash
191
192
    // we have a tab after this call, too!
    m_docToLruCounterAndHasTab[doc] = std::make_pair(++m_lruCounter, true);
193
194
195
196
197
198
199
200
201
202
203
204
205

    // do we have a tab for this document?
    // if yes => just set as current one
    const int existingIndex = documentIdx(doc);
    if (existingIndex != -1) {
        setCurrentIndex(existingIndex);
        return;
    }

    // else: if we are still inside the allowed number of tabs or have no limit
    // => create new tab and be done
    if ((m_tabCountLimit == 0) || count() < m_tabCountLimit) {
        m_beingAdded = doc;
206
        insertTab(-1, doc->documentName());
207
208
209
210
211
212
213
214
215
        return;
    }

    // ok, we have already the limit of tabs reached:
    // replace the tab with the lowest lru counter => the least recently used

    // search for the right tab
    quint64 minCounter = static_cast<quint64>(-1);
    int indexToReplace = 0;
216
    KTextEditor::Document *docToReplace = nullptr;
217
218
219
220
221
    for (int idx = 0; idx < count(); idx++) {
        QVariant data = tabData(idx);
        if (!data.isValid()) {
            continue;
        }
222
        const quint64 currentCounter = m_docToLruCounterAndHasTab[data.value<KateTabButtonData>().doc].first;
223
224
225
        if (currentCounter <= minCounter) {
            minCounter = currentCounter;
            indexToReplace = idx;
226
            docToReplace = data.value<KateTabButtonData>().doc;
227
228
229
        }
    }

230
231
232
    // mark the replace doc as "has no tab"
    m_docToLruCounterAndHasTab[docToReplace].second = false;

233
234
235
236
237
238
239
    // replace it's data + set it as active
    setTabDocument(indexToReplace, doc);
    setCurrentIndex(indexToReplace);
}

void KateTabBar::removeDocument(KTextEditor::Document *doc)
{
240
241
242
    // purge LRU storage, must work
    Q_ASSERT(m_docToLruCounterAndHasTab.erase(doc) == 1);

243
244
    // remove document if needed, we might have no tab for it, if tab count is limited!
    const int idx = documentIdx(doc);
Christoph Cullmann's avatar
Christoph Cullmann committed
245
246
247
    if (idx == -1) {
        return;
    }
248

Christoph Cullmann's avatar
Christoph Cullmann committed
249
250
251
252
253
254
255
256
257
    // if we have some tab limit, replace the removed tab with the next best document that has none!
    if (m_tabCountLimit > 0) {
        quint64 maxCounter = 0;
        KTextEditor::Document *docToReplace = nullptr;
        for (const auto &lru : m_docToLruCounterAndHasTab) {
            // ignore stuff with tabs
            if (lru.second.second) {
                continue;
            }
258

Christoph Cullmann's avatar
Christoph Cullmann committed
259
260
261
262
            // search most recently used one
            if (lru.second.first >= maxCounter) {
                maxCounter = lru.second.first;
                docToReplace = lru.first;
263
264
            }
        }
265

Christoph Cullmann's avatar
Christoph Cullmann committed
266
267
268
269
270
271
272
273
274
275
276
        // any document found? replace the tab we want to close and be done
        if (docToReplace) {
            // mark the replace doc as "has a tab"
            m_docToLruCounterAndHasTab[docToReplace].second = true;

            // replace info for the tab
            setTabDocument(idx, docToReplace);
            setCurrentIndex(idx);
            emit currentChanged(idx);
            return;
        }
277
    }
Christoph Cullmann's avatar
Christoph Cullmann committed
278
279
280
281

    // if we arrive here, we just need to purge the tab
    // this happens if we have no limit or no document to replace the current one
    removeTab(idx);
282
}
283

284
285
286
287
288
289
290
291
292
293
294
295
296
int KateTabBar::documentIdx(KTextEditor::Document *doc)
{
    for (int idx = 0; idx < count(); idx++) {
        QVariant data = tabData(idx);
        if (!data.isValid()) {
            continue;
        }
        if (data.value<KateTabButtonData>().doc != doc) {
            continue;
        }
        return idx;
    }
    return -1;
297
298
}

299
KTextEditor::Document *KateTabBar::tabDocument(int idx)
300
{
301
302
    QVariant data = ensureValidTabData(idx);
    KateTabButtonData buttonData = data.value<KateTabButtonData>();
303
304
305
306

    KTextEditor::Document *doc = nullptr;
    // The tab got activated before the correct finalixation,
    // we need to plug the document before returning.
307
308
309
310
    if (buttonData.doc == nullptr && m_beingAdded) {
        setTabDocument(idx, m_beingAdded);
        doc = m_beingAdded;
        m_beingAdded = nullptr;
311
312
313
314
315
316
317
318
319
    } else {
        doc = buttonData.doc;
    }

    return doc;
}

void KateTabBar::tabInserted(int idx)
{
320
321
    if (m_beingAdded) {
        setTabDocument(idx, m_beingAdded);
322
    }
323
    m_beingAdded = nullptr;
324
}
325
326
327
328
329
330
331
332
333
334
335
336
337

QVector<KTextEditor::Document *> KateTabBar::documentList() const
{
    QVector<KTextEditor::Document *> result;
    for (int idx = 0; idx < count(); idx++) {
        QVariant data = tabData(idx);
        if (!data.isValid()) {
            continue;
        }
        result.append(data.value<KateTabButtonData>().doc);
    }
    return result;
}