# coding:utf-8 from typing import Union import sys from PySide6.QtCore import Qt, QSize, QRect from PySide6.QtGui import QIcon, QPainter, QColor from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QLabel, QApplication from ..common.config import qconfig from ..common.icon import FluentIconBase from ..common.router import qrouter from ..common.style_sheet import FluentStyleSheet, isDarkTheme, setTheme, Theme from ..common.animation import BackgroundAnimationWidget from ..components.widgets.frameless_window import FramelessWindow from ..components.navigation import (NavigationInterface, NavigationBar, NavigationItemPosition, NavigationBarPushButton, NavigationTreeWidget) from .stacked_widget import StackedWidget from qframelesswindow import TitleBar, TitleBarBase class FluentWindowBase(BackgroundAnimationWidget, FramelessWindow): """ Fluent window base class """ def __init__(self, parent=None): self._isMicaEnabled = False self._lightBackgroundColor = QColor(240, 244, 249) self._darkBackgroundColor = QColor(32, 32, 32) super().__init__(parent=parent) self.hBoxLayout = QHBoxLayout(self) self.stackedWidget = StackedWidget(self) self.navigationInterface = None # initialize layout self.hBoxLayout.setSpacing(0) self.hBoxLayout.setContentsMargins(0, 0, 0, 0) FluentStyleSheet.FLUENT_WINDOW.apply(self.stackedWidget) # enable mica effect on win11 self.setMicaEffectEnabled(True) # show system title bar buttons on macOS if sys.platform == "darwin": self.setSystemTitleBarButtonVisible(True) qconfig.themeChangedFinished.connect(self._onThemeChangedFinished) def addSubInterface(self, interface: QWidget, icon: Union[FluentIconBase, QIcon, str], text: str, position=NavigationItemPosition.TOP): """ add sub interface """ raise NotImplementedError def removeInterface(self, interface: QWidget, isDelete=False): """ remove sub interface Parameters ---------- interface: QWidget sub interface to be removed isDelete: bool whether to delete the sub interface """ raise NotImplementedError def switchTo(self, interface: QWidget): self.stackedWidget.setCurrentWidget(interface, popOut=False) def _onCurrentInterfaceChanged(self, index: int): widget = self.stackedWidget.widget(index) self.navigationInterface.setCurrentItem(widget.objectName()) qrouter.push(self.stackedWidget, widget.objectName()) self._updateStackedBackground() def _updateStackedBackground(self): isTransparent = self.stackedWidget.currentWidget().property("isStackedTransparent") if bool(self.stackedWidget.property("isTransparent")) == isTransparent: return self.stackedWidget.setProperty("isTransparent", isTransparent) self.stackedWidget.setStyle(QApplication.style()) def setCustomBackgroundColor(self, light, dark): """ set custom background color Parameters ---------- light, dark: QColor | Qt.GlobalColor | str background color in light/dark theme mode """ self._lightBackgroundColor = QColor(light) self._darkBackgroundColor = QColor(dark) self._updateBackgroundColor() def _normalBackgroundColor(self): if not self.isMicaEffectEnabled(): return self._darkBackgroundColor if isDarkTheme() else self._lightBackgroundColor return QColor(0, 0, 0, 0) def _onThemeChangedFinished(self): if self.isMicaEffectEnabled(): self.windowEffect.setMicaEffect(self.winId(), isDarkTheme()) def paintEvent(self, e): super().paintEvent(e) painter = QPainter(self) painter.setPen(Qt.NoPen) painter.setBrush(self.backgroundColor) painter.drawRect(self.rect()) def setMicaEffectEnabled(self, isEnabled: bool): """ set whether the mica effect is enabled, only available on Win11 """ if sys.platform != 'win32' or sys.getwindowsversion().build < 22000: return self._isMicaEnabled = isEnabled if isEnabled: self.windowEffect.setMicaEffect(self.winId(), isDarkTheme()) else: self.windowEffect.removeBackgroundEffect(self.winId()) self.setBackgroundColor(self._normalBackgroundColor()) def isMicaEffectEnabled(self): return self._isMicaEnabled def systemTitleBarRect(self, size: QSize) -> QRect: """ Returns the system title bar rect, only works for macOS Parameters ---------- size: QSize original system title bar rect """ return QRect(size.width() - 75, 0 if self.isFullScreen() else 9, 75, size.height()) def setTitleBar(self, titleBar): super().setTitleBar(titleBar) # hide title bar buttons on macOS if sys.platform == "darwin" and self.isSystemButtonVisible() and isinstance(titleBar, TitleBarBase): titleBar.minBtn.hide() titleBar.maxBtn.hide() titleBar.closeBtn.hide() class FluentTitleBar(TitleBar): """ Fluent title bar""" def __init__(self, parent): super().__init__(parent) self.setFixedHeight(48) self.hBoxLayout.removeWidget(self.minBtn) self.hBoxLayout.removeWidget(self.maxBtn) self.hBoxLayout.removeWidget(self.closeBtn) # add window icon self.iconLabel = QLabel(self) self.iconLabel.setFixedSize(18, 18) self.hBoxLayout.insertWidget(0, self.iconLabel, 0, Qt.AlignLeft | Qt.AlignVCenter) self.window().windowIconChanged.connect(self.setIcon) # add title label self.titleLabel = QLabel(self) self.hBoxLayout.insertWidget(1, self.titleLabel, 0, Qt.AlignLeft | Qt.AlignVCenter) self.titleLabel.setObjectName('titleLabel') self.window().windowTitleChanged.connect(self.setTitle) self.vBoxLayout = QVBoxLayout() self.buttonLayout = QHBoxLayout() self.buttonLayout.setSpacing(0) self.buttonLayout.setContentsMargins(0, 0, 0, 0) self.buttonLayout.setAlignment(Qt.AlignTop) self.buttonLayout.addWidget(self.minBtn) self.buttonLayout.addWidget(self.maxBtn) self.buttonLayout.addWidget(self.closeBtn) self.vBoxLayout.addLayout(self.buttonLayout) self.vBoxLayout.addStretch(1) self.hBoxLayout.addLayout(self.vBoxLayout, 0) FluentStyleSheet.FLUENT_WINDOW.apply(self) def setTitle(self, title): self.titleLabel.setText(title) self.titleLabel.adjustSize() def setIcon(self, icon): self.iconLabel.setPixmap(QIcon(icon).pixmap(18, 18)) class FluentWindow(FluentWindowBase): """ Fluent window """ def __init__(self, parent=None): super().__init__(parent) self.setTitleBar(FluentTitleBar(self)) self.navigationInterface = NavigationInterface(self, showReturnButton=True) self.widgetLayout = QHBoxLayout() # initialize layout self.hBoxLayout.addWidget(self.navigationInterface) self.hBoxLayout.addLayout(self.widgetLayout) self.hBoxLayout.setStretchFactor(self.widgetLayout, 1) self.widgetLayout.addWidget(self.stackedWidget) self.widgetLayout.setContentsMargins(0, 48, 0, 0) self.navigationInterface.displayModeChanged.connect(self.titleBar.raise_) self.titleBar.raise_() def addSubInterface(self, interface: QWidget, icon: Union[FluentIconBase, QIcon, str], text: str, position=NavigationItemPosition.TOP, parent=None, isTransparent=False) -> NavigationTreeWidget: """ add sub interface, the object name of `interface` should be set already before calling this method Parameters ---------- interface: QWidget the subinterface to be added icon: FluentIconBase | QIcon | str the icon of navigation item text: str the text of navigation item position: NavigationItemPosition the position of navigation item parent: QWidget the parent of navigation item isTransparent: bool whether to use transparent background """ if not interface.objectName(): raise ValueError("The object name of `interface` can't be empty string.") if parent and not parent.objectName(): raise ValueError("The object name of `parent` can't be empty string.") interface.setProperty("isStackedTransparent", isTransparent) self.stackedWidget.addWidget(interface) # add navigation item routeKey = interface.objectName() item = self.navigationInterface.addItem( routeKey=routeKey, icon=icon, text=text, onClick=lambda: self.switchTo(interface), position=position, tooltip=text, parentRouteKey=parent.objectName() if parent else None ) # initialize selected item if self.stackedWidget.count() == 1: self.stackedWidget.currentChanged.connect(self._onCurrentInterfaceChanged) self.navigationInterface.setCurrentItem(routeKey) qrouter.setDefaultRouteKey(self.stackedWidget, routeKey) self._updateStackedBackground() return item def removeInterface(self, interface, isDelete=False): self.navigationInterface.removeWidget(interface.objectName()) self.stackedWidget.removeWidget(interface) interface.hide() if isDelete: interface.deleteLater() def resizeEvent(self, e): self.titleBar.move(46, 0) self.titleBar.resize(self.width()-46, self.titleBar.height()) class MSFluentTitleBar(FluentTitleBar): def __init__(self, parent): super().__init__(parent) self.hBoxLayout.insertSpacing(0, 20) self.hBoxLayout.insertSpacing(2, 2) class MSFluentWindow(FluentWindowBase): """ Fluent window in Microsoft Store style """ def __init__(self, parent=None): super().__init__(parent) self.setTitleBar(MSFluentTitleBar(self)) self.navigationInterface = NavigationBar(self) # initialize layout self.hBoxLayout.setContentsMargins(0, 48, 0, 0) self.hBoxLayout.addWidget(self.navigationInterface) self.hBoxLayout.addWidget(self.stackedWidget, 1) self.titleBar.raise_() self.titleBar.setAttribute(Qt.WA_StyledBackground) def addSubInterface(self, interface: QWidget, icon: Union[FluentIconBase, QIcon, str], text: str, selectedIcon=None, position=NavigationItemPosition.TOP, isTransparent=False) -> NavigationBarPushButton: """ add sub interface, the object name of `interface` should be set already before calling this method Parameters ---------- interface: QWidget the subinterface to be added icon: FluentIconBase | QIcon | str the icon of navigation item text: str the text of navigation item selectedIcon: str | QIcon | FluentIconBase the icon of navigation item in selected state position: NavigationItemPosition the position of navigation item """ if not interface.objectName(): raise ValueError("The object name of `interface` can't be empty string.") interface.setProperty("isStackedTransparent", isTransparent) self.stackedWidget.addWidget(interface) # add navigation item routeKey = interface.objectName() item = self.navigationInterface.addItem( routeKey=routeKey, icon=icon, text=text, onClick=lambda: self.switchTo(interface), selectedIcon=selectedIcon, position=position ) if self.stackedWidget.count() == 1: self.stackedWidget.currentChanged.connect(self._onCurrentInterfaceChanged) self.navigationInterface.setCurrentItem(routeKey) qrouter.setDefaultRouteKey(self.stackedWidget, routeKey) self._updateStackedBackground() return item def removeInterface(self, interface, isDelete=False): self.navigationInterface.removeWidget(interface.objectName()) self.stackedWidget.removeWidget(interface) interface.hide() if isDelete: interface.deleteLater() class SplitTitleBar(TitleBar): def __init__(self, parent): super().__init__(parent) # add window icon self.iconLabel = QLabel(self) self.iconLabel.setFixedSize(18, 18) self.hBoxLayout.insertSpacing(0, 12) self.hBoxLayout.insertWidget(1, self.iconLabel, 0, Qt.AlignLeft | Qt.AlignBottom) self.window().windowIconChanged.connect(self.setIcon) # add title label self.titleLabel = QLabel(self) self.hBoxLayout.insertWidget(2, self.titleLabel, 0, Qt.AlignLeft | Qt.AlignBottom) self.titleLabel.setObjectName('titleLabel') self.window().windowTitleChanged.connect(self.setTitle) FluentStyleSheet.FLUENT_WINDOW.apply(self) def setTitle(self, title): self.titleLabel.setText(title) self.titleLabel.adjustSize() def setIcon(self, icon): self.iconLabel.setPixmap(QIcon(icon).pixmap(18, 18)) class SplitFluentWindow(FluentWindow): """ Fluent window with split style """ def __init__(self, parent=None): super().__init__(parent) self.setTitleBar(SplitTitleBar(self)) if sys.platform == "darwin": self.titleBar.setFixedHeight(48) self.widgetLayout.setContentsMargins(0, 0, 0, 0) self.titleBar.raise_() self.navigationInterface.displayModeChanged.connect(self.titleBar.raise_) class FluentBackgroundTheme: """ Fluent background theme """ DEFAULT = (QColor(243, 243, 243), QColor(32, 32, 32)) # light, dark DEFAULT_BLUE = (QColor(240, 244, 249), QColor(25, 33, 42))