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
imageio==2.31.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)
self.__navItems: list[NavItem] = []
self.__cachedNavItems: list[NavItem] = []
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
def navItems(self) -> list[NavItem]:
return self.__navItems
return self.__cachedNavItems
@property
def currentNavItem(self) -> NavItem:
@ -101,6 +113,7 @@ class NavHost(QObject):
def registerNavItem(self, item: NavItem):
self.__navItems.append(item)
self.__flushCachedNavItems()
self.navItemsChanged.emit()
def navigate(self, navItemId: str):

View File

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