mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-29 10:28:07 +03:00
Polish navigation controls
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -455,11 +455,11 @@ class AppStyleSheet:
|
||||
border-radius: 8px;
|
||||
}}
|
||||
|
||||
QToolButton#NavigationToggleButton,
|
||||
QToolButton#NavigationPageButton {{
|
||||
QPushButton#NavigationToggleButton,
|
||||
QPushButton#NavigationPageButton {{
|
||||
min-width: 0;
|
||||
min-height: 38px;
|
||||
padding: 4px 10px;
|
||||
padding: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 7px;
|
||||
background-color: transparent;
|
||||
@@ -467,14 +467,14 @@ class AppStyleSheet:
|
||||
text-align: left;
|
||||
}}
|
||||
|
||||
QToolButton#NavigationToggleButton:hover,
|
||||
QToolButton#NavigationPageButton:hover {{
|
||||
QPushButton#NavigationToggleButton:hover,
|
||||
QPushButton#NavigationPageButton:hover {{
|
||||
border-color: transparent;
|
||||
background-color: {palette['hover']};
|
||||
color: {palette['text_strong']};
|
||||
}}
|
||||
|
||||
QToolButton#NavigationPageButton:checked {{
|
||||
QPushButton#NavigationPageButton:checked {{
|
||||
border: none;
|
||||
border-left: 3px solid {palette['accent']};
|
||||
border-radius: 7px;
|
||||
@@ -483,10 +483,25 @@ class AppStyleSheet:
|
||||
font-weight: 600;
|
||||
}}
|
||||
|
||||
QToolButton#NavigationPageButton:checked:hover {{
|
||||
QPushButton#NavigationPageButton:checked:hover {{
|
||||
background-color: {palette['accent_soft_hover']};
|
||||
}}
|
||||
|
||||
QPushButton#NavigationPageButton QLabel {{
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: {palette['text']};
|
||||
}}
|
||||
|
||||
QPushButton#NavigationPageButton:checked QLabel {{
|
||||
color: {palette['text_strong']};
|
||||
font-weight: 600;
|
||||
}}
|
||||
|
||||
QPushButton#SearchButton {{
|
||||
padding: 0;
|
||||
}}
|
||||
|
||||
QToolButton:hover {{
|
||||
border-color: {palette['border']};
|
||||
background-color: {palette['hover']};
|
||||
|
||||
@@ -49,12 +49,14 @@ __all__ = [
|
||||
'AppQMenu',
|
||||
'AppQMenuBar',
|
||||
'AppQMessageBox',
|
||||
'AppQIconTextPushButton',
|
||||
'AppQPushButton',
|
||||
'AppQSpinBox',
|
||||
'AppQTableView',
|
||||
'AppQTableWidget',
|
||||
'AppQTabWidget',
|
||||
'AppQToolBar',
|
||||
'IconTextPushButton',
|
||||
'MBoxQuestionDelete',
|
||||
'MBoxNewChangesNextTime',
|
||||
'MBoxDirectRulesNotAllowed',
|
||||
@@ -592,6 +594,113 @@ class AppQMessageBox(Mixins.QTranslatable, Mixins.ConnectionAware, QMessageBox):
|
||||
self.setWindowIcon(AppHue.connectedWindowIcon())
|
||||
|
||||
|
||||
class IconTextPushButton(QPushButton):
|
||||
"""Present a push-button icon and label through an explicit layout."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
iconTextSpacing=12,
|
||||
horizontalMargin=13,
|
||||
verticalMargin=3,
|
||||
iconSize=QtCore.QSize(16, 16),
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize layout-managed icon and text presentation."""
|
||||
icon = kwargs.pop('icon', None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
text = QPushButton.text(self)
|
||||
|
||||
QPushButton.setText(self, '')
|
||||
QPushButton.setIcon(self, QIcon())
|
||||
QPushButton.setIconSize(self, iconSize)
|
||||
|
||||
self._text = ''
|
||||
self._icon = QIcon()
|
||||
|
||||
self._iconLabel = QLabel(parent=self)
|
||||
self._iconLabel.setObjectName('IconTextPushButtonIcon')
|
||||
self._iconLabel.setFixedSize(iconSize)
|
||||
self._iconLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self._iconLabel.setAttribute(
|
||||
QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents
|
||||
)
|
||||
|
||||
self._textLabel = QLabel(parent=self)
|
||||
self._textLabel.setObjectName('IconTextPushButtonText')
|
||||
self._textLabel.setAttribute(
|
||||
QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents
|
||||
)
|
||||
|
||||
self._iconTextLayout = QHBoxLayout(self)
|
||||
self._iconTextLayout.setContentsMargins(
|
||||
horizontalMargin,
|
||||
verticalMargin,
|
||||
horizontalMargin,
|
||||
verticalMargin,
|
||||
)
|
||||
self._iconTextLayout.setSpacing(iconTextSpacing)
|
||||
self._iconTextLayout.addWidget(self._iconLabel)
|
||||
self._iconTextLayout.addWidget(self._textLabel)
|
||||
self._iconTextLayout.addStretch()
|
||||
|
||||
self.setText(text)
|
||||
|
||||
if icon is not None:
|
||||
self.setIcon(icon)
|
||||
|
||||
def setIcon(self, icon: QIcon):
|
||||
"""Render *icon* in the layout-managed icon label."""
|
||||
self._icon = icon
|
||||
self._iconLabel.setPixmap(icon.pixmap(self.iconSize()))
|
||||
|
||||
def icon(self) -> QIcon:
|
||||
"""Return the displayed icon."""
|
||||
return self._icon
|
||||
|
||||
def setIconSize(self, size: QtCore.QSize):
|
||||
"""Resize and rerender the layout-managed icon."""
|
||||
QPushButton.setIconSize(self, size)
|
||||
|
||||
if not hasattr(self, '_iconLabel'):
|
||||
return
|
||||
|
||||
self._iconLabel.setFixedSize(size)
|
||||
self._iconLabel.setPixmap(self._icon.pixmap(size))
|
||||
|
||||
def setText(self, text: str):
|
||||
"""Set the visible and accessible button label."""
|
||||
self._text = text
|
||||
self._textLabel.setText(text)
|
||||
self.setAccessibleName(text)
|
||||
self.updateGeometry()
|
||||
|
||||
def text(self) -> str:
|
||||
"""Return the button label."""
|
||||
return self._text
|
||||
|
||||
def setTextVisible(self, visible: bool):
|
||||
"""Set whether the text label participates in the layout."""
|
||||
self._textLabel.setVisible(visible)
|
||||
self.updateGeometry()
|
||||
|
||||
def iconTextSpacing(self) -> int:
|
||||
"""Return the space between the icon and text label."""
|
||||
return self._iconTextLayout.spacing()
|
||||
|
||||
def sizeHint(self):
|
||||
"""Include the managed icon and text layout in the preferred size."""
|
||||
baseHint = super().sizeHint()
|
||||
layoutHint = self._iconTextLayout.sizeHint()
|
||||
|
||||
return QtCore.QSize(
|
||||
max(baseHint.width(), layoutHint.width()),
|
||||
max(baseHint.height(), layoutHint.height()),
|
||||
)
|
||||
|
||||
|
||||
class AppQPushButton(Mixins.QTranslatable, Mixins.ThemeAware, QPushButton):
|
||||
"""Represent app q push button."""
|
||||
|
||||
@@ -662,6 +771,66 @@ class AppQPushButton(Mixins.QTranslatable, Mixins.ThemeAware, QPushButton):
|
||||
self.setText(_(self.text()))
|
||||
|
||||
|
||||
class AppQIconTextPushButton(
|
||||
Mixins.QTranslatable,
|
||||
Mixins.ThemeAware,
|
||||
IconTextPushButton,
|
||||
):
|
||||
"""Provide a theme-aware application icon-text push button."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize a button with an optional application icon."""
|
||||
icon = kwargs.pop('icon', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.iconFileName = ''
|
||||
|
||||
if icon is not None:
|
||||
self.setIcon(icon)
|
||||
|
||||
def setIconByTheme(self, theme):
|
||||
"""Apply the stored application icon for *theme*."""
|
||||
if not self.iconFileName:
|
||||
return
|
||||
|
||||
if AppSettings.isStateON_('DarkMode'):
|
||||
IconTextPushButton.setIcon(
|
||||
self,
|
||||
bootstrapIconWhite(self.iconFileName),
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
if theme == 'Dark':
|
||||
if PLATFORM == 'Windows' and versionToValue(
|
||||
PYSIDE6_VERSION
|
||||
) < versionToValue('6.7.0'):
|
||||
icon = bootstrapIcon(self.iconFileName)
|
||||
else:
|
||||
icon = bootstrapIconWhite(self.iconFileName)
|
||||
else:
|
||||
icon = bootstrapIcon(self.iconFileName)
|
||||
|
||||
IconTextPushButton.setIcon(self, icon)
|
||||
|
||||
def setIcon(self, icon: AppQIcon):
|
||||
"""Store an application icon and apply its themed variant."""
|
||||
self.iconFileName = AppQPushButton.getIconFileName(icon.iconFileName)
|
||||
|
||||
if not self.iconFileName:
|
||||
IconTextPushButton.setIcon(self, icon)
|
||||
else:
|
||||
self.setIconByTheme(APP().theme())
|
||||
|
||||
def themeChangedCallback(self, theme):
|
||||
"""Refresh the icon after an application theme change."""
|
||||
self.setIconByTheme(theme)
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated button text."""
|
||||
self.setText(_(self.text()))
|
||||
|
||||
|
||||
class AppQSpinBox(QSpinBox):
|
||||
"""Represent app q spin box."""
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ from .QtWidgets import (
|
||||
AppQDialogButtonBox,
|
||||
AppQGroupBox,
|
||||
AppQHeaderView,
|
||||
AppQIconTextPushButton,
|
||||
AppQLabel,
|
||||
AppQLineEdit,
|
||||
AppQListWidget,
|
||||
@@ -70,6 +71,7 @@ from .QtWidgets import (
|
||||
AppQTableWidget,
|
||||
AppQTabWidget,
|
||||
AppQToolBar,
|
||||
IconTextPushButton,
|
||||
MBoxDirectRulesNotAllowed,
|
||||
MBoxNewChangesNextTime,
|
||||
MBoxQuestionDelete,
|
||||
@@ -105,6 +107,7 @@ __all__ = [
|
||||
'AppQDialogButtonBox',
|
||||
'AppQGroupBox',
|
||||
'AppQHeaderView',
|
||||
'AppQIconTextPushButton',
|
||||
'AppQIcon',
|
||||
'AppQLabel',
|
||||
'AppQLineEdit',
|
||||
@@ -140,6 +143,7 @@ __all__ = [
|
||||
'GuiEditorWidgetQDialog',
|
||||
'GuiEditorWidgetQGroupBox',
|
||||
'GuiEditorWidgetQWidget',
|
||||
'IconTextPushButton',
|
||||
'LANGUAGE_TO_ABBR',
|
||||
'MBoxDirectRulesNotAllowed',
|
||||
'MBoxNewChangesNextTime',
|
||||
|
||||
@@ -20,7 +20,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import APP, Mixins
|
||||
from Furious.Qt import AppStyleSheet, bootstrapIcon, bootstrapIconWhite
|
||||
from Furious.Qt import (
|
||||
AppStyleSheet,
|
||||
IconTextPushButton,
|
||||
bootstrapIcon,
|
||||
bootstrapIconWhite,
|
||||
)
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
from PySide6 import QtCore
|
||||
@@ -31,6 +36,28 @@ from dataclasses import dataclass
|
||||
__all__ = ['NavigationView']
|
||||
|
||||
|
||||
class _NavigationButton(IconTextPushButton):
|
||||
"""Present one navigation item with layout-managed icon spacing."""
|
||||
|
||||
IconSize = QtCore.QSize(20, 20)
|
||||
IconTextSpacing = 12
|
||||
HorizontalMargin = 12
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize the icon and text presentation."""
|
||||
super().__init__(
|
||||
parent,
|
||||
iconTextSpacing=self.IconTextSpacing,
|
||||
horizontalMargin=self.HorizontalMargin,
|
||||
verticalMargin=0,
|
||||
iconSize=self.IconSize,
|
||||
)
|
||||
|
||||
def setExpanded(self, expanded: bool):
|
||||
"""Show the text label only in expanded navigation mode."""
|
||||
self.setTextVisible(expanded)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _NavigationPage:
|
||||
"""Describe one registered page and its navigation control."""
|
||||
@@ -39,7 +66,7 @@ class _NavigationPage:
|
||||
title: str
|
||||
iconFileName: str
|
||||
widget: QWidget
|
||||
button: QToolButton
|
||||
button: _NavigationButton
|
||||
translatable: bool = True
|
||||
|
||||
|
||||
@@ -51,7 +78,8 @@ class NavigationView(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
|
||||
CollapsedWidth = 56
|
||||
ExpandedWidth = 220
|
||||
IconSize = QtCore.QSize(20, 20)
|
||||
IconSize = _NavigationButton.IconSize
|
||||
AnimationDuration = 150
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize an empty page registry and its navigation controls."""
|
||||
@@ -60,16 +88,17 @@ class NavigationView(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
self._pages: dict[str, _NavigationPage] = {}
|
||||
self._currentPageId = ''
|
||||
self._expanded = False
|
||||
self._navigationWidth = self.CollapsedWidth
|
||||
|
||||
self.setObjectName('NavigationView')
|
||||
|
||||
self.navigationPanel = QFrame(parent=self)
|
||||
self.navigationPanel.setObjectName('NavigationPanel')
|
||||
|
||||
self.toggleButton = QToolButton(parent=self.navigationPanel)
|
||||
self.toggleButton = _NavigationButton(parent=self.navigationPanel)
|
||||
self.toggleButton.setObjectName('NavigationToggleButton')
|
||||
self.toggleButton.setExpanded(False)
|
||||
self.toggleButton.setToolTip(_('Expand Navigation'))
|
||||
self.toggleButton.setIconSize(self.IconSize)
|
||||
self.toggleButton.clicked.connect(self.toggleExpanded)
|
||||
|
||||
self.navigationLayout = QVBoxLayout(self.navigationPanel)
|
||||
@@ -95,7 +124,16 @@ class NavigationView(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
self._layout.addWidget(self.navigationPanel)
|
||||
self._layout.addWidget(self.pageStack, 1)
|
||||
|
||||
self.setExpanded(False)
|
||||
self._widthAnimation = QtCore.QPropertyAnimation(
|
||||
self,
|
||||
b'navigationWidth',
|
||||
parent=self,
|
||||
)
|
||||
self._widthAnimation.setDuration(self.AnimationDuration)
|
||||
self._widthAnimation.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad)
|
||||
self._widthAnimation.finished.connect(self._widthAnimationFinished)
|
||||
|
||||
self.setExpanded(False, animated=False)
|
||||
self.setIconByTheme(APP().theme())
|
||||
|
||||
def addPage(
|
||||
@@ -117,16 +155,11 @@ class NavigationView(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
if not isinstance(widget, QWidget):
|
||||
raise TypeError('widget must be a QWidget')
|
||||
|
||||
button = QToolButton(parent=self.navigationPanel)
|
||||
button = _NavigationButton(parent=self.navigationPanel)
|
||||
button.setObjectName('NavigationPageButton')
|
||||
button.setProperty('pageId', pageId)
|
||||
button.setCheckable(True)
|
||||
button.setIconSize(self.IconSize)
|
||||
button.setToolButtonStyle(
|
||||
QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon
|
||||
if self._expanded
|
||||
else QtCore.Qt.ToolButtonStyle.ToolButtonIconOnly
|
||||
)
|
||||
button.setExpanded(self._expanded)
|
||||
button.setSizePolicy(
|
||||
QSizePolicy.Policy.Expanding,
|
||||
QSizePolicy.Policy.Fixed,
|
||||
@@ -191,16 +224,13 @@ class NavigationView(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
"""Toggle between the compact rail and expanded navigation menu."""
|
||||
self.setExpanded(not self._expanded)
|
||||
|
||||
@QtCore.Slot(bool)
|
||||
def setExpanded(self, expanded: bool):
|
||||
"""Set the navigation menu expansion state."""
|
||||
def setExpanded(self, expanded: bool, *, animated: bool = True):
|
||||
"""Set expansion state, optionally animating the panel width."""
|
||||
expanded = bool(expanded)
|
||||
changed = expanded != self._expanded
|
||||
|
||||
self._expanded = expanded
|
||||
|
||||
self.navigationPanel.setFixedWidth(
|
||||
self.ExpandedWidth if expanded else self.CollapsedWidth
|
||||
)
|
||||
self.toggleButton.setToolTip(
|
||||
_('Collapse Navigation') if expanded else _('Expand Navigation')
|
||||
)
|
||||
@@ -208,19 +238,55 @@ class NavigationView(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
self._refreshWidgetStyle(self.toggleButton)
|
||||
self.setIconByTheme(APP().theme())
|
||||
|
||||
style = (
|
||||
QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon
|
||||
if expanded
|
||||
else QtCore.Qt.ToolButtonStyle.ToolButtonIconOnly
|
||||
)
|
||||
|
||||
for page in self._pages.values():
|
||||
page.button.setToolButtonStyle(style)
|
||||
if expanded:
|
||||
page.button.setExpanded(True)
|
||||
|
||||
self._updatePageText(page)
|
||||
|
||||
targetWidth = self.ExpandedWidth if expanded else self.CollapsedWidth
|
||||
|
||||
self._widthAnimation.stop()
|
||||
|
||||
if changed and animated:
|
||||
self._widthAnimation.setStartValue(self._navigationWidth)
|
||||
self._widthAnimation.setEndValue(targetWidth)
|
||||
self._widthAnimation.start()
|
||||
else:
|
||||
self._setNavigationWidth(targetWidth)
|
||||
|
||||
if not expanded:
|
||||
self._setPageButtonsExpanded(False)
|
||||
|
||||
if changed:
|
||||
self.expandedChanged.emit(expanded)
|
||||
|
||||
def _getNavigationWidth(self) -> int:
|
||||
"""Return the width exposed to the property animation."""
|
||||
return self._navigationWidth
|
||||
|
||||
def _setNavigationWidth(self, width: int):
|
||||
"""Apply an animated width to the navigation panel."""
|
||||
self._navigationWidth = int(width)
|
||||
self.navigationPanel.setFixedWidth(self._navigationWidth)
|
||||
|
||||
navigationWidth = QtCore.Property(
|
||||
int,
|
||||
_getNavigationWidth,
|
||||
_setNavigationWidth,
|
||||
)
|
||||
|
||||
def _setPageButtonsExpanded(self, expanded: bool):
|
||||
"""Set label visibility for all registered page buttons."""
|
||||
for page in self._pages.values():
|
||||
page.button.setExpanded(expanded)
|
||||
|
||||
@QtCore.Slot()
|
||||
def _widthAnimationFinished(self):
|
||||
"""Finish label compaction after a collapse animation."""
|
||||
if not self._expanded:
|
||||
self._setPageButtonsExpanded(False)
|
||||
|
||||
@staticmethod
|
||||
def _refreshWidgetStyle(widget: QWidget):
|
||||
"""Re-polish a widget after a dynamic property changes."""
|
||||
@@ -246,12 +312,8 @@ class NavigationView(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
def setIconByTheme(self, theme: str):
|
||||
"""Refresh navigation icons for the active theme."""
|
||||
iconFactory = self._iconFactory(theme)
|
||||
toggleIcon = (
|
||||
'layout-sidebar-inset-reverse.svg'
|
||||
if self._expanded
|
||||
else 'layout-sidebar-inset.svg'
|
||||
)
|
||||
self.toggleButton.setIcon(iconFactory(toggleIcon))
|
||||
|
||||
self.toggleButton.setIcon(iconFactory('list.svg'))
|
||||
|
||||
for page in self._pages.values():
|
||||
self._updatePageIcon(page, theme)
|
||||
|
||||
@@ -370,24 +370,24 @@ class ConnectionStatusWidget(QWidget):
|
||||
self._layout.addWidget(self.networkState)
|
||||
|
||||
|
||||
class SearchButton(AppQPushButton):
|
||||
class SearchButton(AppQIconTextPushButton):
|
||||
"""Represent search button."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the SearchButton."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setText(self.customText())
|
||||
self.setObjectName('SearchButton')
|
||||
self.setSizePolicy(
|
||||
QSizePolicy.Policy.Fixed,
|
||||
QSizePolicy.Policy.Fixed,
|
||||
)
|
||||
self.setText(_('Search'))
|
||||
self.setIcon(bootstrapIcon('search.svg'))
|
||||
|
||||
@staticmethod
|
||||
def customText():
|
||||
"""Return the user-facing message text for the search button."""
|
||||
return ' ' * 2 + _('Search')
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the search button."""
|
||||
self.setText(self.customText())
|
||||
self.setText(_('Search'))
|
||||
|
||||
|
||||
class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
|
||||
@@ -73,19 +73,20 @@ class MainWindow(AppQMainWindow):
|
||||
'home',
|
||||
self.homePage,
|
||||
'Home',
|
||||
'house.svg',
|
||||
'house-door.svg',
|
||||
)
|
||||
self.navigationView.addPage(
|
||||
'log',
|
||||
self.logPage,
|
||||
'Log',
|
||||
'terminal.svg',
|
||||
'pin-angle.svg',
|
||||
)
|
||||
self.navigationView.pageChanged.connect(self._pageChanged)
|
||||
self.navigationView.expandedChanged.connect(self._navigationExpandedChanged)
|
||||
|
||||
self.navigationView.setExpanded(
|
||||
AppSettings.isStateON_('AppMainWindowNavigationExpanded')
|
||||
AppSettings.isStateON_('AppMainWindowNavigationExpanded'),
|
||||
animated=False,
|
||||
)
|
||||
self.navigationView.setCurrentPage(
|
||||
str(AppSettings.get('AppMainWindowSelectedPage'))
|
||||
|
||||
Reference in New Issue
Block a user