aboutsummaryrefslogtreecommitdiffstats
path: root/src/plugins
diff options
context:
space:
mode:
authorEike Ziller <[email protected]>2023-11-15 08:56:19 +0100
committerEike Ziller <[email protected]>2025-07-01 14:03:43 +0000
commit05bae4aca15a24ed46acf7928a18f0af953c69ec (patch)
tree12a4aa0edd8e1421e34403e030c4f23bda8e2c9c /src/plugins
parentb607c1ba96df6ade250809df3fc7c2ec876f7685 (diff)
Add optional tabs to the editor viewsHEADmaster
Tabs behave differently than our non-tabbed views in some ways: - Without tabs, we did not keep a list of "documents that are open here" for the editor views (splits). Certain sets of IEditor instances were managed by each view, and we keep a history per view, but that isn't the same (see below). All documents were available to choose from a views document dropdown. Now we have to keep such a list per view. - For performance / to limit resource consumption we limit the number of actually open documents and "suspend" documents that are not visible when exceeded this threshhold (Preferences > Environment > System > Auto-suspend). We need to keep tabs open even if an IEditor instance is actually closed. Without tabs, we just removed the IEditor from the view and closed it, and just kept an entry in the list of "open" documents in the DocumentModel. - Because views didn't really have documents "attached" to them, IEditor instances can be internally moved to another view, if they are not visible in the origin view. This avoids duplicating editors which saves resources again. Now we have to keep the tab for the origin view. - Because views didn't really have documents "attached" to them, it didn't make sense to show a view as empty, if there are documents "open" in general. If the last IEditor is removed/closed from a view, we showed another editor in that view. In contrast, when you close the last tab in a view, it would be unexpected to automatically open a new tab. It is actually expected to show no editor in that case. Also, when a tab is closed that is not the last one, we should switch to another tab in the view, not open some editor there that might not have a tab in that view anymore. - When closing a view (split), effectively no document was ever closed, and if there are modified documents the user was consequently not asked to save them. When closing a view with tabs, this is unexpected. Basically all "open" documents should be represented by a tab in some view at all times. - And the other way round, sometimes we asked about closing modified editors when we should not do that when showing tabs: - open editor A in a view, and editor B, so A is no longer visible - open A in a different view -> it is moved to the new view - modify A and close it -> we would ask about closing (and close the document if accepted) because it is the only IEditor instance for the document Alternatively this could happen if editor A is suspended before opening it in a different view. We didn't keep note of the editor A in the first view. With tabs, this is unexpected: Even though the editor is closed in the second view, a tab is still visible in the first view, even though it doesn't refer to an IEditor instance. With tabs we need to keep hold of the IEditor (with the modifications) and just close the tab in the second view. This patch adds a QTabBar to each view. The tabs in the tab bar is used as the list of "open documents in the tab". Each tab corresponds to a DocumentModel::Entry, and an IEditor instance if that is available (which isn't the case if e.g. the document was suspended, or the editor moved to a different view). That must be taken into account at various places in the code now, if tabs are actually shown. The existing code in closeEditors and placeEditor, that ensures (or at least tried to ensure) that a view switches to a different editor if the current editor gets removed, is generalized to switch to a different "tab" first, and to not open an arbitrary editor if tabs are actually shown for the editors. This change does *not*: - Add shortcuts for moving between tabs left/right in a tab bar. - Add drag and drop of tabs between views. Tabs are movable within their tab bar as of the QTabBar::setMovable functionality. Dragging editors to a different view is currently only possible with the file icon in the tool bar, like with the non-tabbed view. - Handle the UI issues when lots of tabs are open in any way. The standard behavior of QTabBar is that tabs are squeezed together, and after some minimum size is reached some "left/right" buttons are shown. There is the general question of what to do in that case, and if fixes/improvements should be done in QTabBar. Fixes: QTCREATORBUG-31644 Change-Id: Iaaad6d6bc8656eb3fe93e8e17b038082e64b77f0 Reviewed-by: hjk <[email protected]> Reviewed-by: David Schulz <[email protected]>
Diffstat (limited to 'src/plugins')
-rw-r--r--src/plugins/coreplugin/editormanager/editormanager.cpp317
-rw-r--r--src/plugins/coreplugin/editormanager/editormanager_p.h13
-rw-r--r--src/plugins/coreplugin/editormanager/editorview.cpp234
-rw-r--r--src/plugins/coreplugin/editormanager/editorview.h29
-rw-r--r--src/plugins/coreplugin/editortoolbar.cpp6
-rw-r--r--src/plugins/coreplugin/editortoolbar.h1
-rw-r--r--src/plugins/coreplugin/generalsettings.cpp11
-rw-r--r--src/plugins/coreplugin/generalsettings.h1
8 files changed, 501 insertions, 111 deletions
diff --git a/src/plugins/coreplugin/editormanager/editormanager.cpp b/src/plugins/coreplugin/editormanager/editormanager.cpp
index 41b61461ccf..d050b51a46b 100644
--- a/src/plugins/coreplugin/editormanager/editormanager.cpp
+++ b/src/plugins/coreplugin/editormanager/editormanager.cpp
@@ -4,15 +4,6 @@
#include "editormanager.h"
#include "editormanager_p.h"
-#include "documentmodel.h"
-#include "documentmodel_p.h"
-#include "editorview.h"
-#include "editorwindow.h"
-#include "ieditor.h"
-#include "ieditorfactory.h"
-#include "ieditorfactory_p.h"
-#include "openeditorsview.h"
-#include "openeditorswindow.h"
#include "../actionmanager/actioncontainer.h"
#include "../actionmanager/actionmanager.h"
#include "../actionmanager/command.h"
@@ -34,6 +25,15 @@
#include "../settingsdatabase.h"
#include "../systemsettings.h"
#include "../vcsmanager.h"
+#include "documentmodel.h"
+#include "documentmodel_p.h"
+#include "editorview.h"
+#include "editorwindow.h"
+#include "ieditor.h"
+#include "ieditorfactory.h"
+#include "ieditorfactory_p.h"
+#include "openeditorsview.h"
+#include "openeditorswindow.h"
#include <extensionsystem/pluginmanager.h>
@@ -1408,25 +1408,21 @@ IEditor *EditorManagerPrivate::placeEditor(EditorView *view, IEditor *editor)
const QByteArray state = editor->saveState();
if (EditorView *sourceView = viewForEditor(editor)) {
- // try duplication or pull editor over to new view
- bool duplicateSupported = editor->duplicateSupported();
- if (editor != sourceView->currentEditor() || !duplicateSupported) {
+ // Pulling the editor over is preferred in case the editor is currently not visible,
+ // to decrease resource consumption. Otherwise we duplicate if the editor supports it.
+ const bool duplicateSupported = editor->duplicateSupported();
+ const bool isEditorVisible = editor == sourceView->currentEditor();
+ if (!isEditorVisible || !duplicateSupported) {
// pull the IEditor over to the new view
- sourceView->removeEditor(editor);
+ removeEditorsFromViews(
+ {{sourceView, editor}},
+ !duplicateSupported ? EditorView::RemoveTab : EditorView::KeepTab,
+ RemoveEditorFlag::EnsureNewEditor);
view->addEditor(editor);
// possibly adapts old state to new layout
editor->restoreState(state);
- if (!sourceView->currentEditor()) {
- EditorView *replacementView = nullptr;
- if (IEditor *replacement = pickUnusedEditor(&replacementView)) {
- if (replacementView)
- replacementView->removeEditor(replacement);
- sourceView->addEditor(replacement);
- sourceView->setCurrentEditor(replacement);
- }
- }
return editor;
- } else if (duplicateSupported) {
+ } else if (QTC_GUARD(duplicateSupported)) {
editor = duplicateEditor(editor);
Q_ASSERT(editor);
}
@@ -1506,16 +1502,126 @@ bool EditorManagerPrivate::activateEditorForEntry(EditorView *view, DocumentMode
return true;
}
+// Removes the editors given in \a editorsPerView from the respective view.
+// If \a option specifies to remove tabs, and \a flag is \c EnsureNewEditor,
+// for any editor that is the current editor of its view, a new editor or tab
+// is switched to, as appropriate.
+void EditorManagerPrivate::removeEditorsFromViews(
+ const QList<std::pair<EditorView *, IEditor *>> &editorsPerView,
+ EditorView::RemovalOption option,
+ RemoveEditorFlag flag)
+{
+ EditorView *globallyCurrentView = currentEditorView();
+ QList<std::pair<EditorView *, IEditor *>> secondPass;
+ // First close all editors that are not the current editor of a view,
+ // then all "current" editors.
+ // Since we might switch to a different editor/tab when we close the "current" editor
+ // of a view, and doing that might move another (non-current) editor from a different
+ // view, we prevent that from messing with our list of editors to close. Otherwise,
+ // a (non-current) editor that we have in our list might move to be the "current" editor
+ // of a different view before we have processed (and closed) it.
+ for (const std::pair<EditorView *, IEditor *> &item : editorsPerView) {
+ EditorView *view = item.first;
+ IEditor *editor = item.second;
+ QTC_ASSERT(view->hasEditor(editor), continue);
+ if (flag == RemoveEditorFlag::None || editor != view->currentEditor()
+ || option == EditorView::KeepTab) {
+ view->removeEditor(editor, option);
+ continue;
+ }
+ secondPass.append(item);
+ }
+
+ // Second pass. Close "current" editors, and choose a different one to show in the view,
+ // as appropriate.
+ for (const std::pair<EditorView *, IEditor *> &item : secondPass) {
+ EditorView *view = item.first;
+ IEditor *editor = item.second;
+ // Prefer setting an IEditor that already is in the view
+ const QList<IEditor *> editors = view->editors(); // last seen are at the back, current last
+ if (editors.size() > 1) {
+ // has more than only the closed editor,
+ // so set the last seen one as the new current
+ view->setCurrentEditor(editors.at(editors.size() - 2));
+ } else {
+ const EditorManager::OpenEditorFlags openEditorflags
+ = view != globallyCurrentView ? EditorManager::DoNotChangeCurrentEditor
+ : EditorManager::NoFlags;
+ // Find a next (suspended) tab to open instead.
+ // If the view doesn't show tabs, this still represents previously opened documents there.
+ const QList<EditorView::TabData> tabs = view->tabs();
+ const int tabToBeRemoved = view->tabForEditor(editor);
+ if (tabs.size() > 1 && QTC_GUARD(tabToBeRemoved >= 0)) {
+ const int tabToSwitchTo = tabToBeRemoved > 0 ? tabToBeRemoved - 1
+ : tabToBeRemoved + 1;
+ if (QTC_GUARD(tabToSwitchTo < tabs.size()))
+ activateEditorForEntry(view, tabs.at(tabToSwitchTo).entry, openEditorflags);
+ } else if (!view->isShowingTabs()) {
+ // If the view *is* showing tabs, we don't want to open an arbitrary editor there,
+ // but in the non-tabbed case we want to avoid an "empty" view, so pick something.
+ // TODO do not choose an editor for the same document that we were closing
+ IEditor *newCurrent = pickUnusedEditor();
+ if (newCurrent) {
+ activateEditor(view, newCurrent, openEditorflags);
+ } else {
+ DocumentModel::Entry *entry = DocumentModelPrivate::firstSuspendedEntry();
+ if (entry) {
+ activateEditorForEntry(view, entry, openEditorflags);
+ } else { // no "suspended" ones, so any entry left should have a document
+ const QList<DocumentModel::Entry *> documents = DocumentModel::entries();
+ if (!documents.isEmpty()) {
+ if (IDocument *document = documents.last()->document) {
+ // Do not auto-switch to design mode if the new editor will be for
+ // the same document as the one that was closed.
+ // TODO we should not open the same document that we closed in the
+ // first place
+ const EditorManager::OpenEditorFlags addFlags
+ = (view == globallyCurrentView && document == editor->document())
+ ? EditorManager::DoNotSwitchToDesignMode
+ : EditorManager::NoFlags;
+ activateEditorForDocument(view, document, openEditorflags | addFlags);
+ }
+ }
+ }
+ }
+ }
+ }
+ view->removeEditor(editor, option);
+ }
+}
+
void EditorManagerPrivate::closeEditorOrDocument(IEditor *editor)
{
QTC_ASSERT(editor, return);
+
+ static const auto isEditorDocumentVisibleInDifferentView = [](IEditor *editor) {
+ const QList<EditorView *> allViews = EditorManagerPrivate::allEditorViews();
+ return Utils::contains(allViews, [editor](EditorView *view) {
+ if (view->hasEditor(editor)) // this is the view where the editor comes from, ignore
+ return false;
+ return Utils::contains(view->visibleTabs(), [editor](const EditorView::TabData &tabData) {
+ return tabData.entry->document == editor->document();
+ });
+ });
+ };
+
EditorManager::addCurrentPositionToNavigationHistory();
- QList<IEditor *> visible = EditorManager::visibleEditors();
- if (Utils::contains(visible,
- [&editor](IEditor *other) {
- return editor != other && other->document() == editor->document();
- })) {
- EditorManager::closeEditors({editor});
+ if (isEditorDocumentVisibleInDifferentView(editor)) {
+ // Another view either shows a tab for the document, or it is the visible editor there if
+ // tabs are not shown
+ EditorManagerPrivate::addClosedDocumentToCloseHistory(editor);
+ if (DocumentModel::editorsForDocument(editor->document()).size() == 1) {
+ // It's the only editor for that file, but tabs are still open somewhere
+ // so we need to keep it around (--> in the editor model).
+ // If the view doesn't show tabs, we do not end up in this code.
+ removeEditorsFromViews(
+ {{viewForEditor(editor), editor}},
+ EditorView::RemoveTab,
+ RemoveEditorFlag::EnsureNewEditor);
+ } else {
+ EditorManagerPrivate::closeEditors(
+ {editor}, EditorManagerPrivate::CloseFlag::SuspendRemoveTab);
+ }
} else {
EditorManager::closeDocuments({editor->document()});
}
@@ -1597,16 +1703,17 @@ bool EditorManagerPrivate::closeEditors(const QList<IEditor*> &editors, CloseFla
// Remove accepted editors from document model/manager and context list,
// and sort them per view, so we can remove them from views in an orderly
// manner.
- QMultiHash<EditorView *, IEditor *> editorsPerView;
+ QList<std::pair<EditorView *, IEditor *>> editorsPerView;
for (IEditor *editor : std::as_const(acceptedEditors)) {
emit m_instance->editorAboutToClose(editor);
const DocumentModel::Entry *entry = DocumentModel::entryForDocument(editor->document());
// If the file is pinned, closing it should remove the editor but keep it in Open Documents.
const bool isPinned = QTC_GUARD(entry) && entry->pinned;
- const bool removeSuspendedEntry = !isPinned && flag != CloseFlag::Suspend;
+ const bool removeSuspendedEntry = !isPinned && flag != CloseFlag::Suspend
+ && flag != CloseFlag::SuspendRemoveTab;
removeEditor(editor, removeSuspendedEntry);
if (EditorView *view = viewForEditor(editor)) {
- editorsPerView.insert(view, editor);
+ editorsPerView.append({view, editor});
if (QApplication::focusWidget()
&& QApplication::focusWidget() == editor->widget()->focusWidget()) {
focusView = view;
@@ -1615,68 +1722,10 @@ bool EditorManagerPrivate::closeEditors(const QList<IEditor*> &editors, CloseFla
}
QTC_CHECK(!focusView || focusView == currentView);
- // Go through views, remove the editors from them.
- // Sort such that views for which the current editor is closed come last,
- // and if the global current view is one of them, that comes very last.
- // When handling the last view in the list we handle the case where all
- // visible editors are closed, and we need to e.g. revive an invisible or
- // a suspended editor
- const QList<EditorView *> views = Utils::sorted(editorsPerView.keys(),
- [editorsPerView, currentView](EditorView *a, EditorView *b) {
- if (a == b)
- return false;
- const bool aHasCurrent = editorsPerView.values(a).contains(a->currentEditor());
- const bool bHasCurrent = editorsPerView.values(b).contains(b->currentEditor());
- const bool aHasGlobalCurrent = (a == currentView && aHasCurrent);
- const bool bHasGlobalCurrent = (b == currentView && bHasCurrent);
- if (bHasGlobalCurrent && !aHasGlobalCurrent)
- return true;
- if (bHasCurrent && !aHasCurrent)
- return true;
- return false;
- });
- for (EditorView *view : views) {
- QList<IEditor *> editors = editorsPerView.values(view);
- // handle current editor in view last
- IEditor *viewCurrentEditor = view->currentEditor();
- if (editors.contains(viewCurrentEditor) && editors.last() != viewCurrentEditor) {
- editors.removeAll(viewCurrentEditor);
- editors.append(viewCurrentEditor);
- }
- for (IEditor *editor : std::as_const(editors)) {
- if (editor == viewCurrentEditor && view == views.last()) {
- // Avoid removing the globally current editor from its view,
- // set a new current editor before.
- EditorManager::OpenEditorFlags flags = view != currentView
- ? EditorManager::DoNotChangeCurrentEditor : EditorManager::NoFlags;
- const QList<IEditor *> viewEditors = view->editors();
- IEditor *newCurrent = viewEditors.size() > 1 ? viewEditors.at(viewEditors.size() - 2)
- : nullptr;
- if (!newCurrent)
- newCurrent = pickUnusedEditor();
- if (newCurrent) {
- activateEditor(view, newCurrent, flags);
- } else {
- DocumentModel::Entry *entry = DocumentModelPrivate::firstSuspendedEntry();
- if (entry) {
- activateEditorForEntry(view, entry, flags);
- } else { // no "suspended" ones, so any entry left should have a document
- const QList<DocumentModel::Entry *> documents = DocumentModel::entries();
- if (!documents.isEmpty()) {
- if (IDocument *document = documents.last()->document) {
- // Do not auto-switch to design mode if the new editor will be for
- // the same document as the one that was closed.
- if (view == currentView && document == editor->document())
- flags = EditorManager::DoNotSwitchToDesignMode;
- activateEditorForDocument(view, document, flags);
- }
- }
- }
- }
- }
- view->removeEditor(editor);
- }
- }
+ removeEditorsFromViews(
+ editorsPerView,
+ flag == CloseFlag::Suspend ? EditorView::KeepTab : EditorView::RemoveTab,
+ RemoveEditorFlag::EnsureNewEditor);
emit m_instance->editorsClosed(Utils::toList(acceptedEditors));
@@ -1690,6 +1739,49 @@ bool EditorManagerPrivate::closeEditors(const QList<IEditor*> &editors, CloseFla
return !closingFailed;
}
+void EditorManagerPrivate::tabClosed(DocumentModel::Entry *entry)
+{
+ // a tab was closed that wasn't backed by an IEditor (e.g. suspended)
+ // close the entry if it was the only one
+ const QList<EditorView *> allViews = EditorManagerPrivate::allEditorViews();
+ if (Utils::contains(allViews, [entry](EditorView *view) {
+ return Utils::contains(
+ view->visibleTabs(), Utils::equal(&EditorView::TabData::entry, entry));
+ })) {
+ return;
+ }
+ DocumentModelPrivate::removeEntry(entry);
+}
+
+// Collects all tabs from the given viewsToClose for which no other tab is shown anywhere.
+QSet<DocumentModel::Entry *> EditorManagerPrivate::entriesToCloseForTabbedViews(
+ const QSet<EditorView *> &viewsToClose)
+{
+ QSet<DocumentModel::Entry *> entriesToClose;
+ QSet<DocumentModel::Entry *> visibleTabs;
+ for (EditorView *view : viewsToClose) {
+ if (view->isShowingTabs()) {
+ const QList<EditorView::TabData> tabs = view->visibleTabs();
+ for (const EditorView::TabData &tab : tabs)
+ visibleTabs.insert(tab.entry);
+ }
+ }
+ if (!visibleTabs.isEmpty()) {
+ const QSet<EditorView *> allOtherViews
+ = toSet(EditorManagerPrivate::allEditorViews()).subtract(viewsToClose);
+ for (DocumentModel::Entry *tabEntry : visibleTabs) {
+ if (!Utils::contains(allOtherViews, [tabEntry](EditorView *otherView) {
+ return Utils::contains(
+ otherView->visibleTabs(),
+ Utils::equal(&EditorView::TabData::entry, tabEntry));
+ })) {
+ entriesToClose.insert(tabEntry);
+ }
+ }
+ }
+ return entriesToClose;
+}
+
void EditorManagerPrivate::activateView(EditorView *view)
{
QTC_ASSERT(view, return);
@@ -1798,6 +1890,13 @@ void EditorManagerPrivate::closeView(EditorView *view)
if (!view)
return;
+ // Check if there are documents that should be closed because there are no other views
+ // with tabs open for it.
+ const QSet<DocumentModel::Entry *> entriesToClose
+ = EditorManagerPrivate::entriesToCloseForTabbedViews({view});
+ if (!entriesToClose.isEmpty() && !EditorManager::closeDocuments(toList(entriesToClose)))
+ return;
+
const QList<IEditor *> editorsToDelete = emptyView(view);
EditorView *newCurrent = view->editorArea()->unsplit(view);
if (newCurrent)
@@ -1827,11 +1926,11 @@ const QList<IEditor *> EditorManagerPrivate::emptyView(EditorView *view)
setCurrentView(view);
setCurrentEditor(nullptr);
}
- view->removeEditor(editor);
+ removeEditorsFromViews({{view, editor}}, EditorView::RemoveTab, RemoveEditorFlag::None);
} else {
emit m_instance->editorAboutToClose(editor);
removeEditor(editor, true /*=removeSuspendedEntry, but doesn't matter since it's not the last editor anyhow*/);
- view->removeEditor(editor);
+ removeEditorsFromViews({{view, editor}}, EditorView::RemoveTab, RemoveEditorFlag::None);
removedEditors.append(editor);
}
}
@@ -1850,6 +1949,13 @@ void EditorManagerPrivate::deleteEditors(const QList<IEditor *> &editors)
}
}
+void EditorManagerPrivate::setShowingTabs(bool visible)
+{
+ const QList<EditorView *> allViews = allEditorViews();
+ for (EditorView *view : allViews)
+ view->setTabsVisible(visible);
+}
+
EditorWindow *EditorManagerPrivate::createEditorWindow()
{
auto win = new EditorWindow;
@@ -2257,7 +2363,11 @@ void EditorManagerPrivate::gotoPreviousSplit()
void EditorManagerPrivate::addClosedDocumentToCloseHistory(IEditor *editor)
{
EditorView *view = EditorManagerPrivate::viewForEditor(editor);
- QTC_ASSERT(view, return);
+ // Editors can be owned by the document model.
+ // E.g. split, open some document there, modify it, close the split
+ // -> we do not close the editor, because we'd lose the modifications.
+ if (!view)
+ return;
view->addClosedEditorToCloseHistory(editor);
EditorManagerPrivate::updateActions();
}
@@ -2678,6 +2788,19 @@ void EditorManagerPrivate::removeAllSplits()
QTC_ASSERT(view, return);
EditorArea *currentArea = view->editorArea();
QTC_ASSERT(currentArea, return);
+ // Check if there are documents that should be closed because there are no other views
+ // with tabs open for it.
+ QSet<EditorView *> viewsToClose; // all views from the area except the current one
+ EditorView *current = currentArea->findFirstView();
+ while (current) {
+ if (current != view)
+ viewsToClose.insert(current);
+ current = current->findNextView();
+ }
+ const QSet<DocumentModel::Entry *> entriesToClose
+ = EditorManagerPrivate::entriesToCloseForTabbedViews(viewsToClose);
+ if (!entriesToClose.isEmpty() && !EditorManager::closeDocuments(toList(entriesToClose)))
+ return;
currentArea->unsplitAll(view);
}
diff --git a/src/plugins/coreplugin/editormanager/editormanager_p.h b/src/plugins/coreplugin/editormanager/editormanager_p.h
index fea522e4bed..1a96a49860d 100644
--- a/src/plugins/coreplugin/editormanager/editormanager_p.h
+++ b/src/plugins/coreplugin/editormanager/editormanager_p.h
@@ -50,9 +50,12 @@ public:
enum class CloseFlag {
CloseWithAsking,
CloseWithoutAsking,
- Suspend
+ Suspend, // tab is kept, and document model entry is suspended if editor was the last one
+ SuspendRemoveTab // tab is removed, but document model entry is suspended if editor was the last one
};
+ enum class RemoveEditorFlag { None, EnsureNewEditor };
+
static EditorManagerPrivate *instance();
static void extensionsInitialized(); // only use from MainWindow
@@ -83,6 +86,13 @@ public:
/* closes the document if there is no other editor on the document visible */
static void closeEditorOrDocument(IEditor *editor);
static bool closeEditors(const QList<IEditor *> &editors, CloseFlag flag);
+ static void tabClosed(DocumentModel::Entry *entry);
+ static QSet<DocumentModel::Entry *> entriesToCloseForTabbedViews(
+ const QSet<EditorView *> &viewsToClose);
+ static void removeEditorsFromViews(
+ const QList<std::pair<EditorView *, IEditor *>> &editorsPerView,
+ EditorView::RemovalOption option,
+ RemoveEditorFlag flag);
static EditorView *viewForEditor(IEditor *editor);
static void setCurrentView(EditorView *view);
@@ -105,6 +115,7 @@ public:
static void splitNewWindow(Internal::EditorView *view);
static void closeView(Internal::EditorView *view);
static const QList<IEditor *> emptyView(Internal::EditorView *view);
+ static void setShowingTabs(bool visible);
static void deleteEditors(const QList<IEditor *> &editors);
static void updateActions();
diff --git a/src/plugins/coreplugin/editormanager/editorview.cpp b/src/plugins/coreplugin/editormanager/editorview.cpp
index 4e9e040a01f..f38e944ae0b 100644
--- a/src/plugins/coreplugin/editormanager/editorview.cpp
+++ b/src/plugins/coreplugin/editormanager/editorview.cpp
@@ -3,16 +3,18 @@
#include "editorview.h"
-#include "editormanager.h"
-#include "editormanager_p.h"
-#include "documentmodel.h"
-#include "documentmodel_p.h"
#include "../editormanager/ieditor.h"
#include "../editortoolbar.h"
#include "../findplaceholder.h"
+#include "../generalsettings.h"
#include "../minisplitter.h"
+#include "documentmodel.h"
+#include "documentmodel_p.h"
+#include "editormanager.h"
+#include "editormanager_p.h"
#include <utils/algorithm.h>
+#include <utils/environment.h>
#include <utils/infobar.h>
#include <utils/layoutbuilder.h>
#include <utils/link.h>
@@ -27,10 +29,12 @@
#include <QMenu>
#include <QMouseEvent>
#include <QPainter>
-#include <QStackedWidget>
-#include <QToolButton>
#include <QSplitter>
#include <QStackedLayout>
+#include <QStackedWidget>
+#include <QStylePainter>
+#include <QTabBar>
+#include <QToolButton>
using namespace Core;
using namespace Utils;
@@ -39,10 +43,23 @@ namespace Core::Internal {
// EditorView
+static void updateTabText(QTabBar *tabBar, int index, IDocument *document)
+{
+ QTC_ASSERT(index >= 0 && index < tabBar->count(), return);
+ const auto data = tabBar->tabData(index).value<EditorView::TabData>();
+ QString title = document->displayName();
+ if (document->isModified())
+ title += '*';
+ if (qtcEnvironmentVariableIsSet("QTC_DEBUG_DOCUMENTMODEL") && !data.editor)
+ title += " (s)";
+ tabBar->setTabText(index, title);
+}
+
EditorView::EditorView(SplitterOrView *parentSplitterOrView, QWidget *parent)
: QWidget(parent)
, m_parentSplitterOrView(parentSplitterOrView)
, m_toolBar(new EditorToolBar(this))
+ , m_tabBar(new QTabBar(this))
, m_container(new QStackedWidget(this))
, m_infoBarDisplay(new InfoBarDisplay(this))
, m_statusHLine(Layouting::createHr(this))
@@ -74,7 +91,79 @@ EditorView::EditorView(SplitterOrView *parentSplitterOrView, QWidget *parent)
tl->addWidget(m_toolBar);
}
- m_infoBarDisplay->setTarget(tl, 1);
+ m_tabBar->setVisible(false);
+ m_tabBar->setDocumentMode(true);
+ m_tabBar->setUsesScrollButtons(true);
+ m_tabBar->setExpanding(false);
+ m_tabBar->setMovable(true);
+ m_tabBar->setTabsClosable(true);
+ m_tabBar->setContextMenuPolicy(Qt::CustomContextMenu);
+ m_tabBar->setShape(QTabBar::RoundedNorth);
+ connect(m_tabBar, &QTabBar::tabBarClicked, this, &EditorView::activateTab);
+ connect(
+ m_tabBar,
+ &QTabBar::tabCloseRequested,
+ this,
+ [this](int index) {
+ const auto data = m_tabBar->tabData(index).value<TabData>();
+ if (data.editor)
+ EditorManagerPrivate::closeEditorOrDocument(data.editor);
+ else {
+ m_tabBar->removeTab(index);
+ EditorManagerPrivate::tabClosed(data.entry);
+ }
+ },
+ Qt::QueuedConnection /* do not modify tab bar in tab bar signal */);
+ connect(
+ m_tabBar,
+ &QWidget::customContextMenuRequested,
+ m_tabBar,
+ [this](const QPoint &pos) {
+ const int index = m_tabBar->tabAt(pos);
+ if (index < 0 || index >= m_tabBar->count())
+ return;
+ const auto data = m_tabBar->tabData(index).value<TabData>();
+ QMenu menu;
+ EditorManager::addContextMenuActions(&menu, data.entry, data.editor);
+ menu.exec(m_tabBar->mapToGlobal(pos));
+ },
+ Qt::QueuedConnection);
+ // We cannot watch for IDocument changes, because the tab might refer
+ // to a suspended document. And if a new editor for that is opened in another view,
+ // this view will not know about that.
+ connect(
+ DocumentModel::model(),
+ &QAbstractItemModel::dataChanged,
+ m_tabBar,
+ [this](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
+ for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
+ DocumentModel::Entry *e = DocumentModel::entryAtRow(i);
+ const int tabIndex = tabForEntry(e);
+ if (tabIndex >= 0)
+ updateTabText(m_tabBar, tabIndex, e->document);
+ }
+ });
+ // Watch for items that are removed from the document model, e.g. suspended items
+ // when the user closes one in Open Documents view, which we otherwise not get notified for
+ connect(
+ DocumentModel::model(),
+ &QAbstractItemModel::rowsAboutToBeRemoved,
+ m_tabBar,
+ [this](const QModelIndex & /*parent*/, int first, int last) {
+ for (int i = first; i <= last; ++i) {
+ DocumentModel::Entry *e = DocumentModel::entryAtRow(i);
+ const int tabIndex = tabForEntry(e);
+ if (tabIndex >= 0) {
+ const auto data = m_tabBar->tabData(tabIndex).value<TabData>();
+ // for editors we get a call of removeEditor, but not for suspended tabs
+ if (!data.editor)
+ m_tabBar->removeTab(tabIndex);
+ }
+ }
+ });
+ tl->addWidget(m_tabBar);
+
+ m_infoBarDisplay->setTarget(tl, 2);
tl->addWidget(m_container);
@@ -286,6 +375,18 @@ void EditorView::setCloseSplitIcon(const QIcon &icon)
m_toolBar->setCloseSplitIcon(icon);
}
+bool EditorView::isShowingTabs() const
+{
+ return m_isShowingTabs;
+}
+
+void EditorView::setTabsVisible(bool visible)
+{
+ m_isShowingTabs = visible;
+ m_tabBar->setVisible(m_isShowingTabs);
+ m_toolBar->setDocumentDropdownVisible(!visible);
+}
+
bool EditorView::canGoForward() const
{
return m_currentNavigationHistoryPosition < m_navigationHistory.size() - 1;
@@ -320,6 +421,36 @@ void EditorView::updateEditorHistory(IEditor *editor, QList<EditLocation> &histo
history.prepend(location);
}
+void EditorView::saveTabState(QDataStream *stream) const
+{
+ QStringList tabs;
+ for (int i = 0; i < m_tabBar->count(); ++i) {
+ const auto data = m_tabBar->tabData(i).value<TabData>();
+ IDocument *document = data.entry->document;
+ const FilePath path = document->filePath();
+ if (!document->isTemporary() && !path.isEmpty())
+ tabs << path.toUrlishString();
+ }
+ *stream << tabs;
+}
+
+void EditorView::restoreTabState(QDataStream *stream)
+{
+ if (stream->atEnd())
+ return;
+ QStringList tabs;
+ *stream >> tabs;
+ for (const QString &tab : std::as_const(tabs)) {
+ DocumentModel::Entry *entry = DocumentModel::entryForFilePath(FilePath::fromString(tab));
+ if (!entry)
+ continue;
+ m_tabBar->addTab(""); // text set below
+ const int tabIndex = m_tabBar->count() - 1;
+ m_tabBar->setTabData(tabIndex, QVariant::fromValue(TabData({nullptr, entry})));
+ updateTabText(m_tabBar, tabIndex, entry->document);
+ }
+}
+
void EditorView::paintEvent(QPaintEvent *)
{
EditorView *editorView = EditorManagerPrivate::currentEditorView();
@@ -377,12 +508,23 @@ void EditorView::addEditor(IEditor *editor)
{
if (m_editors.contains(editor))
return;
+ QTC_ASSERT(
+ !Utils::contains(m_editors, Utils::equal(&IEditor::document, editor->document())), return);
m_editors.append(editor);
m_container->addWidget(editor->widget());
m_widgetEditorMap.insert(editor->widget(), editor);
m_toolBar->addEditor(editor);
+ IDocument *document = editor->document();
+ int tabIndex = tabForEditor(editor);
+ if (tabIndex < 0)
+ tabIndex = m_tabBar->addTab(""); // text set below
+ m_tabBar->setTabData(
+ tabIndex, QVariant::fromValue(TabData({editor, DocumentModel::entryForDocument(document)})));
+ updateTabText(m_tabBar, tabIndex, document);
+ m_tabBar->setVisible(false); // something is wrong with QTabBar... this is needed
+ m_tabBar->setVisible(m_isShowingTabs);
if (editor == currentEditor())
setCurrentEditor(editor);
@@ -393,7 +535,7 @@ bool EditorView::hasEditor(IEditor *editor) const
return m_editors.contains(editor);
}
-void EditorView::removeEditor(IEditor *editor)
+void EditorView::removeEditor(IEditor *editor, RemovalOption option)
{
QTC_ASSERT(editor, return);
if (!m_editors.contains(editor))
@@ -401,7 +543,7 @@ void EditorView::removeEditor(IEditor *editor)
const int index = m_container->indexOf(editor->widget());
QTC_ASSERT((index != -1), return);
- bool wasCurrent = (index == m_container->currentIndex());
+ const bool wasCurrent = (index == m_container->currentIndex());
m_editors.removeAll(editor);
m_container->removeWidget(editor->widget());
@@ -409,8 +551,19 @@ void EditorView::removeEditor(IEditor *editor)
editor->widget()->setParent(nullptr);
m_toolBar->removeToolbarForEditor(editor);
+ const int tabIndex = tabForEditor(editor);
+ if (QTC_GUARD(tabIndex >= 0)) {
+ if (option == RemoveTab) {
+ m_tabBar->removeTab(tabIndex);
+ } else {
+ const auto data = m_tabBar->tabData(tabIndex).value<TabData>();
+ m_tabBar->setTabData(tabIndex, QVariant::fromValue(TabData({nullptr, data.entry})));
+ updateTabText(m_tabBar, tabIndex, editor->document());
+ }
+ }
+
if (wasCurrent)
- setCurrentEditor(!m_editors.isEmpty() ? m_editors.last() : nullptr);
+ setCurrentEditor(nullptr);
}
IEditor *EditorView::currentEditor() const
@@ -458,6 +611,45 @@ void EditorView::closeSplit()
EditorManagerPrivate::updateActions();
}
+int EditorView::tabForEditor(IEditor *editor) const
+{
+ for (int i = 0; i < m_tabBar->count(); ++i) {
+ const auto data = m_tabBar->tabData(i).value<TabData>();
+ if (data.editor ? data.editor == editor : data.entry->document == editor->document())
+ return i;
+ }
+ return -1;
+}
+
+QList<EditorView::TabData> EditorView::tabs() const
+{
+ QList<TabData> result;
+ for (int i = 0; i < m_tabBar->count(); ++i)
+ result.append(m_tabBar->tabData(i).value<TabData>());
+ return result;
+}
+
+int EditorView::tabForEntry(DocumentModel::Entry *entry) const
+{
+ for (int i = 0; i < m_tabBar->count(); ++i) {
+ const auto data = m_tabBar->tabData(i).value<TabData>();
+ if (data.entry == entry)
+ return i;
+ }
+ return -1;
+}
+
+void EditorView::activateTab(int index)
+{
+ if (index < 0) // this happens when clicking in the bar outside of tabs on macOS...
+ return;
+ const auto data = m_tabBar->tabData(index).value<TabData>();
+ if (data.editor)
+ EditorManagerPrivate::activateEditor(this, data.editor);
+ else
+ EditorManagerPrivate::activateEditorForEntry(this, data.entry);
+}
+
void EditorView::openDroppedFiles(const QList<DropSupport::FileSpec> &files)
{
bool first = true;
@@ -494,6 +686,10 @@ void EditorView::setCurrentEditor(IEditor *editor)
m_toolBar->setCurrentEditor(nullptr);
m_infoBarDisplay->setInfoBar(nullptr);
m_container->setCurrentIndex(0);
+ // QTabBar still shows a tab selected in the UI when setting current index to -1
+ // but we should only temporarily end up here when tabs are actually shown
+ // anyway.
+ m_tabBar->setCurrentIndex(-1);
emit currentEditorChanged(nullptr);
return;
}
@@ -505,6 +701,8 @@ void EditorView::setCurrentEditor(IEditor *editor)
QTC_ASSERT(idx >= 0, return);
m_container->setCurrentIndex(idx);
m_toolBar->setCurrentEditor(editor);
+ const int tabIndex = tabForEditor(editor);
+ m_tabBar->setCurrentIndex(tabIndex);
updateEditorHistory(editor);
@@ -527,6 +725,16 @@ IEditor *EditorView::editorForDocument(const IDocument *document) const
return Utils::findOrDefault(m_editors, Utils::equal(&IEditor::document, document));
}
+QList<EditorView::TabData> EditorView::visibleTabs() const
+{
+ if (isShowingTabs()) {
+ return tabs();
+ }
+ if (IEditor *current = currentEditor())
+ return {TabData({current, DocumentModel::entryForDocument(current->document())})};
+ return {};
+}
+
void EditorView::updateEditorHistory(IEditor *editor)
{
updateEditorHistory(editor, m_editorHistory);
@@ -736,6 +944,7 @@ SplitterOrView::SplitterOrView(IEditor *editor)
m_layout = new QStackedLayout(this);
m_layout->setSizeConstraint(QLayout::SetNoConstraint);
m_view = new EditorView(this);
+ m_view->setTabsVisible(generalSettings().useTabsInEditorViews.value());
if (editor)
m_view->addEditor(editor);
m_splitter = nullptr;
@@ -893,6 +1102,7 @@ void SplitterOrView::unsplitAll(EditorView *currentView)
currentView->setParentSplitterOrView(this);
} else {
currentView = new EditorView(this);
+ currentView->setTabsVisible(generalSettings().useTabsInEditorViews.value());
}
m_splitter->hide();
m_layout->removeWidget(m_splitter); // workaround Qt bug
@@ -953,7 +1163,7 @@ void SplitterOrView::unsplit()
if (m_view) {
m_view->copyNavigationHistoryFrom(childView);
if (IEditor *e = childView->currentEditor()) {
- childView->removeEditor(e);
+ childView->removeEditor(e, EditorView::RemoveTab);
m_view->addEditor(e);
m_view->setCurrentEditor(e);
}
@@ -1036,6 +1246,7 @@ QByteArray SplitterOrView::saveState() const
// save edit history
stream << saveHistory(view()->editorHistory());
+ view()->saveTabState(&stream);
}
}
return bytes;
@@ -1062,6 +1273,7 @@ void SplitterOrView::restoreState(const QByteArray &state)
stream >> fileName >> id >> editorState;
if (!stream.atEnd())
stream >> historyData;
+ view()->restoreTabState(&stream);
view()->m_editorHistory = loadHistory(historyData);
if (!QFileInfo::exists(fileName))
diff --git a/src/plugins/coreplugin/editormanager/editorview.h b/src/plugins/coreplugin/editormanager/editorview.h
index 2049379dc0e..e757b3d26b2 100644
--- a/src/plugins/coreplugin/editormanager/editorview.h
+++ b/src/plugins/coreplugin/editormanager/editorview.h
@@ -3,6 +3,8 @@
#pragma once
+#include "documentmodel.h"
+
#include <utils/dropsupport.h>
#include <utils/filepath.h>
#include <utils/id.h>
@@ -15,12 +17,14 @@
#include <functional>
QT_BEGIN_NAMESPACE
+class QDataStream;
class QFrame;
class QLabel;
class QMenu;
class QSplitter;
class QStackedLayout;
class QStackedWidget;
+class QTabBar;
class QToolButton;
QT_END_NAMESPACE
@@ -58,6 +62,14 @@ class EditorView : public QWidget
Q_OBJECT
public:
+ enum RemovalOption { RemoveTab, KeepTab };
+
+ struct TabData
+ {
+ IEditor *editor = nullptr;
+ DocumentModel::Entry *entry = nullptr;
+ };
+
explicit EditorView(SplitterOrView *parentSplitterOrView, QWidget *parent = nullptr);
~EditorView() override;
@@ -71,7 +83,7 @@ public:
int editorCount() const;
void addEditor(IEditor *editor);
- void removeEditor(IEditor *editor);
+ void removeEditor(IEditor *editor, RemovalOption option = RemoveTab);
IEditor *currentEditor() const;
void setCurrentEditor(IEditor *editor);
@@ -79,6 +91,11 @@ public:
QList<IEditor *> editors() const;
IEditor *editorForDocument(const IDocument *document) const;
+ // If no tabs are shown, this is just the current, visible editor, if any
+ QList<TabData> visibleTabs() const;
+ int tabForEditor(IEditor *editor) const;
+ // all "tabs" (even if no actual tabs are shown)
+ QList<TabData> tabs() const;
void showEditorStatusBar(const QString &id,
const QString &infoText,
@@ -88,6 +105,9 @@ public:
void setCloseSplitEnabled(bool enable);
void setCloseSplitIcon(const QIcon &icon);
+ bool isShowingTabs() const;
+ void setTabsVisible(bool visible);
+
bool canGoForward() const;
bool canGoBack() const;
bool canReopen() const;
@@ -109,6 +129,9 @@ public:
void updateEditorHistory(IEditor *editor);
static void updateEditorHistory(IEditor *editor, QList<EditLocation> &history);
+ void saveTabState(QDataStream *stream) const;
+ void restoreTabState(QDataStream *stream);
+
signals:
void currentEditorChanged(Core::IEditor *editor);
@@ -128,6 +151,8 @@ private:
void splitNewWindow();
void closeSplit();
void openDroppedFiles(const QList<Utils::DropSupport::FileSpec> &files);
+ int tabForEntry(DocumentModel::Entry *entry) const;
+ void activateTab(int index);
void setParentSplitterOrView(SplitterOrView *splitterOrView);
@@ -142,6 +167,8 @@ private:
SplitterOrView *m_parentSplitterOrView;
EditorToolBar *m_toolBar;
+ QTabBar *m_tabBar;
+ bool m_isShowingTabs = false;
QStackedWidget *m_container;
Utils::InfoBarDisplay *m_infoBarDisplay;
diff --git a/src/plugins/coreplugin/editortoolbar.cpp b/src/plugins/coreplugin/editortoolbar.cpp
index 6b4147c8f2f..add4180fe75 100644
--- a/src/plugins/coreplugin/editortoolbar.cpp
+++ b/src/plugins/coreplugin/editortoolbar.cpp
@@ -375,6 +375,12 @@ void EditorToolBar::setGoForwardMenu(QMenu *menu)
d->m_forwardButton->setMenu(menu);
}
+void EditorToolBar::setDocumentDropdownVisible(bool visible)
+{
+ d->m_editorList->setVisible(visible);
+ d->m_closeEditorButton->setVisible(visible);
+}
+
void EditorToolBar::updateActionShortcuts()
{
d->m_closeEditorButton->setToolTip(ActionManager::command(Constants::CLOSE)->stringWithAppendedShortcut(Tr::tr("Close Document")));
diff --git a/src/plugins/coreplugin/editortoolbar.h b/src/plugins/coreplugin/editortoolbar.h
index 72925c0fd63..1c6d63561c9 100644
--- a/src/plugins/coreplugin/editortoolbar.h
+++ b/src/plugins/coreplugin/editortoolbar.h
@@ -61,6 +61,7 @@ public:
void setCanGoForward(bool canGoForward);
void setGoBackMenu(QMenu *menu);
void setGoForwardMenu(QMenu *menu);
+ void setDocumentDropdownVisible(bool visible);
void removeToolbarForEditor(IEditor *editor);
void setCloseSplitEnabled(bool enable);
void setCloseSplitIcon(const QIcon &icon);
diff --git a/src/plugins/coreplugin/generalsettings.cpp b/src/plugins/coreplugin/generalsettings.cpp
index 9be6124bc13..a14491690a2 100644
--- a/src/plugins/coreplugin/generalsettings.cpp
+++ b/src/plugins/coreplugin/generalsettings.cpp
@@ -1,10 +1,11 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
-#include "dialogs/ioptionspage.h"
#include "generalsettings.h"
#include "coreconstants.h"
#include "coreplugintr.h"
+#include "dialogs/ioptionspage.h"
+#include "editormanager/editormanager_p.h"
#include "icore.h"
#include "themechooser.h"
@@ -77,6 +78,13 @@ GeneralSettings::GeneralSettings()
preferInfoBarOverPopup.setDefaultValue(false);
preferInfoBarOverPopup.setLabelText(Tr::tr("Prefer banner style info bars over pop-ups"));
+ useTabsInEditorViews.setSettingsKey("General/UseTabsInEditorViews");
+ useTabsInEditorViews.setDefaultValue(false);
+ useTabsInEditorViews.setLabelText(Tr::tr("Use tabbed editors"));
+ useTabsInEditorViews.addOnChanged(EditorManagerPrivate::instance(), [this] {
+ EditorManagerPrivate::setShowingTabs(useTabsInEditorViews.value());
+ });
+
readSettings();
}
@@ -190,6 +198,7 @@ GeneralSettingsWidget::GeneralSettingsWidget()
form.addRow({empty, generalSettings().showShortcutsInContextMenus});
form.addRow({empty, generalSettings().provideSplitterCursors});
form.addRow({empty, generalSettings().preferInfoBarOverPopup});
+ form.addRow({empty, generalSettings().useTabsInEditorViews});
form.addRow({Row{m_resetWarningsButton, st}});
Column{Group{title(Tr::tr("User Interface")), form}}.attachTo(this);
diff --git a/src/plugins/coreplugin/generalsettings.h b/src/plugins/coreplugin/generalsettings.h
index 45d0400df0c..1bc74ebe697 100644
--- a/src/plugins/coreplugin/generalsettings.h
+++ b/src/plugins/coreplugin/generalsettings.h
@@ -15,6 +15,7 @@ public:
Utils::BoolAspect showShortcutsInContextMenus{this};
Utils::BoolAspect provideSplitterCursors{this};
Utils::BoolAspect preferInfoBarOverPopup{this};
+ Utils::BoolAspect useTabsInEditorViews{this};
static void applyToolbarStyleFromSettings();
};