Compare commits

..

2 Commits

Author SHA1 Message Date
ac0d4fec8a
wip: navigation system - NavItemWidgets 2023-11-23 00:42:34 +08:00
7821816d09
wip: navigation components 2023-11-18 23:05:10 +08:00
11 changed files with 207 additions and 15 deletions

View File

@ -2,3 +2,4 @@ black == 23.7.0
isort == 5.12.0 isort == 5.12.0
imageio==2.31.4 imageio==2.31.4
Nuitka==1.8.4 Nuitka==1.8.4
pytest==7.4.3

0
tests/__init__.py Normal file
View File

0
tests/ui/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,31 @@
from ui.navigation.navhost import NavHost
from ui.navigation.navitem import NavItem
class TestNavHost:
def test_auto_append_parent(self):
navHost = NavHost()
navHost.registerNavItem(NavItem(id="aaa.bbb.ccc.ddd"))
navItems = navHost.navItems
assert NavItem(id="aaa.bbb.ccc.ddd") in navItems
assert NavItem(id="aaa.bbb.ccc") in navItems
assert NavItem(id="aaa.bbb") in navItems
assert NavItem(id="aaa") in navItems
def test_auto_select_child(self):
navHost = NavHost()
navHost.registerNavItem(NavItem(id="aaa"))
navHost.registerNavItem(NavItem(id="bbb"))
assert navHost.currentNavItem.id == "aaa"
navHost.registerNavItem(NavItem(id="aaa.bbb"))
navHost.registerNavItem(NavItem(id="aaa.ccc"))
navHost.navigate("aaa")
assert navHost.currentNavItem.id == "aaa.bbb"

View File

View File

@ -0,0 +1,47 @@
from PySide6.QtWidgets import QWidget
from ui.navigation.navhost import NavHost, navHost
from ui.navigation.navitem import NavItem
from ui.navigation.navitemwidgets import NavItemWidgets
from ui.widgets.slidingstackedwidget import SlidingStackedWidget
class AnimatedStackedNavItemsWidgets(SlidingStackedWidget):
def __init__(
self, navItemWidgets: NavItemWidgets, navHost: NavHost = navHost, parent=None
):
super().__init__(parent)
self.navItemWidgets = navItemWidgets
self.navHost = navHost
self.navHost.activated.connect(self.__switchTo)
self.animationFinished.connect(self.endChangingWidget)
def __switchTo(self, oldNavItem: NavItem, newNavItem: NavItem):
oldNavItemDepth = self.navHost.getNavItemDepth(oldNavItem.id)
newNavItemDepth = self.navHost.getNavItemDepth(newNavItem.id)
if oldNavItemDepth != newNavItemDepth:
slidingDirection = (
self.slidingDirection.RightToLeft
if newNavItemDepth > oldNavItemDepth
else self.slidingDirection.LeftToRight
)
else:
slidingDirection = self.slidingDirection.TopToBottom
newWidget = self.navItemWidgets.get(newNavItem.id) or QWidget()
self.startChangingWidget(newWidget, slidingDirection)
def startChangingWidget(self, newWidget: QWidget, slidingDirection):
newIndex = self.addWidget(newWidget)
[self.widget(i).setEnabled(False) for i in range(self.count())]
self.slideInIdx(newIndex, slidingDirection)
def endChangingWidget(self):
oldWidget = self.widget(0)
self.removeWidget(oldWidget)
newWidget = self.widget(0)
newWidget.setEnabled(True)

View File

@ -24,11 +24,23 @@ class NavHost(QObject):
super().__init__(parent) super().__init__(parent)
self.__navItems: list[NavItem] = [] self.__navItems: list[NavItem] = []
self.__cachedNavItems: list[NavItem] = []
self.__currentNavItem: NavItem = None self.__currentNavItem: NavItem = None
def __flushCachedNavItems(self):
navItems = set(self.__navItems)
for item in self.__navItems:
parts = item.id.split(".")
for i in range(1, len(parts)):
parentItemId = ".".join(parts[:i])
navItems.add(NavItem(id=parentItemId))
self.__cachedNavItems = list(navItems)
@property @property
def navItems(self) -> list[NavItem]: def navItems(self) -> list[NavItem]:
return self.__navItems return self.__cachedNavItems
@property @property
def currentNavItem(self) -> NavItem: def currentNavItem(self) -> NavItem:
@ -101,6 +113,7 @@ class NavHost(QObject):
def registerNavItem(self, item: NavItem): def registerNavItem(self, item: NavItem):
self.__navItems.append(item) self.__navItems.append(item)
self.__flushCachedNavItems()
self.navItemsChanged.emit() self.navItemsChanged.emit()
def navigate(self, navItemId: str): def navigate(self, navItemId: str):

