From a4265e7fffb756f35bc60b777bc06bee3153389d Mon Sep 17 00:00:00 2001 From: glubsy Date: Sun, 12 Jul 2020 16:58:01 +0200 Subject: [PATCH 1/7] Use tabs instead of floating windows * Directories dialog, Results window and ignore list dialog are the three dialog windows which can now be tabbed instead of previously floating. * Menus are automatically updated depending on the type of dialog as the current tab. Menu items which do not apply to the currently displayed tab are disabled but not hidden. * The floating windows logic is preserved in case we want to use them again later (I don't see why though) * There are two different versions of the tab bar: the default one used in TabBarWindow class places the tabs next to the top menu to save screen real estate. The other option is to use TabWindow which uses a regular QTabWidget where the tab bar is placed right on top of the displayed window. * There is a toggle option in the View menu to hide the tabs, the windows can still be navigated to with the View menu items. --- qt/app.py | 84 +++++++-- qt/directories_dialog.py | 44 +++-- qt/ignore_list_dialog.py | 2 + qt/preferences.py | 9 + qt/result_window.py | 66 +++++--- qt/tabbed_window.py | 356 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 509 insertions(+), 52 deletions(-) create mode 100644 qt/tabbed_window.py diff --git a/qt/app.py b/qt/app.py index ac59dd0a..6ed0df31 100644 --- a/qt/app.py +++ b/qt/app.py @@ -35,6 +35,7 @@ from .se.preferences_dialog import PreferencesDialog as PreferencesDialogStandar from .me.preferences_dialog import PreferencesDialog as PreferencesDialogMusic from .pe.preferences_dialog import PreferencesDialog as PreferencesDialogPicture from .pe.photo import File as PlatSpecificPhoto +from .tabbed_window import TabBarWindow tr = trget("ui") @@ -47,6 +48,9 @@ class DupeGuru(QObject): super().__init__(**kwargs) self.prefs = Preferences() self.prefs.load() + # Enable tabs instead of separate floating windows for each dialog + # Could be passed as an argument to this class if we wanted + self.use_tabs = True self.model = DupeGuruModel(view=self) self._setup() @@ -59,22 +63,41 @@ class DupeGuru(QObject): self.recentResults.mustOpenItem.connect(self.model.load_from) self.resultWindow = None self.details_dialog = None - self.directories_dialog = DirectoriesDialog(self) + if self.use_tabs: + self.main_window = TabBarWindow(self) + parent_window = self.main_window + self.directories_dialog = self.main_window.createPage("DirectoriesDialog", app=self) + self.main_window.addTab( + self.directories_dialog, "Directories", switch=False) + else: # floating windows only + self.main_window = None + self.directories_dialog = DirectoriesDialog(self) + parent_window = self.directories_dialog + self.progress_window = ProgressWindow( - self.directories_dialog, self.model.progress_window + parent_window, self.model.progress_window ) self.problemDialog = ProblemDialog( - parent=self.directories_dialog, model=self.model.problem_dialog + parent=parent_window, model=self.model.problem_dialog ) - self.ignoreListDialog = IgnoreListDialog( - parent=self.directories_dialog, model=self.model.ignore_list_dialog - ) - self.deletionOptions = DeletionOptions( - parent=self.directories_dialog, model=self.model.deletion_options - ) - self.about_box = AboutBox(self.directories_dialog, self) + if self.main_window: # we use tab widget + self.ignoreListDialog = self.main_window.createPage( + "IgnoreListDialog", + parent=self.main_window, + model=self.model.ignore_list_dialog) + self.ignoreListDialog.accepted.connect(self.main_window.onDialogAccepted) + else: + self.ignoreListDialog = IgnoreListDialog( + parent=parent_window, model=self.model.ignore_list_dialog + ) - self.directories_dialog.show() + self.deletionOptions = DeletionOptions( + parent=parent_window, + model=self.model.deletion_options + ) + self.about_box = AboutBox(parent_window, self) + + parent_window.show() self.model.load() self.SIGTERM.connect(self.handleSIGTERM) @@ -191,7 +214,11 @@ class DupeGuru(QObject): def showResultsWindow(self): if self.resultWindow is not None: - self.resultWindow.show() + if self.main_window: + self.main_window.addTab( + self.resultWindow, "Results", switch=True) + else: + self.resultWindow.show() def shutdown(self): self.willSavePrefs.emit() @@ -212,7 +239,9 @@ class DupeGuru(QObject): "scanning have accented letters, you'll probably get a crash. It is advised that " "you set your system locale properly." ) - QMessageBox.warning(self.directories_dialog, "Wrong Locale", msg) + QMessageBox.warning(self.main_window if self.main_window + else self.directories_dialog, + "Wrong Locale", msg) def clearPictureCacheTriggered(self): title = tr("Clear Picture Cache") @@ -223,7 +252,19 @@ class DupeGuru(QObject): QMessageBox.information(active, title, tr("Picture cache cleared.")) def ignoreListTriggered(self): - self.model.ignore_list_dialog.show() + if self.main_window: + # Fetch the index in the TabWidget or the StackWidget (depends on class): + index = self.main_window.indexOfWidget(self.ignoreListDialog) + if index < 0: + # we have not instantiated and populated it in their internal list yet + index = self.main_window.addTab( + self.ignoreListDialog, "Ignore List", switch=True) + # if not self.main_window.tabWidget.isTabVisible(index): + self.main_window.setTabVisible(index, True) + self.main_window.setCurrentIndex(index) + return + else: + self.model.ignore_list_dialog.show() def openDebugLogTriggered(self): debugLogPath = op.join(self.model.appdata, "debug.log") @@ -231,7 +272,8 @@ class DupeGuru(QObject): def preferencesTriggered(self): preferences_dialog = self._get_preferences_dialog_class()( - self.directories_dialog, self + self.main_window if self.main_window else self.directories_dialog, + self ) preferences_dialog.load() result = preferences_dialog.exec() @@ -242,7 +284,10 @@ class DupeGuru(QObject): preferences_dialog.setParent(None) def quitTriggered(self): - self.directories_dialog.close() + if self.main_window: + self.main_window.close() + else: + self.directories_dialog.close() def showAboutBoxTriggered(self): self.about_box.show() @@ -282,7 +327,12 @@ class DupeGuru(QObject): if self.resultWindow is not None: self.resultWindow.close() self.resultWindow.setParent(None) - self.resultWindow = ResultWindow(self.directories_dialog, self) + if self.main_window: + self.resultWindow = self.main_window.createPage( + "ResultWindow", parent=self.main_window, app=self) + else: # We don't use a tab widget, regular floating QMainWindow + self.resultWindow = ResultWindow(self.directories_dialog, self) + self.directories_dialog._updateActionsState() self.details_dialog = self._get_details_dialog_class()(self.resultWindow, self) def show_results_window(self): diff --git a/qt/directories_dialog.py b/qt/directories_dialog.py index 5b0c1ea4..97c18b18 100644 --- a/qt/directories_dialog.py +++ b/qt/directories_dialog.py @@ -40,6 +40,7 @@ class DirectoriesDialog(QMainWindow): def __init__(self, app, **kwargs): super().__init__(None, **kwargs) self.app = app + self.specific_actions = set() self.lastAddedFolder = platform.INITIAL_FOLDER_IN_DIALOGS self.recentFolders = Recent(self.app, "recentFolders") self._setupUi() @@ -94,21 +95,36 @@ class DirectoriesDialog(QMainWindow): ] createActions(ACTIONS, self) + if self.app.main_window: # We use tab widgets in this case + # Keep track of actions which should only be accessible from this class + for action, _, _, _, _ in ACTIONS: + self.specific_actions.add(getattr(self, action)) + def _setupMenu(self): - self.menubar = QMenuBar(self) - self.menubar.setGeometry(QRect(0, 0, 42, 22)) - self.menuFile = QMenu(self.menubar) - self.menuFile.setTitle(tr("File")) - self.menuView = QMenu(self.menubar) - self.menuView.setTitle(tr("View")) - self.menuHelp = QMenu(self.menubar) - self.menuHelp.setTitle(tr("Help")) + if not self.app.main_window: + # we are our own QMainWindow, we need our own menu bar + self.menubar = QMenuBar(self) + self.menubar.setGeometry(QRect(0, 0, 42, 22)) + self.menuFile = QMenu(self.menubar) + self.menuFile.setTitle(tr("File")) + self.menuView = QMenu(self.menubar) + self.menuView.setTitle(tr("View")) + self.menuHelp = QMenu(self.menubar) + self.menuHelp.setTitle(tr("Help")) + self.setMenuBar(self.menubar) + menubar = self.menubar + else: + # we are part of a tab widget, we populate its window's menubar instead + self.menuFile = self.app.main_window.menuFile + self.menuView = self.app.main_window.menuView + self.menuHelp = self.app.main_window.menuHelp + menubar = self.app.main_window.menubar + self.menuLoadRecent = QMenu(self.menuFile) self.menuLoadRecent.setTitle(tr("Load Recent Results")) - self.setMenuBar(self.menubar) - self.menuFile.addAction(self.actionLoadResults) self.menuFile.addAction(self.menuLoadRecent.menuAction()) + self.specific_actions.add(self.menuLoadRecent.menuAction()) self.menuFile.addSeparator() self.menuFile.addAction(self.app.actionClearPictureCache) self.menuFile.addSeparator() @@ -120,9 +136,9 @@ class DirectoriesDialog(QMainWindow): self.menuHelp.addAction(self.app.actionOpenDebugLog) self.menuHelp.addAction(self.app.actionAbout) - self.menubar.addAction(self.menuFile.menuAction()) - self.menubar.addAction(self.menuView.menuAction()) - self.menubar.addAction(self.menuHelp.menuAction()) + menubar.addAction(self.menuFile.menuAction()) + menubar.addAction(self.menuView.menuAction()) + menubar.addAction(self.menuHelp.menuAction()) # Recent folders menu self.menuRecentFolders = QMenu() @@ -139,6 +155,8 @@ class DirectoriesDialog(QMainWindow): self.resize(420, 338) self.centralwidget = QWidget(self) self.verticalLayout = QVBoxLayout(self.centralwidget) + self.verticalLayout.setContentsMargins(4, 0, 4, 0) + self.verticalLayout.setSpacing(0) hl = QHBoxLayout() label = QLabel(tr("Application Mode:"), self) label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) diff --git a/qt/ignore_list_dialog.py b/qt/ignore_list_dialog.py index 99d2efe7..5643cc96 100644 --- a/qt/ignore_list_dialog.py +++ b/qt/ignore_list_dialog.py @@ -26,6 +26,7 @@ class IgnoreListDialog(QDialog): def __init__(self, parent, model, **kwargs): flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint super().__init__(parent, flags, **kwargs) + self.specific_actions = frozenset() self._setupUi() self.model = model self.model.view = self @@ -39,6 +40,7 @@ class IgnoreListDialog(QDialog): self.setWindowTitle(tr("Ignore List")) self.resize(540, 330) self.verticalLayout = QVBoxLayout(self) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.tableView = QTableView() self.tableView.setEditTriggers(QAbstractItemView.NoEditTriggers) self.tableView.setSelectionMode(QAbstractItemView.ExtendedSelection) diff --git a/qt/preferences.py b/qt/preferences.py index c9691cca..009bab13 100644 --- a/qt/preferences.py +++ b/qt/preferences.py @@ -35,9 +35,14 @@ class Preferences(PreferencesBase): "ResultWindowIsMaximized", self.resultWindowIsMaximized ) self.resultWindowRect = self.get_rect("ResultWindowRect", self.resultWindowRect) + self.mainWindowIsMaximized = get( + "MainWindowIsMaximized", self.mainWindowIsMaximized + ) + self.mainWindowRect = self.get_rect("MainWindowRect", self.mainWindowRect) self.directoriesWindowRect = self.get_rect( "DirectoriesWindowRect", self.directoriesWindowRect ) + self.recentResults = get("RecentResults", self.recentResults) self.recentFolders = get("RecentFolders", self.recentFolders) @@ -70,6 +75,8 @@ class Preferences(PreferencesBase): self.resultWindowIsMaximized = False self.resultWindowRect = None self.directoriesWindowRect = None + self.mainWindowRect = None + self.mainWindowIsMaximized = False self.recentResults = [] self.recentFolders = [] @@ -101,7 +108,9 @@ class Preferences(PreferencesBase): set_("TableFontSize", self.tableFontSize) set_('ReferenceBoldFont', self.reference_bold_font) set_("ResultWindowIsMaximized", self.resultWindowIsMaximized) + set_("MainWindowIsMaximized", self.mainWindowIsMaximized) self.set_rect("ResultWindowRect", self.resultWindowRect) + self.set_rect("MainWindowRect", self.mainWindowRect) self.set_rect("DirectoriesWindowRect", self.directoriesWindowRect) set_("RecentResults", self.recentResults) set_("RecentFolders", self.recentFolders) diff --git a/qt/result_window.py b/qt/result_window.py index 764a6273..5656fbeb 100644 --- a/qt/result_window.py +++ b/qt/result_window.py @@ -42,6 +42,7 @@ class ResultWindow(QMainWindow): def __init__(self, parent, app, **kwargs): super().__init__(parent, **kwargs) self.app = app + self.specific_actions = set() self._setupUi() if app.model.app_mode == AppMode.Picture: MODEL_CLASS = ResultsModelPicture @@ -207,22 +208,39 @@ class ResultWindow(QMainWindow): self.actionDelta.setCheckable(True) self.actionPowerMarker.setCheckable(True) + if self.app.main_window: # We use tab widgets in this case + # Keep track of actions which should only be accessible from this class + for action, _, _, _, _ in ACTIONS: + self.specific_actions.add(getattr(self, action)) + def _setupMenu(self): - self.menubar = QMenuBar() - self.menubar.setGeometry(QRect(0, 0, 630, 22)) - self.menuFile = QMenu(self.menubar) - self.menuFile.setTitle(tr("File")) - self.menuMark = QMenu(self.menubar) - self.menuMark.setTitle(tr("Mark")) - self.menuActions = QMenu(self.menubar) - self.menuActions.setTitle(tr("Actions")) - self.menuColumns = QMenu(self.menubar) - self.menuColumns.setTitle(tr("Columns")) - self.menuView = QMenu(self.menubar) - self.menuView.setTitle(tr("View")) - self.menuHelp = QMenu(self.menubar) - self.menuHelp.setTitle(tr("Help")) - self.setMenuBar(self.menubar) + if not self.app.main_window: + # we are our own QMainWindow, we need our own menu bar + self.menubar = QMenuBar() # self.menuBar() works as well here + self.menubar.setGeometry(QRect(0, 0, 630, 22)) + self.menuFile = QMenu(self.menubar) + self.menuFile.setTitle(tr("File")) + self.menuMark = QMenu(self.menubar) + self.menuMark.setTitle(tr("Mark")) + self.menuActions = QMenu(self.menubar) + self.menuActions.setTitle(tr("Actions")) + self.menuColumns = QMenu(self.menubar) + self.menuColumns.setTitle(tr("Columns")) + self.menuView = QMenu(self.menubar) + self.menuView.setTitle(tr("View")) + self.menuHelp = QMenu(self.menubar) + self.menuHelp.setTitle(tr("Help")) + self.setMenuBar(self.menubar) + menubar = self.menubar + else: + # we are part of a tab widget, we populate its window's menubar instead + self.menuFile = self.app.main_window.menuFile + self.menuMark = self.app.main_window.menuMark + self.menuActions = self.app.main_window.menuActions + self.menuColumns = self.app.main_window.menuColumns + self.menuView = self.app.main_window.menuView + self.menuHelp = self.app.main_window.menuHelp + menubar = self.app.main_window.menubar self.menuActions.addAction(self.actionDeleteMarked) self.menuActions.addAction(self.actionMoveMarked) @@ -257,15 +275,19 @@ class ResultWindow(QMainWindow): self.menuFile.addSeparator() self.menuFile.addAction(self.app.actionQuit) - self.menubar.addAction(self.menuFile.menuAction()) - self.menubar.addAction(self.menuMark.menuAction()) - self.menubar.addAction(self.menuActions.menuAction()) - self.menubar.addAction(self.menuColumns.menuAction()) - self.menubar.addAction(self.menuView.menuAction()) - self.menubar.addAction(self.menuHelp.menuAction()) + menubar.addAction(self.menuFile.menuAction()) + menubar.addAction(self.menuMark.menuAction()) + menubar.addAction(self.menuActions.menuAction()) + menubar.addAction(self.menuColumns.menuAction()) + menubar.addAction(self.menuView.menuAction()) + menubar.addAction(self.menuHelp.menuAction()) # Columns menu menu = self.menuColumns + # Avoid adding duplicate actions in tab widget menu in case we recreated + # the Result Window instance. + if menu.actions(): + menu.clear() self._column_actions = [] for index, (display, visible) in enumerate( self.app.model.result_table.columns.menu_items() @@ -280,7 +302,7 @@ class ResultWindow(QMainWindow): action.item_index = -1 # Action menu - actionMenu = QMenu(tr("Actions"), self.menubar) + actionMenu = QMenu(tr("Actions"), menubar) actionMenu.addAction(self.actionDeleteMarked) actionMenu.addAction(self.actionMoveMarked) actionMenu.addAction(self.actionCopyMarked) diff --git a/qt/tabbed_window.py b/qt/tabbed_window.py new file mode 100644 index 00000000..c8a06fd3 --- /dev/null +++ b/qt/tabbed_window.py @@ -0,0 +1,356 @@ +# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.gnu.org/licenses/gpl-3.0.html + +from PyQt5.QtCore import QRect, pyqtSlot, Qt +from PyQt5.QtWidgets import ( + QWidget, + QVBoxLayout, + QHBoxLayout, + QMainWindow, + QTabWidget, + QMenu, + QTabBar, + QStackedWidget, +) +from hscommon.trans import trget +from qtlib.util import moveToScreenCenter, createActions +from .directories_dialog import DirectoriesDialog +from .result_window import ResultWindow +from .ignore_list_dialog import IgnoreListDialog +tr = trget("ui") + + +class TabWindow(QMainWindow): + def __init__(self, app, **kwargs): + super().__init__(None, **kwargs) + self.app = app + self.pages = {} + self.menubar = None + self.menuList = set() + self.last_index = -1 + self.previous_widget_actions = set() + self._setupUi() + self.app.willSavePrefs.connect(self.appWillSavePrefs) + + def _setupActions(self): + # (name, shortcut, icon, desc, func) + ACTIONS = [ + ( + "actionToggleTabs", + "", + "", + tr("Show tab bar"), + self.toggleTabBar, + ), + ] + createActions(ACTIONS, self) + self.actionToggleTabs.setCheckable(True) + self.actionToggleTabs.setChecked(True) + + def _setupUi(self): + self.setWindowTitle(self.app.NAME) + self.resize(640, 480) + self.tabWidget = QTabWidget() + # self.tabWidget.setTabPosition(QTabWidget.South) + self.tabWidget.setContentsMargins(0, 0, 0, 0) + # self.tabWidget.setTabBarAutoHide(True) + # This gets rid of the annoying margin around the TabWidget: + self.tabWidget.setDocumentMode(True) + + self._setupActions() + self._setupMenu() + # This should be the same as self.centralWidget.setLayout(self.verticalLayout) + self.verticalLayout = QVBoxLayout(self.tabWidget) + # self.verticalLayout.addWidget(self.tabWidget) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.tabWidget.setTabsClosable(True) + self.setCentralWidget(self.tabWidget) # only for QMainWindow + + self.tabWidget.currentChanged.connect(self.updateMenuBar) + self.tabWidget.tabCloseRequested.connect(self.onTabCloseRequested) + self.updateMenuBar(self.tabWidget.currentIndex()) + self.restoreGeometry() + + def restoreGeometry(self): + if self.app.prefs.mainWindowRect is not None: + self.setGeometry(self.app.prefs.mainWindowRect) + else: + moveToScreenCenter(self) + + def _setupMenu(self): + """Setup the menubar boiler plates which will be filled by the underlying + tab's widgets whenever they are instantiated.""" + self.menubar = self.menuBar() # QMainWindow, similar to just QMenuBar() here + # self.setMenuBar(self.menubar) # already set if QMainWindow class + self.menubar.setGeometry(QRect(0, 0, 100, 22)) + self.menuFile = QMenu(self.menubar) + self.menuFile.setTitle(tr("File")) + self.menuMark = QMenu(self.menubar) + self.menuMark.setTitle(tr("Mark")) + self.menuActions = QMenu(self.menubar) + self.menuActions.setTitle(tr("Actions")) + self.menuColumns = QMenu(self.menubar) + self.menuColumns.setTitle(tr("Columns")) + self.menuView = QMenu(self.menubar) + self.menuView.setTitle(tr("View")) + self.menuHelp = QMenu(self.menubar) + self.menuHelp.setTitle(tr("Help")) + + self.menuView.addAction(self.actionToggleTabs) + self.menuView.addSeparator() + + self.menuList.add(self.menuFile) + self.menuList.add(self.menuMark) + self.menuList.add(self.menuActions) + self.menuList.add(self.menuColumns) + self.menuList.add(self.menuView) + self.menuList.add(self.menuHelp) + + @pyqtSlot(int) + def updateMenuBar(self, page_index=None): + if page_index < 0: + return + current_index = self.getCurrentIndex() + active_widget = self.getWidgetAtIndex(current_index) + if self.last_index < 0: + self.last_index = current_index + self.previous_widget_actions = active_widget.specific_actions + return + isResultWindow = isinstance(active_widget, ResultWindow) + isIgnoreListDialog = isinstance(active_widget, IgnoreListDialog) + for menu in self.menuList: + if menu is self.menuColumns or menu is self.menuActions or menu is self.menuMark: + if not isResultWindow: + menu.setEnabled(False) + continue + else: + menu.setEnabled(True) + for action in menu.actions(): + if action is self.app.directories_dialog.actionShowResultsWindow: + if isResultWindow: + # Action points to ourselves, always disable it + self.app.directories_dialog.actionShowResultsWindow\ + .setEnabled(False) + continue + else: + self.app.directories_dialog.actionShowResultsWindow\ + .setEnabled(self.app.resultWindow is not None) + if isIgnoreListDialog: + self.app.actionIgnoreList.setEnabled(False) + continue + else: + self.app.actionIgnoreList.setEnabled(self.app.ignoreListDialog is not None) + continue + if action not in active_widget.specific_actions: + if action in self.previous_widget_actions: + action.setEnabled(False) + continue + action.setEnabled(True) + + self.previous_widget_actions = active_widget.specific_actions + self.last_index = current_index + + def createPage(self, cls, **kwargs): + app = kwargs.get("app", self.app) + page = None + if cls == "DirectoriesDialog": + page = DirectoriesDialog(app) + elif cls == "ResultWindow": + parent = kwargs.get("parent", self) + page = ResultWindow(parent, app) + elif cls == "IgnoreListDialog": + parent = kwargs.get("parent", self) + model = kwargs.get("model") + page = IgnoreListDialog(parent, model) + self.pages[cls] = page + return page + + def addTab(self, page, title, switch=False): + # Warning: this supposedly takes ownership of the page + index = self.tabWidget.addTab(page, title) + # index = self.tabWidget.insertTab(-1, page, title) + if isinstance(page, DirectoriesDialog): + self.tabWidget.tabBar().setTabButton( + index, QTabBar.RightSide, None) + if switch: + self.setCurrentIndex(index) + return index + + def indexOfWidget(self, widget): + return self.tabWidget.indexOf(widget) + + def setCurrentIndex(self, index): + return self.tabWidget.setCurrentIndex(index) + + def setTabVisible(self, index, value): + return self.tabWidget.setTabVisible(index, value) + + def removeTab(self, index): + return self.tabWidget.removeTab(index) + + def isTabVisible(self, index): + return self.tabWidget.isTabVisible(index) + + def getCurrentIndex(self): + return self.tabWidget.currentIndex() + + def getWidgetAtIndex(self, index): + return self.tabWidget.widget(index) + + def getCount(self): + return self.tabWidget.count() + + # --- Events + def appWillSavePrefs(self): + # Right now this is useless since the first spawn dialog inside the + # QTabWidget will assign its geometry after restoring it + prefs = self.app.prefs + prefs.mainWindowIsMaximized = self.isMaximized() + prefs.mainWindowRect = self.geometry() + + def closeEvent(self, close_event): + # Force closing of our tabbed widgets in reverse order so that the + # directories dialog (which usually is at index 0) will be called last + for index in range(self.getCount() - 1, -1, -1): + self.getWidgetAtIndex(index).closeEvent(close_event) + self.appWillSavePrefs() + + @pyqtSlot(int) + def onTabCloseRequested(self, index): + current_widget = self.getWidgetAtIndex(index) + if isinstance(current_widget, DirectoriesDialog): + # if we close this one, the application quits. Force user to use the + # menu or shortcut. But this is useless if we don't have a button + # set up to make a close request anyway. This check could be removed. + return + current_widget.close() + self.setTabVisible(index, False) + # self.tabWidget.widget(index).hide() + self.removeTab(index) + + @pyqtSlot() + def onDialogAccepted(self): + """Remove tabbed dialog when Accepted/Done.""" + widget = self.sender() + index = self.indexOfWidget(widget) + if index > -1: + self.removeTab(index) + + @pyqtSlot() + def toggleTabBar(self): + value = self.sender().isChecked() + self.actionToggleTabs.setChecked(value) + self.tabWidget.tabBar().setVisible(value) + + +class TabBarWindow(TabWindow): + """Implementation which uses a separate QTabBar and QStackedWidget. + The Tab bar is placed next to the menu bar to save real estate.""" + def __init__(self, app, **kwargs): + super().__init__(app, **kwargs) + + def _setupUi(self): + self.setWindowTitle(self.app.NAME) + self.resize(640, 480) + self.tabBar = QTabBar() + self.verticalLayout = QVBoxLayout() + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self._setupActions() + self._setupMenu() + + self.centralWidget = QWidget(self) + self.setCentralWidget(self.centralWidget) + self.stackedWidget = QStackedWidget() + self.centralWidget.setLayout(self.verticalLayout) + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.addWidget(self.menubar, 0, Qt.AlignTop) + self.horizontalLayout.addWidget(self.tabBar, 0, Qt.AlignTop) + self.verticalLayout.addLayout(self.horizontalLayout) + self.verticalLayout.addWidget(self.stackedWidget) + + self.tabBar.currentChanged.connect(self.showWidget) + self.tabBar.tabCloseRequested.connect(self.onTabCloseRequested) + + self.stackedWidget.currentChanged.connect(self.updateMenuBar) + self.stackedWidget.widgetRemoved.connect(self.onRemovedWidget) + + self.tabBar.setTabsClosable(True) + self.restoreGeometry() + + def addTab(self, page, title, switch=True): + stack_index = self.stackedWidget.insertWidget(-1, page) + tab_index = self.tabBar.addTab(title) + + if isinstance(page, DirectoriesDialog): + self.tabBar.setTabButton( + tab_index, QTabBar.RightSide, None) + if switch: # switch to the added tab immediately upon creation + self.setTabIndex(tab_index) + self.stackedWidget.setCurrentWidget(page) + return stack_index + + @pyqtSlot(int) + def showWidget(self, index): + if index >= 0 and index <= self.stackedWidget.count() - 1: + self.stackedWidget.setCurrentIndex(index) + # if not self.tabBar.isTabVisible(index): + self.setTabVisible(index, True) + + def indexOfWidget(self, widget): + # Warning: this may return -1 if widget is not a child of stackedwidget + return self.stackedWidget.indexOf(widget) + + def setCurrentIndex(self, tab_index): + # The signal will handle switching the stackwidget's widget + self.setTabIndex(tab_index) + # self.stackedWidget.setCurrentWidget(self.stackedWidget.widget(tab_index)) + + @pyqtSlot(int) + def setTabIndex(self, index): + if not index: + return + self.tabBar.setCurrentIndex(index) + + def setTabVisible(self, index, value): + return self.tabBar.setTabVisible(index, value) + + @pyqtSlot(int) + def onRemovedWidget(self, index): + self.removeTab(index) + + @pyqtSlot(int) + def removeTab(self, index): + # No need to remove the widget here: + # self.stackedWidget.removeWidget(self.stackedWidget.widget(index)) + return self.tabBar.removeTab(index) + + @pyqtSlot(int) + def removeWidget(self, widget): + return self.stackedWidget.removeWidget(widget) + + def isTabVisible(self, index): + return self.tabBar.isTabVisible(index) + + def getCurrentIndex(self): + return self.stackedWidget.currentIndex() + + def getWidgetAtIndex(self, index): + return self.stackedWidget.widget(index) + + def getCount(self): + return self.stackedWidget.count() + + @pyqtSlot() + def toggleTabBar(self): + value = self.sender().isChecked() + self.actionToggleTabs.setChecked(value) + self.tabBar.setVisible(value) + + @pyqtSlot(int) + def onTabCloseRequested(self, index): + current_widget = self.getWidgetAtIndex(index) + current_widget.close() + self.stackedWidget.removeWidget(current_widget) + # In this case the signal will take care of the tab itself after removing the widget + # self.removeTab(index) From dd6ffe08d7b61d95a9bb0ce250e77109f74d4821 Mon Sep 17 00:00:00 2001 From: glubsy Date: Fri, 31 Jul 2020 01:32:29 +0200 Subject: [PATCH 2/7] Add option to place tab bar below main menu --- qt/app.py | 4 ++-- qt/preferences.py | 4 +++- qt/preferences_dialog.py | 8 +++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/qt/app.py b/qt/app.py index 6ed0df31..7869b382 100644 --- a/qt/app.py +++ b/qt/app.py @@ -35,7 +35,7 @@ from .se.preferences_dialog import PreferencesDialog as PreferencesDialogStandar from .me.preferences_dialog import PreferencesDialog as PreferencesDialogMusic from .pe.preferences_dialog import PreferencesDialog as PreferencesDialogPicture from .pe.photo import File as PlatSpecificPhoto -from .tabbed_window import TabBarWindow +from .tabbed_window import TabBarWindow, TabWindow tr = trget("ui") @@ -64,7 +64,7 @@ class DupeGuru(QObject): self.resultWindow = None self.details_dialog = None if self.use_tabs: - self.main_window = TabBarWindow(self) + self.main_window = TabBarWindow(self) if not self.prefs.tabs_default_pos else TabWindow(self) parent_window = self.main_window self.directories_dialog = self.main_window.createPage("DirectoriesDialog", app=self) self.main_window.addTab( diff --git a/qt/preferences.py b/qt/preferences.py index 009bab13..0b2db472 100644 --- a/qt/preferences.py +++ b/qt/preferences.py @@ -45,7 +45,7 @@ class Preferences(PreferencesBase): self.recentResults = get("RecentResults", self.recentResults) self.recentFolders = get("RecentFolders", self.recentFolders) - + self.tabs_default_pos = get("TabsDefaultPosition", self.tabs_default_pos) self.word_weighting = get("WordWeighting", self.word_weighting) self.match_similar = get("MatchSimilar", self.match_similar) self.ignore_small_files = get("IgnoreSmallFiles", self.ignore_small_files) @@ -80,6 +80,7 @@ class Preferences(PreferencesBase): self.recentResults = [] self.recentFolders = [] + self.tabs_default_pos = False self.word_weighting = True self.match_similar = False self.ignore_small_files = True @@ -115,6 +116,7 @@ class Preferences(PreferencesBase): set_("RecentResults", self.recentResults) set_("RecentFolders", self.recentFolders) + set_("TabsDefaultPosition", self.tabs_default_pos) set_("WordWeighting", self.word_weighting) set_("MatchSimilar", self.match_similar) set_("IgnoreSmallFiles", self.ignore_small_files) diff --git a/qt/preferences_dialog.py b/qt/preferences_dialog.py index eb3462e3..289ae926 100644 --- a/qt/preferences_dialog.py +++ b/qt/preferences_dialog.py @@ -117,8 +117,12 @@ class PreferencesDialogBase(QDialog): self.widgetsVLayout.addLayout( horizontalWrap([self.fontSizeLabel, self.fontSizeSpinBox, None]) ) - self._setupAddCheckbox("reference_bold_font", tr("Bold font for reference.")) + self._setupAddCheckbox("reference_bold_font", tr("Bold font for reference")) self.widgetsVLayout.addWidget(self.reference_bold_font) + self._setupAddCheckbox("tabs_default_pos", tr("Use default position for tab bar (requires restart)")) + self.tabs_default_pos.setToolTip(tr("Place the tab bar below the main menu instead of next to it")) + self.widgetsVLayout.addWidget(self.tabs_default_pos) + self.languageLabel = QLabel(tr("Language:"), self) self.languageComboBox = QComboBox(self) for lang in self.supportedLanguages: @@ -190,6 +194,7 @@ class PreferencesDialogBase(QDialog): setchecked(self.ignoreHardlinkMatches, prefs.ignore_hardlink_matches) setchecked(self.debugModeBox, prefs.debug_mode) setchecked(self.reference_bold_font, prefs.reference_bold_font) + setchecked(self.tabs_default_pos, prefs.tabs_default_pos) self.copyMoveDestinationComboBox.setCurrentIndex(prefs.destination_type) self.customCommandEdit.setText(prefs.custom_command) self.fontSizeSpinBox.setValue(prefs.tableFontSize) @@ -213,6 +218,7 @@ class PreferencesDialogBase(QDialog): prefs.destination_type = self.copyMoveDestinationComboBox.currentIndex() prefs.custom_command = str(self.customCommandEdit.text()) prefs.tableFontSize = self.fontSizeSpinBox.value() + prefs.tabs_default_pos = ischecked(self.tabs_default_pos) lang = self.supportedLanguages[self.languageComboBox.currentIndex()] oldlang = self.app.prefs.language if oldlang not in self.supportedLanguages: From 1b3b40543b2139dcda3a34c5f4eaf3a1b13081cf Mon Sep 17 00:00:00 2001 From: glubsy Date: Fri, 31 Jul 2020 03:59:37 +0200 Subject: [PATCH 3/7] Fix ignore list view menu entry being disabled --- qt/tabbed_window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qt/tabbed_window.py b/qt/tabbed_window.py index c8a06fd3..2bec88b5 100644 --- a/qt/tabbed_window.py +++ b/qt/tabbed_window.py @@ -129,6 +129,7 @@ class TabWindow(QMainWindow): for action in menu.actions(): if action is self.app.directories_dialog.actionShowResultsWindow: if isResultWindow: + self.app.actionIgnoreList.setEnabled(self.app.ignoreListDialog is not None) # Action points to ourselves, always disable it self.app.directories_dialog.actionShowResultsWindow\ .setEnabled(False) From 86e1b55b0216be143a6320771f8c9412455302b5 Mon Sep 17 00:00:00 2001 From: glubsy Date: Fri, 31 Jul 2020 05:08:08 +0200 Subject: [PATCH 4/7] Fix menu items being wrongly disabled * Add Directories to the View menu. * View menu items should be disabled properly depending on whether they point to the current page/tab. * Keep "Load scan results" actions active while viewing pages other than the Directories tab. --- qt/app.py | 12 ++++++++++++ qt/directories_dialog.py | 12 ++++++------ qt/tabbed_window.py | 35 +++++++++++++++-------------------- 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/qt/app.py b/qt/app.py index 7869b382..5cbdf43e 100644 --- a/qt/app.py +++ b/qt/app.py @@ -69,6 +69,7 @@ class DupeGuru(QObject): self.directories_dialog = self.main_window.createPage("DirectoriesDialog", app=self) self.main_window.addTab( self.directories_dialog, "Directories", switch=False) + self.actionDirectoriesWindow.setEnabled(False) else: # floating windows only self.main_window = None self.directories_dialog = DirectoriesDialog(self) @@ -121,6 +122,7 @@ class DupeGuru(QObject): self.preferencesTriggered, ), ("actionIgnoreList", "", "", tr("Ignore List"), self.ignoreListTriggered), + ("actionDirectoriesWindow", "", "", tr("Directories"), self.showDirectoriesWindow), ( "actionClearPictureCache", "Ctrl+Shift+P", @@ -220,6 +222,16 @@ class DupeGuru(QObject): else: self.resultWindow.show() + def showDirectoriesWindow(self): + if self.directories_dialog is not None: + if self.main_window: + index = self.main_window.indexOfWidget(self.directories_dialog) + # if not self.main_window.tabWidget.isTabVisible(index): + self.main_window.setTabVisible(index, True) + self.main_window.setCurrentIndex(index) + else: + self.directories_dialog.show() + def shutdown(self): self.willSavePrefs.emit() self.prefs.save() diff --git a/qt/directories_dialog.py b/qt/directories_dialog.py index 97c18b18..5d772cb7 100644 --- a/qt/directories_dialog.py +++ b/qt/directories_dialog.py @@ -88,17 +88,17 @@ class DirectoriesDialog(QMainWindow): "actionShowResultsWindow", "", "", - tr("Results Window"), + tr("Scan Results"), self.app.showResultsWindow, ), ("actionAddFolder", "", "", tr("Add Folder..."), self.addFolderTriggered), ] createActions(ACTIONS, self) - if self.app.main_window: # We use tab widgets in this case - # Keep track of actions which should only be accessible from this class - for action, _, _, _, _ in ACTIONS: - self.specific_actions.add(getattr(self, action)) + # if self.app.main_window: # We use tab widgets in this case + # # Keep track of actions which should only be accessible from this class + # for action, _, _, _, _ in ACTIONS: + # self.specific_actions.add(getattr(self, action)) def _setupMenu(self): if not self.app.main_window: @@ -124,7 +124,6 @@ class DirectoriesDialog(QMainWindow): self.menuLoadRecent.setTitle(tr("Load Recent Results")) self.menuFile.addAction(self.actionLoadResults) self.menuFile.addAction(self.menuLoadRecent.menuAction()) - self.specific_actions.add(self.menuLoadRecent.menuAction()) self.menuFile.addSeparator() self.menuFile.addAction(self.app.actionClearPictureCache) self.menuFile.addSeparator() @@ -132,6 +131,7 @@ class DirectoriesDialog(QMainWindow): self.menuView.addAction(self.app.actionPreferences) self.menuView.addAction(self.actionShowResultsWindow) self.menuView.addAction(self.app.actionIgnoreList) + self.menuView.addAction(self.app.actionDirectoriesWindow) self.menuHelp.addAction(self.app.actionShowHelp) self.menuHelp.addAction(self.app.actionOpenDebugLog) self.menuHelp.addAction(self.app.actionAbout) diff --git a/qt/tabbed_window.py b/qt/tabbed_window.py index 2bec88b5..66ea13cc 100644 --- a/qt/tabbed_window.py +++ b/qt/tabbed_window.py @@ -117,38 +117,33 @@ class TabWindow(QMainWindow): self.last_index = current_index self.previous_widget_actions = active_widget.specific_actions return - isResultWindow = isinstance(active_widget, ResultWindow) - isIgnoreListDialog = isinstance(active_widget, IgnoreListDialog) + + page_type = type(active_widget).__name__ for menu in self.menuList: if menu is self.menuColumns or menu is self.menuActions or menu is self.menuMark: - if not isResultWindow: + if not isinstance(active_widget, ResultWindow): menu.setEnabled(False) continue else: menu.setEnabled(True) + for action in menu.actions(): - if action is self.app.directories_dialog.actionShowResultsWindow: - if isResultWindow: - self.app.actionIgnoreList.setEnabled(self.app.ignoreListDialog is not None) - # Action points to ourselves, always disable it - self.app.directories_dialog.actionShowResultsWindow\ - .setEnabled(False) - continue - else: - self.app.directories_dialog.actionShowResultsWindow\ - .setEnabled(self.app.resultWindow is not None) - if isIgnoreListDialog: - self.app.actionIgnoreList.setEnabled(False) - continue - else: - self.app.actionIgnoreList.setEnabled(self.app.ignoreListDialog is not None) - continue if action not in active_widget.specific_actions: if action in self.previous_widget_actions: - action.setEnabled(False) + # action.setEnabled(False) + menu.removeAction(action) continue action.setEnabled(True) + self.app.directories_dialog.actionShowResultsWindow.setEnabled( + False if page_type == "ResultWindow" + else self.app.resultWindow is not None) + self.app.actionIgnoreList.setEnabled( + True if self.app.ignoreListDialog is not None + and not page_type == "IgnoreListDialog" else False) + self.app.actionDirectoriesWindow.setEnabled( + False if page_type == "DirectoriesDialog" else True) + self.previous_widget_actions = active_widget.specific_actions self.last_index = current_index From a542168a0d02fd6757895b5edea08fff9a3612ec Mon Sep 17 00:00:00 2001 From: glubsy Date: Fri, 31 Jul 2020 16:57:18 +0200 Subject: [PATCH 5/7] Reorganize view menu entries and keep consistency --- qt/directories_dialog.py | 9 ++++++--- qt/result_window.py | 12 +++++++++--- qt/tabbed_window.py | 4 +--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/qt/directories_dialog.py b/qt/directories_dialog.py index 5d772cb7..283731e9 100644 --- a/qt/directories_dialog.py +++ b/qt/directories_dialog.py @@ -94,7 +94,6 @@ class DirectoriesDialog(QMainWindow): ("actionAddFolder", "", "", tr("Add Folder..."), self.addFolderTriggered), ] createActions(ACTIONS, self) - # if self.app.main_window: # We use tab widgets in this case # # Keep track of actions which should only be accessible from this class # for action, _, _, _, _ in ACTIONS: @@ -122,16 +121,20 @@ class DirectoriesDialog(QMainWindow): self.menuLoadRecent = QMenu(self.menuFile) self.menuLoadRecent.setTitle(tr("Load Recent Results")) + self.menuFile.addAction(self.actionLoadResults) self.menuFile.addAction(self.menuLoadRecent.menuAction()) self.menuFile.addSeparator() self.menuFile.addAction(self.app.actionClearPictureCache) self.menuFile.addSeparator() self.menuFile.addAction(self.app.actionQuit) - self.menuView.addAction(self.app.actionPreferences) + + self.menuView.addAction(self.app.actionDirectoriesWindow) self.menuView.addAction(self.actionShowResultsWindow) self.menuView.addAction(self.app.actionIgnoreList) - self.menuView.addAction(self.app.actionDirectoriesWindow) + self.menuView.addSeparator() + self.menuView.addAction(self.app.actionPreferences) + self.menuHelp.addAction(self.app.actionShowHelp) self.menuHelp.addAction(self.app.actionOpenDebugLog) self.menuHelp.addAction(self.app.actionAbout) diff --git a/qt/result_window.py b/qt/result_window.py index 5656fbeb..06c24e44 100644 --- a/qt/result_window.py +++ b/qt/result_window.py @@ -214,7 +214,7 @@ class ResultWindow(QMainWindow): self.specific_actions.add(getattr(self, action)) def _setupMenu(self): - if not self.app.main_window: + if not self.app.use_tabs: # we are our own QMainWindow, we need our own menu bar self.menubar = QMenuBar() # self.menuBar() works as well here self.menubar.setGeometry(QRect(0, 0, 630, 22)) @@ -260,12 +260,18 @@ class ResultWindow(QMainWindow): self.menuMark.addAction(self.actionMarkNone) self.menuMark.addAction(self.actionInvertMarking) self.menuMark.addAction(self.actionMarkSelected) + + self.menuView.addAction(self.actionDetails) + self.menuView.addSeparator() self.menuView.addAction(self.actionPowerMarker) self.menuView.addAction(self.actionDelta) self.menuView.addSeparator() - self.menuView.addAction(self.actionDetails) - self.menuView.addAction(self.app.actionIgnoreList) + if not self.app.use_tabs: + self.menuView.addAction(self.app.actionIgnoreList) + # This also pushes back the options entry to the bottom of the menu + self.menuView.addSeparator() self.menuView.addAction(self.app.actionPreferences) + self.menuHelp.addAction(self.app.actionShowHelp) self.menuHelp.addAction(self.app.actionOpenDebugLog) self.menuHelp.addAction(self.app.actionAbout) diff --git a/qt/tabbed_window.py b/qt/tabbed_window.py index 66ea13cc..83f1c5c8 100644 --- a/qt/tabbed_window.py +++ b/qt/tabbed_window.py @@ -126,12 +126,10 @@ class TabWindow(QMainWindow): continue else: menu.setEnabled(True) - for action in menu.actions(): if action not in active_widget.specific_actions: if action in self.previous_widget_actions: - # action.setEnabled(False) - menu.removeAction(action) + action.setEnabled(False) continue action.setEnabled(True) From 87f931780539787e911fe3ed4fd4e46b46bfa654 Mon Sep 17 00:00:00 2001 From: glubsy Date: Fri, 31 Jul 2020 16:59:34 +0200 Subject: [PATCH 6/7] Place tab bar below menu bar by default --- qt/preferences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/preferences.py b/qt/preferences.py index 0b2db472..e99e0d9b 100644 --- a/qt/preferences.py +++ b/qt/preferences.py @@ -80,7 +80,7 @@ class Preferences(PreferencesBase): self.recentResults = [] self.recentFolders = [] - self.tabs_default_pos = False + self.tabs_default_pos = True self.word_weighting = True self.match_similar = False self.ignore_small_files = True From 63a9f00552d5d474d8094c31e5c5b240d023b184 Mon Sep 17 00:00:00 2001 From: glubsy Date: Fri, 31 Jul 2020 22:27:18 +0200 Subject: [PATCH 7/7] Add minor change to variable names --- qt/app.py | 11 +++++------ qt/directories_dialog.py | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/qt/app.py b/qt/app.py index 5cbdf43e..43e9e8db 100644 --- a/qt/app.py +++ b/qt/app.py @@ -81,7 +81,7 @@ class DupeGuru(QObject): self.problemDialog = ProblemDialog( parent=parent_window, model=self.model.problem_dialog ) - if self.main_window: # we use tab widget + if self.use_tabs: self.ignoreListDialog = self.main_window.createPage( "IgnoreListDialog", parent=self.main_window, @@ -216,7 +216,7 @@ class DupeGuru(QObject): def showResultsWindow(self): if self.resultWindow is not None: - if self.main_window: + if self.use_tabs: self.main_window.addTab( self.resultWindow, "Results", switch=True) else: @@ -224,9 +224,8 @@ class DupeGuru(QObject): def showDirectoriesWindow(self): if self.directories_dialog is not None: - if self.main_window: + if self.use_tabs: index = self.main_window.indexOfWidget(self.directories_dialog) - # if not self.main_window.tabWidget.isTabVisible(index): self.main_window.setTabVisible(index, True) self.main_window.setCurrentIndex(index) else: @@ -264,7 +263,7 @@ class DupeGuru(QObject): QMessageBox.information(active, title, tr("Picture cache cleared.")) def ignoreListTriggered(self): - if self.main_window: + if self.use_tabs: # Fetch the index in the TabWidget or the StackWidget (depends on class): index = self.main_window.indexOfWidget(self.ignoreListDialog) if index < 0: @@ -339,7 +338,7 @@ class DupeGuru(QObject): if self.resultWindow is not None: self.resultWindow.close() self.resultWindow.setParent(None) - if self.main_window: + if self.use_tabs: self.resultWindow = self.main_window.createPage( "ResultWindow", parent=self.main_window, app=self) else: # We don't use a tab widget, regular floating QMainWindow diff --git a/qt/directories_dialog.py b/qt/directories_dialog.py index 283731e9..45e53be6 100644 --- a/qt/directories_dialog.py +++ b/qt/directories_dialog.py @@ -94,13 +94,13 @@ class DirectoriesDialog(QMainWindow): ("actionAddFolder", "", "", tr("Add Folder..."), self.addFolderTriggered), ] createActions(ACTIONS, self) - # if self.app.main_window: # We use tab widgets in this case + # if self.app.use_tabs: # # Keep track of actions which should only be accessible from this class # for action, _, _, _, _ in ACTIONS: # self.specific_actions.add(getattr(self, action)) def _setupMenu(self): - if not self.app.main_window: + if not self.app.use_tabs: # we are our own QMainWindow, we need our own menu bar self.menubar = QMenuBar(self) self.menubar.setGeometry(QRect(0, 0, 42, 22))