From 70182ef2b0cffc66a82a5420f53235b48cd1090c Mon Sep 17 00:00:00 2001 From: Alexander Barthel Date: Sat, 27 Jul 2019 15:37:50 +0200 Subject: [PATCH] Added tab widget handler to manage move and closeable tabs. albar965/littlenavmap#437 --- atools.pro | 2 + src/gui/tabwidgethandler.cpp | 475 +++++++++++++++++++++++++++++++++++ src/gui/tabwidgethandler.h | 171 +++++++++++++ 3 files changed, 648 insertions(+) create mode 100644 src/gui/tabwidgethandler.cpp create mode 100644 src/gui/tabwidgethandler.h diff --git a/atools.pro b/atools.pro index a7ecc6b7..fb03d91c 100644 --- a/atools.pro +++ b/atools.pro @@ -185,6 +185,7 @@ HEADERS += \ src/gui/mapposhistory.h \ src/gui/palettesettings.h \ src/gui/signalblocker.h \ + src/gui/tabwidgethandler.h \ src/gui/tools.h \ src/gui/translator.h \ src/gui/widgetstate.h \ @@ -288,6 +289,7 @@ SOURCES += \ src/gui/mapposhistory.cpp \ src/gui/palettesettings.cpp \ src/gui/signalblocker.cpp \ + src/gui/tabwidgethandler.cpp \ src/gui/tools.cpp \ src/gui/translator.cpp \ src/gui/widgetstate.cpp \ diff --git a/src/gui/tabwidgethandler.cpp b/src/gui/tabwidgethandler.cpp new file mode 100644 index 00000000..e3273820 --- /dev/null +++ b/src/gui/tabwidgethandler.cpp @@ -0,0 +1,475 @@ +/***************************************************************************** +* Copyright 2015-2019 Alexander Barthel alex@littlenavmap.org +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*****************************************************************************/ + +#include "gui/tabwidgethandler.h" + +#include "settings/settings.h" + +#include +#include +#include +#include +#include +#include +#include + +const static char ID_PROPERTY[] = "tabid"; + +namespace atools { +namespace gui { + +TabWidgetHandler::TabWidgetHandler(QTabWidget *tabWidgetParam, const QIcon& icon, const QString& toolButtonTooltip) + : QObject(tabWidgetParam), tabWidget(tabWidgetParam) +{ + connect(tabWidget, &QTabWidget::tabCloseRequested, this, &TabWidgetHandler::tabCloseRequested); + connect(tabWidget, &QTabWidget::currentChanged, this, &TabWidgetHandler::currentChanged); + + // Create tool button ================================================= + toolButtonCorner = new QToolButton(tabWidget); + toolButtonCorner->setIcon(icon); + toolButtonCorner->setToolTip(toolButtonTooltip); + toolButtonCorner->setStatusTip(toolButtonTooltip); + toolButtonCorner->setPopupMode(QToolButton::InstantPopup); + toolButtonCorner->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + tabWidget->setCornerWidget(toolButtonCorner); + + // Create and add select all action ===================================== + actionAll = new QAction(tr("&Open All"), toolButtonCorner); + actionAll->setToolTip(tr("Show all tabs")); + actionAll->setStatusTip(actionAll->toolTip()); + toolButtonCorner->addAction(actionAll); + connect(actionAll, &QAction::triggered, this, &TabWidgetHandler::toolbarActionTriggered); + + // Create and add select none action ===================================== + actionNone = new QAction(tr("&Close All Except Current"), toolButtonCorner); + actionNone->setToolTip(tr("Close all tabs except the current tab")); + actionNone->setStatusTip(actionNone->toolTip()); + toolButtonCorner->addAction(actionNone); + connect(actionNone, &QAction::triggered, this, &TabWidgetHandler::toolbarActionTriggered); + + // Create and add select all action ===================================== + actionReset = new QAction(tr("&Reset View"), toolButtonCorner); + actionReset->setToolTip(tr("Show all tabs and reset order back to default")); + actionReset->setStatusTip(actionReset->toolTip()); + toolButtonCorner->addAction(actionReset); + connect(actionReset, &QAction::triggered, this, &TabWidgetHandler::toolbarActionTriggered); + + // Enable and connect context menu + tabWidget->tabBar()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(tabWidget->tabBar(), &QTabBar::customContextMenuRequested, this, &TabWidgetHandler::tableContextMenu); + + // Enable close on double click + connect(tabWidget, &QTabWidget::tabBarDoubleClicked, this, &TabWidgetHandler::tabCloseRequested); +} + +TabWidgetHandler::~TabWidgetHandler() +{ + clear(); + delete actionReset; + delete actionNone; + delete actionAll; + delete toolButtonCorner; +} + +void TabWidgetHandler::clear() +{ + for(const Tab& tab : tabs) + { + toolButtonCorner->removeAction(tab.action); + delete tab.action; + } + tabs.clear(); +} + +void TabWidgetHandler::reset() +{ + { + QSignalBlocker blocker(tabWidget); + resetInternal(); + } + updateWidgets(); +} + +void TabWidgetHandler::resetInternal() +{ + clearTabWidget(); + for(const Tab& tab : tabs) + addTab(tab.action->data().toInt()); + + // Activate first + tabWidget->setCurrentIndex(0); +} + +void TabWidgetHandler::tableContextMenu(const QPoint& pos) +{ + QPoint menuPos = QCursor::pos(); + // Move menu position off the cursor to avoid accidental selection on touchpads + menuPos += QPoint(3, 3); + + QMenu menu; + + // Create close this tab action + QAction *closeAction = new QAction(QString(), &menu); + int index = tabWidget->tabBar()->tabAt(pos); + if(index != -1) + // Enabled + closeAction->setText(tr("&Close tab %1").arg(tabWidget->tabText(index).remove('&'))); + else + { + // Not over tab + closeAction->setText(tr("Close tab")); + closeAction->setDisabled(true); + } + closeAction->setToolTip(closeAction->text()); + closeAction->setStatusTip(closeAction->text()); + + menu.addAction(actionAll); + menu.addAction(actionNone); + menu.addAction(actionReset); + menu.addSeparator(); + menu.addAction(closeAction); + menu.addSeparator(); + + for(const Tab& tab : tabs) + menu.addAction(tab.action); + + // Open menu + QAction *action = menu.exec(menuPos); + if(action == closeAction) + tabCloseRequested(index); +} + +void TabWidgetHandler::init(const QVector& tabIdsParam, const QString& settingsPrefixParam) +{ + clear(); + settingsPrefix = settingsPrefixParam; + + Q_ASSERT(tabIdsParam.size() == tabWidget->count()); + + for(int index = 0; index < tabWidget->count(); index++) + { + QWidget *widget = tabWidget->widget(index); + widget->setProperty(ID_PROPERTY, tabIdsParam.at(index)); + + // Get id from array. This is doable because all tabs are expected to be loaded + int id = tabIdsParam.at(index); + + // Get name with mnemonic + QString name = tabWidget->tabText(index); + if(!name.contains("&")) + name = "&" + name; + + // Create and fill action + QAction *action = new QAction(name, toolButtonCorner); + action->setCheckable(true); + action->setChecked(true); + QString tooltip = tr("Open or close tab %1").arg(name).remove('&'); + action->setToolTip(tooltip); + action->setStatusTip(tooltip); + action->setData(id); + toolButtonCorner->addAction(action); + connect(action, &QAction::toggled, this, &TabWidgetHandler::toolbarActionTriggered); + + tabs.append(Tab(widget, tabWidget->tabText(index), tabWidget->tabToolTip(index), action)); + } +} + +void TabWidgetHandler::restoreState() +{ + // A list of tab ids in the same order as contained by the tab widget + QStringList tabList = atools::settings::Settings::instance().valueStrList(settingsPrefix + "TabIds"); + + if(!tabList.isEmpty()) + { + clearTabWidget(); + QSignalBlocker blocker(tabWidget); + + for(const QString& str : tabList) + { + bool ok = false; + int id = str.toInt(&ok); + if(ok) + { + Tab tab = tabs.value(id); + + if(tab.isValid()) + { + // Add tab to widget + int idx = tabWidget->addTab(tab.widget, tab.title); + if(idx != -1) + { + tabWidget->setTabToolTip(idx, tab.tooltip); + tabWidget->setCurrentWidget(tab.widget); + } + } + } + } + } + + // Restore current tab from separate setting + setCurrentTab(atools::settings::Settings::instance().valueInt(settingsPrefix + "CurrentTabId", 0)); + + // Update actions + updateWidgets(); +} + +void TabWidgetHandler::saveState() +{ + // A list of tab ids in the same order as contained by the tab widget + QStringList tabList; + for(int index = 0; index < tabWidget->count(); index++) + tabList.append(QString::number(idForIndex(index))); + + // Save tabs + atools::settings::Settings::instance().setValue(settingsPrefix + "TabIds", tabList); + + // Save current tab + atools::settings::Settings::instance().setValue(settingsPrefix + "CurrentTabId", getCurrentTabId()); +} + +int TabWidgetHandler::getCurrentTabId() const +{ + return tabWidget->currentWidget() == nullptr ? -1 : tabWidget->currentWidget()->property(ID_PROPERTY).toInt(); +} + +void TabWidgetHandler::setCurrentTab(int id, bool left) +{ + int index = indexForId(id); + if(index != -1) + // Already open - push to front + tabWidget->setCurrentIndex(index); + else + { + { + QSignalBlocker blocker(tabWidget); + + // Add and set active + int idx = insertTab(tabWidget->currentIndex() + (left ? 0 : 1), id); + if(idx != -1) + tabWidget->setCurrentIndex(idx); + } + + updateWidgets(); + } +} + +void TabWidgetHandler::openTab(int id, bool left) +{ + int index = indexForId(id); + if(index == -1) + { + { + QSignalBlocker blocker(tabWidget); + insertTab(tabWidget->currentIndex() + (left ? 0 : 1), id); + } + updateWidgets(); + } +} + +bool TabWidgetHandler::isTabVisible(int id) const +{ + return indexForId(id) != -1; +} + +void TabWidgetHandler::currentChanged() +{ + qDebug() << Q_FUNC_INFO; + + emit tabChanged(getCurrentTabId()); +} + +void TabWidgetHandler::tabCloseRequested(int index) +{ + qDebug() << Q_FUNC_INFO; + + // int height = tabWidget->cornerWidget()->height(); + int id = idForIndex(index); + tabWidget->removeTab(index); + // tabWidget->cornerWidget()->setMinimumHeight(height); + + // Update action but disable signals to avoid recursion + QAction *action = tabs.at(index).action; + QSignalBlocker actionBlocker(action); + action->setChecked(true); + + updateWidgets(); + emit tabClosed(id); +} + +void TabWidgetHandler::toolbarActionTriggered() +{ + qDebug() << Q_FUNC_INFO; + + QAction *sendAction = dynamic_cast(sender()); + QWidget *current = tabWidget->currentWidget(); + + { + QSignalBlocker blocker(tabWidget); + + if(sendAction == actionAll) + { + // Add all closed tabs at the end of the list - keep current selected ============================== + QVector missing = missingTabIds(); + + for(int id : missing) + addTab(id); + tabWidget->setCurrentWidget(current); + } + else if(sendAction == actionNone) + { + // Close all tabls except current one ============================================================ + clearTabWidget(); + addTab(idForWidget(current)); + } + else if(sendAction == actionReset) + // Reset open/close state and order and select first ============================================ + resetInternal(); + else if(sendAction != nullptr) + { + // Open/close individual tabs ============================================================ + int actionId = sendAction->data().toInt(); + + if(sendAction->isChecked()) + { + // Insert and activate + int idx = insertTab(tabWidget->currentIndex() + 1, actionId); + if(idx != -1) + tabWidget->setCurrentIndex(idx); + } + else + { + // Remove + int index = indexForId(actionId); + if(index != -1) + tabWidget->removeTab(index); + } + } + } + + updateWidgets(); +} + +int TabWidgetHandler::insertTab(int index, int id) +{ + // Re-enables the close button + fixSingleTab(); + + // Add at index + const Tab& tab = tabs.at(id); + int idx = tabWidget->insertTab(index, tab.widget, tab.title); + tabWidget->setTabToolTip(idx, tab.tooltip); + tabWidget->setCurrentWidget(tab.widget); + return idx; +} + +int TabWidgetHandler::addTab(int id) +{ + // Re-enables the close button + fixSingleTab(); + + // Append to list + const Tab& tab = tabs.at(id); + int idx = tabWidget->addTab(tab.widget, tab.title); + tabWidget->setTabToolTip(idx, tab.tooltip); + return idx; +} + +void TabWidgetHandler::fixSingleTab() +{ + if(tabWidget->count() == 1) + { + QWidget *tab = tabWidget->widget(0); + QString title = tabWidget->tabText(0); + QString tooltip = tabWidget->tabToolTip(0); + tabWidget->removeTab(0); + + int idx = tabWidget->addTab(tab, title); + tabWidget->setTabToolTip(idx, tooltip); + } +} + +void TabWidgetHandler::clearTabWidget() +{ + // int height = tabWidget->cornerWidget()->height(); + tabWidget->blockSignals(true); + tabWidget->clear(); + tabWidget->blockSignals(false); + // tabWidget->cornerWidget()->setMinimumHeight(height); +} + +QVector TabWidgetHandler::missingTabIds() const +{ + QVector retval; + + for(int id = 0; id < tabs.size(); id++) + { + QWidget *widget = tabs.at(id).widget; + int index = tabWidget->indexOf(widget); + if(index == -1) + retval.append(id); + } + + return retval; +} + +int TabWidgetHandler::idForIndex(int index) const +{ + QWidget *widget = tabWidget->widget(index); + return widget != nullptr ? widget->property(ID_PROPERTY).toInt() : -1; +} + +int TabWidgetHandler::idForWidget(QWidget *widget) const +{ + return widget->property(ID_PROPERTY).toInt(); +} + +int TabWidgetHandler::indexForId(int id) const +{ + return tabWidget->indexOf(tabs.at(id).widget); +} + +void TabWidgetHandler::updateWidgets() +{ + for(const Tab& tab : tabs) + { + QSignalBlocker actionBlocker(tab.action); + tab.action->setChecked(false); + tab.action->setDisabled(false); + } + + if(tabWidget->count() == 1) + { + tabWidget->tabBar()->setTabButton(0, QTabBar::RightSide, new QLabel(tabWidget)); + + QAction *action = tabs.at(tabWidget->currentWidget()->property(ID_PROPERTY).toInt()).action; + QSignalBlocker actionBlocker(action); + action->setChecked(true); + action->setDisabled(true); + } + else + { + for(int index = 0; index < tabWidget->count(); index++) + { + QAction *action = tabs.at(idForIndex(index)).action; + QSignalBlocker actionBlocker(action); + action->setChecked(true); + } + } +} + +} // namespace gui +} // namespace atools diff --git a/src/gui/tabwidgethandler.h b/src/gui/tabwidgethandler.h new file mode 100644 index 00000000..7f9b90a2 --- /dev/null +++ b/src/gui/tabwidgethandler.h @@ -0,0 +1,171 @@ +/***************************************************************************** +* Copyright 2015-2019 Alexander Barthel alex@littlenavmap.org +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*****************************************************************************/ + +#ifndef ATOOLS_TABWIDGETHANDLER_H +#define ATOOLS_TABWIDGETHANDLER_H + +#include +#include + +class QTabWidget; +class QToolButton; +class QAction; + +namespace atools { +namespace gui { + +struct TabData +{ + QWidget *widget; + int id; +}; + +/* + * Enhances tab widgets with a right corner widget and a context menu above the tabs that allow to enable and + * disable tabs. State of open/closed and reordered tabs can be saved and loaded to a settings file. + * + * This handler uses a concept of ids instead of indexes. Create an enum starting with 0 with consecutive values for each tab and + * use this for the id space. Actual tab indexes can differ from ids if tabs are closed or re-ordered. + */ +class TabWidgetHandler : + public QObject +{ + Q_OBJECT + +public: + /* Creates corner button and all/none/reset actions */ + explicit TabWidgetHandler(QTabWidget *tabWidgetParam, const QIcon& icon, const QString& toolButtonTooltip); + virtual ~TabWidgetHandler() override; + + /* Creates the menu entries for the tool button and the context menu. tabIdsParam contains the ids for + * each tab and has to correspond to the actual tabs contained by the tab widget. + * tabIdsParam.size() has to be equal to tabWidgetParam->count() set in the constructor */ + void init(const QVector& tabIdsParam, const QString& settingsPrefixParam); + + /* Save or restore open/close and order of tabs. Call init before. */ + void saveState(); + void restoreState(); + + /* Id of currently open tab or -1 if none */ + int getCurrentTabId() const; + + /* Set current tab. Tab will be re-opened if was closed. + * left = true: left side of current otherwise to the right.*/ + void setCurrentTab(int id, bool left = false); + + /* Open tab in background if it was closed. left = true: left side of current otherwise to the right. */ + void openTab(int id, bool left = false); + + /* true if tab is not closed */ + bool isTabVisible(int id) const; + + /* Reset tab order to default (as defined in tabIdsParam of method init. Closed tabs are reopened and the first tab + * is set to current. */ + void reset(); + +signals: + /* Current tab has changed */ + void tabChanged(int id); + + /* A tab was closed */ + void tabClosed(int id); + +private: + /* Removes actions from menu button before running init */ + void clear(); + + void resetInternal(); + + /* Blocks signals and removes all tabs from widget */ + void clearTabWidget(); + + /* Signal from tab widget */ + void currentChanged(); + + /* Signal from tab widget - Replaces close button with an empty dummy label if only one tab is left. */ + void tabCloseRequested(int index); + + /* Any of the none, all, reset or other actions were triggered or toggled */ + void toolbarActionTriggered(); + + /* Update widget/action state based on active tabs in the widget - also replaces close button on last tab */ + void updateWidgets(); + + /* Get index in tab widget for id or -1 if tab is not contained by widget */ + int indexForId(int id) const; + + /* Get id for tab or -1 if index is not valid */ + int idForIndex(int index) const; + + /* Get id from property widget */ + int idForWidget(QWidget *widget) const; + + /* Get a list of tabs that are not part of the widget - i.e. currently closed */ + QVector missingTabIds() const; + + /* Add tab at the end or at index */ + int addTab(int id); + int insertTab(int index, int id); + + /* Context menu for tab bar */ + void tableContextMenu(const QPoint& pos); + + /* Re-enables the close button after the widget had ony one tab */ + void fixSingleTab(); + + /* Contains all information for tabs */ + struct Tab + { + Tab() + : widget(nullptr) + { + } + + Tab(QWidget *tabParam, const QString& titleParam, const QString& tooltipParam, QAction *actionParam) + : widget(tabParam), title(titleParam), tooltip(tooltipParam), action(actionParam) + { + } + + /* true if initialized and not default constructed */ + bool isValid() const + { + return widget != nullptr; + } + + QWidget *widget; /* The tab widget. Contains id as property ID_PROPERTY */ + QString title, tooltip; /* Saved texts needed when adding tab */ + QAction *action; /* Action for tool button or menu. Has id in "data" field. */ + }; + + /* Contains all (also closed) tabs. Index corresponds to tab id as given in tabIdsParam */ + QVector tabs; + + /* Various action to restore, close or reset all tabs */ + QAction *actionAll = nullptr, *actionNone = nullptr, *actionReset = nullptr; + + QTabWidget *tabWidget; + QToolButton *toolButtonCorner = nullptr; + + /* Prefix used when saving settings */ + QString settingsPrefix; + +}; + +} // namespace gui +} // namespace atools + +#endif // ATOOLS_TABWIDGETHANDLER_H