View File

@ -12,3 +12,6 @@ class NavItem:
def text(self): def text(self):
return QCoreApplication.translate("NavItem", f"{self.id}.title") return QCoreApplication.translate("NavItem", f"{self.id}.title")
def __hash__(self):
return hash(self.id)

View File

@ -0,0 +1,84 @@
from PySide6.QtCore import QObject
from PySide6.QtGui import QFont
from PySide6.QtWidgets import QLabel, QSizePolicy, QSpacerItem, QVBoxLayout, QWidget
from ui.navigation.navhost import NavHost, navHost
from ui.navigation.navitem import NavItem
from ui.navigation.navsidebar import NavigationSideBar
class DefaultParentNavItemWidget(QWidget):
def __init__(self, navItem: NavItem, navItemChildren: list[NavItem], parent=None):
super().__init__(parent)
self.navItem = navItem
self.partialNavHost = NavHost(self)
self.partialNavHost.registerNavItem(navItem)
for _navItem in navItemChildren:
self.partialNavHost.registerNavItem(_navItem)
self.partialNavHost.navigate(navItem.id)
self.verticalLayout = QVBoxLayout(self)
self.navItemLabelFont = QFont(self.font())
self.navItemLabelFont.setPointSize(14)
self.navItemLabel = QLabel(self)
self.navItemLabel.setFont(self.navItemLabelFont)
spacer = QSpacerItem(
20, 20, QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding
)
self.verticalLayout.addSpacerItem(spacer)
self.navSideBar = NavigationSideBar(self, self.partialNavHost)
self.navSideBar.navigateUpButton.setEnabled(False)
self.verticalLayout.addWidget(self.navSideBar)
self.verticalLayout.addSpacerItem(spacer)
self.retranslateUi()
def retranslateUi(self):
self.navItemLabel.setText(self.navItem.text())
class NavItemWidgets(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self.__map: dict[str, QWidget] = {}
# this reference holds all the `DefaultParentNavItemWidget`s
# since these widgets are created with no parents, not keeping a reference to
# them may result in errors, even a silent app crash that is hard to debug
self.__defaultParentWidgetRefs: dict[str, QWidget] = {}
def register(self, navItemId: str, widget: QWidget):
self.__map[navItemId] = widget
def unregister(self, navItemId: str) -> bool:
try:
widget = self.__map.pop(navItemId)
widget.deleteLater()
return True
except KeyError:
return False
def get(self, navItemId: str) -> QWidget | None:
widget = self.__map.get(navItemId)
if widget is not None:
return widget
elif navItemChildren := navHost.getNavItemRelatives(navItemId).children:
if self.__defaultParentWidgetRefs.get(navItemId) is None:
defaultParentNavItemWidget = DefaultParentNavItemWidget(
navHost.findNavItem(navItemId), navItemChildren
)
self.__defaultParentWidgetRefs[navItemId] = defaultParentNavItemWidget
defaultParentNavItemWidget.partialNavHost.activated.connect(
lambda o, n: navHost.navigate(n.id)
)
return self.__defaultParentWidgetRefs.get(navItemId)
else:
return None

View File

@ -1,4 +1,4 @@
from PySide6.QtCore import QModelIndex, Qt, Slot from PySide6.QtCore import QModelIndex, Qt, Signal, Slot
from PySide6.QtGui import QFont, QIcon, QKeySequence, QShortcut from PySide6.QtGui import QFont, QIcon, QKeySequence, QShortcut
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QListWidget, QListWidget,
@ -8,7 +8,7 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from ui.navigation.navhost import NavItem, navHost from ui.navigation.navhost import NavHost, NavItem, navHost
from ui.widgets.slidingstackedwidget import SlidingStackedWidget from ui.widgets.slidingstackedwidget import SlidingStackedWidget
@ -69,18 +69,14 @@ class NavigationWidget(QWidget):
class NavigationSideBar(QWidget): class NavigationSideBar(QWidget):
def __init__(self, parent=None): navItemActivated = Signal(NavItem)
def __init__(self, parent=None, navHost=navHost):
super().__init__(parent) super().__init__(parent)
self.navHost = navHost self.navHost = None
navHost.navItemsChanged.connect(self.reloadNavWidget)
navHost.activated.connect(self.navItemActivated)
self.navigateUpKeyboardShortcut = QShortcut( self.navigateUpKeyboardShortcut = QShortcut(
QKeySequence(Qt.Modifier.ALT | Qt.Key.Key_Left), QKeySequence(Qt.Modifier.ALT | Qt.Key.Key_Left), self, lambda: True
self,
self.navHost.navigateUp,
) )
self.verticalLayout = QVBoxLayout(self) self.verticalLayout = QVBoxLayout(self)
@ -88,7 +84,6 @@ class NavigationSideBar(QWidget):
self.navigateUpButton = QPushButton(QIcon(":/icons/back.svg"), "") self.navigateUpButton = QPushButton(QIcon(":/icons/back.svg"), "")
self.navigateUpButton.setFlat(True) self.navigateUpButton.setFlat(True)
self.navigateUpButton.setFixedHeight(20) self.navigateUpButton.setFixedHeight(20)
self.navigateUpButton.clicked.connect(self.navHost.navigateUp)
self.verticalLayout.addWidget(self.navigateUpButton) self.verticalLayout.addWidget(self.navigateUpButton)
self.slidingStackedWidget = SlidingStackedWidget(self) self.slidingStackedWidget = SlidingStackedWidget(self)
@ -101,11 +96,29 @@ class NavigationSideBar(QWidget):
navItemListWidget.activated.connect(self.navItemListWidgetActivatedProxy) navItemListWidget.activated.connect(self.navItemListWidgetActivatedProxy)
self.slidingStackedWidget.addWidget(navItemListWidget) self.slidingStackedWidget.addWidget(navItemListWidget)
self.setNavHost(navHost)
self.reloadNavWidget() self.reloadNavWidget()
def setNavHost(self, navHost: NavHost):
if self.navHost is not None:
self.navHost.navItemsChanged.disconnect(self.reloadNavWidget)
self.navHost.activated.disconnect(self.navItemChanged)
self.navigateUpKeyboardShortcut.activated.disconnect(
self.navHost.navigateUp
)
self.navigateUpButton.clicked.disconnect(self.navHost.navigateUp)
self.navHost = navHost
self.navHost.navItemsChanged.connect(self.reloadNavWidget)
self.navHost.activated.connect(self.navItemChanged)
self.navigateUpKeyboardShortcut.activated.connect(self.navHost.navigateUp)
self.navigateUpButton.clicked.connect(self.navHost.navigateUp)
@Slot(QModelIndex) @Slot(QModelIndex)
def navItemListWidgetActivatedProxy(self, index: QModelIndex): def navItemListWidgetActivatedProxy(self, index: QModelIndex):
self.navHost.navigate(index.data(NavItemListWidget.NavItemRole).id) self.navHost.navigate(index.data(NavItemListWidget.NavItemRole).id)
self.navItemActivated.emit(index.data(NavItemListWidget.NavItemRole))
def fillNavItemListWidget( def fillNavItemListWidget(
self, currentNavItem: NavItem, listWidget: NavItemListWidget self, currentNavItem: NavItem, listWidget: NavItemListWidget
@ -123,10 +136,10 @@ class NavigationSideBar(QWidget):
self.fillNavItemListWidget( self.fillNavItemListWidget(
self.navHost.currentNavItem, self.slidingStackedWidget.widget(0) self.navHost.currentNavItem, self.slidingStackedWidget.widget(0)
) )
self.navItemActivated(self.navHost.currentNavItem, self.navHost.currentNavItem) self.navItemChanged(self.navHost.currentNavItem, self.navHost.currentNavItem)
@Slot(NavItem, NavItem) @Slot(NavItem, NavItem)
def navItemActivated(self, oldNavItem: NavItem, newNavItem: NavItem): def navItemChanged(self, oldNavItem: NavItem, newNavItem: NavItem):
# update navigateUpButton text # update navigateUpButton text
if newNavItemParent := self.navHost.getNavItemRelatives(newNavItem.id).parent: if newNavItemParent := self.navHost.getNavItemRelatives(newNavItem.id).parent:
self.navigateUpButton.setText(newNavItemParent.text()) self.navigateUpButton.setText(newNavItemParent.text())