diff --git a/qt/app.py b/qt/app.py index bd85d8a0..8f02c341 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, TabWindow 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,42 @@ class DupeGuru(QObject): self.recentResults = Recent(self, "recentResults") self.recentResults.mustOpenItem.connect(self.model.load_from) self.resultWindow = None - self.directories_dialog = DirectoriesDialog(self) + if self.use_tabs: + 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( + self.directories_dialog, "Directories", switch=False) + self.actionDirectoriesWindow.setEnabled(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.use_tabs: + 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) @@ -98,6 +122,7 @@ class DupeGuru(QObject): self.preferencesTriggered, ), ("actionIgnoreList", "", "", tr("Ignore List"), self.ignoreListTriggered), + ("actionDirectoriesWindow", "", "", tr("Directories"), self.showDirectoriesWindow), ( "actionClearPictureCache", "Ctrl+Shift+P", @@ -197,7 +222,20 @@ class DupeGuru(QObject): def showResultsWindow(self): if self.resultWindow is not None: - self.resultWindow.show() + if self.use_tabs: + self.main_window.addTab( + self.resultWindow, "Results", switch=True) + else: + self.resultWindow.show() + + def showDirectoriesWindow(self): + if self.directories_dialog is not None: + if self.use_tabs: + index = self.main_window.indexOfWidget(self.directories_dialog) + self.main_window.setTabVisible(index, True) + self.main_window.setCurrentIndex(index) + else: + self.directories_dialog.show() def shutdown(self): self.willSavePrefs.emit() @@ -218,7 +256,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") @@ -229,7 +269,19 @@ class DupeGuru(QObject): QMessageBox.information(active, title, tr("Picture cache cleared.")) def ignoreListTriggered(self): - self.model.ignore_list_dialog.show() + 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: + # 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") @@ -237,7 +289,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() @@ -250,7 +303,11 @@ class DupeGuru(QObject): def quitTriggered(self): if self.details_dialog is not None: self.details_dialog.close() - self.directories_dialog.close() + + if self.main_window: + self.main_window.close() + else: + self.directories_dialog.close() def showAboutBoxTriggered(self): self.about_box.show() @@ -294,8 +351,12 @@ class DupeGuru(QObject): if self.resultWindow is not None: self.resultWindow.close() self.resultWindow.setParent(None) - self.resultWindow = ResultWindow(self.directories_dialog, self) - self.directories_dialog._updateActionsState() + 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 + 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..45e53be6 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() @@ -87,25 +88,39 @@ 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.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): - 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.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)) + 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()) @@ -113,16 +128,20 @@ class DirectoriesDialog(QMainWindow): 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.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) - 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 +158,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 85d118db..f9cd9649 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 a6fdecbe..210bb019 100644 --- a/qt/preferences.py +++ b/qt/preferences.py @@ -56,12 +56,17 @@ 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) - + 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) @@ -99,9 +104,12 @@ class Preferences(PreferencesBase): self.resultWindowIsMaximized = False self.resultWindowRect = None self.directoriesWindowRect = None + self.mainWindowRect = None + self.mainWindowIsMaximized = False self.recentResults = [] self.recentFolders = [] + self.tabs_default_pos = True self.word_weighting = True self.match_similar = False self.ignore_small_files = True @@ -137,11 +145,14 @@ class Preferences(PreferencesBase): set_("ResultTableRefForegroundColor", self.result_table_ref_foreground_color) set_("ResultTableDeltaForegroundColor", self.result_table_delta_foreground_color) 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) + 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 4f362d05..24eb7d53 100644 --- a/qt/preferences_dialog.py +++ b/qt/preferences_dialog.py @@ -152,6 +152,12 @@ class PreferencesDialogBase(QDialog): self.displayVLayout.insertLayout( 0, horizontalWrap([self.languageLabel, self.languageComboBox, None]) ) + 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\n\ +On MacOS, the tab bar will fill up the window's width instead.")) + self.displayVLayout.addWidget(self.tabs_default_pos) gridlayout = QFormLayout() result_groupbox = QGroupBox("&Result Table") @@ -163,9 +169,11 @@ class PreferencesDialogBase(QDialog): gridlayout.addRow(self.reference_bold_font) self.result_table_ref_foreground_color = ColorPickerButton(self) - gridlayout.addRow(tr("Reference foreground color:"), self.result_table_ref_foreground_color) + gridlayout.addRow(tr("Reference foreground color:"), + self.result_table_ref_foreground_color) self.result_table_delta_foreground_color = ColorPickerButton(self) - gridlayout.addRow(tr("Delta foreground color:"), self.result_table_delta_foreground_color) + gridlayout.addRow(tr("Delta foreground color:"), + self.result_table_delta_foreground_color) gridlayout.setLabelAlignment(Qt.AlignLeft) # Keep same vertical spacing as parent layout for consistency @@ -266,6 +274,7 @@ use the modifier key to drag the floating window around") if ISLINUX else self.customCommandEdit.setText(prefs.custom_command) if section & Sections.DISPLAY: setchecked(self.reference_bold_font, prefs.reference_bold_font) + setchecked(self.tabs_default_pos, prefs.tabs_default_pos) setchecked(self.details_dialog_titlebar_enabled, prefs.details_dialog_titlebar_enabled) setchecked(self.details_dialog_vertical_titlebar, @@ -302,6 +311,7 @@ use the modifier key to drag the floating window around") if ISLINUX else 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: diff --git a/qt/result_window.py b/qt/result_window.py index e28d10e0..9a3ab41b 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.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)) + 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) @@ -242,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) @@ -257,15 +281,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 +308,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..83f1c5c8 --- /dev/null +++ b/qt/tabbed_window.py @@ -0,0 +1,350 @@ +# 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 + + 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 isinstance(active_widget, ResultWindow): + menu.setEnabled(False) + 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) + 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 + + 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)