diff --git a/Furious/Qt/QtWidgets.py b/Furious/Qt/QtWidgets.py index 0dcf3bd..0252757 100644 --- a/Furious/Qt/QtWidgets.py +++ b/Furious/Qt/QtWidgets.py @@ -51,6 +51,7 @@ __all__ = [ 'AppQMenuBar', 'AppQMessageBox', 'AppQIconTextPushButton', + 'AppQMenuPushButton', 'AppQPushButton', 'AppQSpinBox', 'AppQTableView', @@ -832,6 +833,39 @@ class AppQPushButton(Mixins.QTranslatable, Mixins.ThemeAware, QPushButton): self.setText(_(self.text())) +class AppQMenuPushButton(AppQPushButton): + """Open a popup menu from a regular Fluent-style push button.""" + + def __init__(self, *args, popupMenu=None, **kwargs): + """Initialize a button without Qt's native menu indicator.""" + super().__init__(*args, **kwargs) + + self._popupMenu = None + self.setPopupMenu(popupMenu) + self.clicked.connect(self.showPopupMenu) + + def popupMenu(self): + """Return the menu presented by this button.""" + return self._popupMenu + + def setPopupMenu(self, menu): + """Set the menu presented below this button.""" + if menu is not None and not isinstance(menu, QMenu): + raise TypeError('popupMenu must be a QMenu or None') + + self._popupMenu = menu + + @QtCore.Slot() + def showPopupMenu(self): + """Open the configured menu immediately below the button.""" + if self._popupMenu is None: + return + + position = self.mapToGlobal(QtCore.QPoint(0, self.height() + 2)) + + self._popupMenu.popup(position) + + class AppQIconTextPushButton( Mixins.QTranslatable, Mixins.ThemeAware, diff --git a/Furious/Qt/__init__.py b/Furious/Qt/__init__.py index 67a7022..7989e7f 100644 --- a/Furious/Qt/__init__.py +++ b/Furious/Qt/__init__.py @@ -65,6 +65,7 @@ from .QtWidgets import ( AppQMainWindow, AppQMenu, AppQMenuBar, + AppQMenuPushButton, AppQMessageBox, AppQPushButton, AppQSpinBox, @@ -116,6 +117,7 @@ __all__ = [ 'AppQMainWindow', 'AppQMenu', 'AppQMenuBar', + 'AppQMenuPushButton', 'AppQMessageBox', 'AppQNetworkAccessManager', 'AppQPlainTextEdit', diff --git a/Furious/Window/HomePage.py b/Furious/Window/HomePage.py index 2194a92..0e064b8 100644 --- a/Furious/Window/HomePage.py +++ b/Furious/Window/HomePage.py @@ -477,13 +477,11 @@ class HomePage(Mixins.QTranslatable, QMainWindow): ) self.serverMenu = AppQMenu(*serverActions, parent=self) - self.serverButton = AppQPushButton( + self.serverButton = AppQMenuPushButton( _('Server'), icon=bootstrapIcon('server.svg'), + popupMenu=self.serverMenu, ) - # Keep the protocol/profile creation menu without presenting this as a - # split or drop-down button. The regular button opens the menu itself. - self.serverButton.clicked.connect(self.showServerMenu) self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) @@ -588,15 +586,6 @@ class HomePage(Mixins.QTranslatable, QMainWindow): self.setCentralWidget(self._widget) - @QtCore.Slot() - def showServerMenu(self): - """Open server creation actions below the regular Server button.""" - position = self.serverButton.mapToGlobal( - QtCore.QPoint(0, self.serverButton.height() + 2) - ) - - self.serverMenu.popup(position) - @QtCore.Slot() def handleServerSelectionChanged(self, *_args): """Apply Home's selection policy to the shared connection control.""" diff --git a/Furious/Window/LogPage.py b/Furious/Window/LogPage.py index 16d9979..73959d7 100644 --- a/Furious/Window/LogPage.py +++ b/Furious/Window/LogPage.py @@ -217,9 +217,34 @@ class LogPage(Mixins.QTranslatable, QMainWindow): parent=self, ) - self.menuBar().addMenu(self._fileMenu) - self.menuBar().addMenu(self._editMenu) - self.menuBar().addMenu(self._viewMenu) + self.fileButton = AppQMenuPushButton( + _('File'), + icon=bootstrapIcon('file-earmark.svg'), + popupMenu=self._fileMenu, + ) + self.editButton = AppQMenuPushButton( + _('Edit'), + icon=bootstrapIcon('pencil-square.svg'), + popupMenu=self._editMenu, + ) + self.viewButton = AppQMenuPushButton( + _('View'), + icon=bootstrapIcon('eye.svg'), + popupMenu=self._viewMenu, + ) + + actionLayout = QHBoxLayout() + actionLayout.setContentsMargins(0, 0, 0, 0) + actionLayout.setSpacing(8) + actionLayout.addWidget(self.fileButton) + actionLayout.addWidget(self.editButton) + actionLayout.addWidget(self.viewButton) + actionLayout.addStretch(1) + + centralLayout.insertLayout(1, actionLayout) + + for menu in (self._fileMenu, self._editMenu, self._viewMenu): + self._registerMenuShortcuts(menu) self._populateFilters(self._preferredFilter) self._refreshEntries() @@ -231,6 +256,19 @@ class LogPage(Mixins.QTranslatable, QMainWindow): self.retranslate() + def _registerMenuShortcuts(self, menu): + """Associate popup actions with the page so shortcuts stay active.""" + for action in menu.actions(): + if action.isSeparator(): + continue + + self.addAction(action) + + submenu = action.menu() if hasattr(action, 'menu') else None + + if submenu is not None: + self._registerMenuShortcuts(submenu) + def _categoryText(self, category) -> str: """Return a category's translated or literal display label.""" if category.id == APPLICATION_LOG_CATEGORY: