mirror of
https://github.com/283375/arcaea-offline-pyside-ui.git
synced 2025-07-01 20:36:26 +00:00
Compare commits
4 Commits
0c6f4f4961
...
ui-refacto
Author | SHA1 | Date | |
---|---|---|---|
ac0d4fec8a
|
|||
7821816d09
|
|||
a930cffe39
|
|||
4442e496aa
|
@ -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
|
||||||
|
@ -2,3 +2,4 @@ arcaea-offline==0.1.0
|
|||||||
arcaea-offline-ocr==0.1.0
|
arcaea-offline-ocr==0.1.0
|
||||||
exif==1.6.0
|
exif==1.6.0
|
||||||
PySide6==6.5.2
|
PySide6==6.5.2
|
||||||
|
typing-extensions==4.8.0
|
||||||
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
0
tests/ui/__init__.py
Normal file
0
tests/ui/__init__.py
Normal file
0
tests/ui/navigation/__init__.py
Normal file
0
tests/ui/navigation/__init__.py
Normal file
31
tests/ui/navigation/test_navhost.py
Normal file
31
tests/ui/navigation/test_navhost.py
Normal 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"
|
0
ui/components/__init__.py
Normal file
0
ui/components/__init__.py
Normal file
0
ui/components/mainwindow/__init__.py
Normal file
0
ui/components/mainwindow/__init__.py
Normal file
47
ui/components/mainwindow/animatedstackednavitemwidgets.py
Normal file
47
ui/components/mainwindow/animatedstackednavitemwidgets.py
Normal 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)
|
0
ui/navigation/__init__.py
Normal file
0
ui/navigation/__init__.py
Normal file
151
ui/navigation/navhost.py
Normal file
151
ui/navigation/navhost.py
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from PySide6.QtCore import QObject, Signal
|
||||||
|
|
||||||
|
from .navitem import NavItem
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NavItemRelatives:
|
||||||
|
parent: Optional[NavItem]
|
||||||
|
children: list[NavItem]
|
||||||
|
|
||||||
|
|
||||||
|
class NavHost(QObject):
|
||||||
|
activated = Signal(NavItem, NavItem)
|
||||||
|
|
||||||
|
navItemsChanged = Signal()
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
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.__cachedNavItems
|
||||||
|
|
||||||
|
@property
|
||||||
|
def currentNavItem(self) -> NavItem:
|
||||||
|
if self.__currentNavItem:
|
||||||
|
return self.__currentNavItem
|
||||||
|
|
||||||
|
if self.__navItems:
|
||||||
|
self.__currentNavItem = self.__navItems[0]
|
||||||
|
return self.__currentNavItem
|
||||||
|
|
||||||
|
def findNavItem(self, navItemId: str) -> Optional[NavItem]:
|
||||||
|
navItemIds = [item.id for item in self.__navItems]
|
||||||
|
try:
|
||||||
|
index = navItemIds.index(navItemId)
|
||||||
|
return self.__navItems[index]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def isNavigatingBack(self, oldNavItemId: str, newNavItemId: str) -> bool:
|
||||||
|
# sourcery skip: class-extract-method
|
||||||
|
# | oldNavItemId | newNavItemId | back? |
|
||||||
|
# |-----------------------|--------------------|-------|
|
||||||
|
# | database.manage.packs | database | True |
|
||||||
|
# | ocr.device | ocr | True |
|
||||||
|
# | database.manage.songs | ocr.b30 | False |
|
||||||
|
# | database | database | False |
|
||||||
|
|
||||||
|
oldNavItemIdSplitted = self.getNavItemIdSplitted(oldNavItemId)
|
||||||
|
newNavItemIdSplitted = self.getNavItemIdSplitted(newNavItemId)
|
||||||
|
|
||||||
|
return self.getNavItemDepth(newNavItemId) < self.getNavItemDepth(
|
||||||
|
oldNavItemId
|
||||||
|
) and all((idFrag in oldNavItemIdSplitted) for idFrag in newNavItemIdSplitted)
|
||||||
|
|
||||||
|
def isChild(self, childNavItemId: str, parentNavItemId: str) -> bool:
|
||||||
|
childNavItemIdSplitted = self.getNavItemIdSplitted(childNavItemId)
|
||||||
|
parentNavItemIdSplitted = self.getNavItemIdSplitted(parentNavItemId)
|
||||||
|
|
||||||
|
return all(
|
||||||
|
(idFrag in childNavItemIdSplitted) for idFrag in parentNavItemIdSplitted
|
||||||
|
)
|
||||||
|
|
||||||
|
def getNavItemIdSplitted(self, navItemId: str) -> list[str]:
|
||||||
|
return navItemId.split(".")
|
||||||
|
|
||||||
|
def getNavItemDepth(self, navItemId: str) -> int:
|
||||||
|
return len(self.getNavItemIdSplitted(navItemId))
|
||||||
|
|
||||||
|
def getNavItemRelatives(self, navItemId: str) -> NavItemRelatives:
|
||||||
|
parent = None
|
||||||
|
if "." in navItemId:
|
||||||
|
navItemIdSplitted = navItemId.split(".")
|
||||||
|
parentId = navItemIdSplitted[:-1]
|
||||||
|
parent = self.findNavItem(".".join(parentId))
|
||||||
|
|
||||||
|
if not navItemId:
|
||||||
|
# return root navItems
|
||||||
|
children = [navItem for navItem in self.__navItems if "." not in navItem.id]
|
||||||
|
else:
|
||||||
|
children = [
|
||||||
|
navItem
|
||||||
|
for navItem in self.__navItems
|
||||||
|
if navItem.id.startswith(navItemId)
|
||||||
|
and navItem.id != navItemId
|
||||||
|
and self.getNavItemDepth(navItem.id)
|
||||||
|
== self.getNavItemDepth(navItemId) + 1
|
||||||
|
]
|
||||||
|
|
||||||
|
return NavItemRelatives(parent=parent, children=children)
|
||||||
|
|
||||||
|
def registerNavItem(self, item: NavItem):
|
||||||
|
self.__navItems.append(item)
|
||||||
|
self.__flushCachedNavItems()
|
||||||
|
self.navItemsChanged.emit()
|
||||||
|
|
||||||
|
def navigate(self, navItemId: str):
|
||||||
|
oldNavItem = self.__currentNavItem or NavItem("")
|
||||||
|
newNavItem = self.findNavItem(navItemId)
|
||||||
|
|
||||||
|
if newNavItem is None:
|
||||||
|
raise IndexError(
|
||||||
|
f"Cannot find '{navItemId}' in {repr(self)}. "
|
||||||
|
"Maybe try registering it?"
|
||||||
|
)
|
||||||
|
|
||||||
|
# if the navItem have children, navigate to first child
|
||||||
|
# but if the navItem is going back, e.g. 'database.manage' -> 'database'
|
||||||
|
# then don't navigate to it's child.
|
||||||
|
if self.isNavigatingBack(oldNavItem.id, newNavItem.id):
|
||||||
|
# navItem is going back
|
||||||
|
currentNavItem = newNavItem
|
||||||
|
else:
|
||||||
|
newNavItemRelatives = self.getNavItemRelatives(newNavItem.id)
|
||||||
|
if newNavItemRelatives.children:
|
||||||
|
currentNavItem = newNavItemRelatives.children[0]
|
||||||
|
else:
|
||||||
|
currentNavItem = newNavItem
|
||||||
|
|
||||||
|
self.__currentNavItem = currentNavItem
|
||||||
|
self.activated.emit(oldNavItem, self.currentNavItem)
|
||||||
|
|
||||||
|
def navigateUp(self):
|
||||||
|
navItemRelatives = self.getNavItemRelatives(self.currentNavItem.id)
|
||||||
|
if navItemRelatives.parent:
|
||||||
|
self.navigate(navItemRelatives.parent.id)
|
||||||
|
|
||||||
|
|
||||||
|
navHost = NavHost()
|
17
ui/navigation/navitem.py
Normal file
17
ui/navigation/navitem.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from PySide6.QtCore import QCoreApplication
|
||||||
|
from PySide6.QtGui import QIcon, QPixmap
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NavItem:
|
||||||
|
id: str
|
||||||
|
icon: Optional[QIcon | QPixmap | str] = None
|
||||||
|
|
||||||
|
def text(self):
|
||||||
|
return QCoreApplication.translate("NavItem", f"{self.id}.title")
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.id)
|
84
ui/navigation/navitemwidgets.py
Normal file
84
ui/navigation/navitemwidgets.py
Normal 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
|
190
ui/navigation/navsidebar.py
Normal file
190
ui/navigation/navsidebar.py
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
from PySide6.QtCore import QModelIndex, Qt, Signal, Slot
|
||||||
|
from PySide6.QtGui import QFont, QIcon, QKeySequence, QShortcut
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QListWidget,
|
||||||
|
QListWidgetItem,
|
||||||
|
QPushButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ui.navigation.navhost import NavHost, NavItem, navHost
|
||||||
|
from ui.widgets.slidingstackedwidget import SlidingStackedWidget
|
||||||
|
|
||||||
|
|
||||||
|
class NavItemListWidget(QListWidget):
|
||||||
|
NavItemRole = Qt.ItemDataRole.UserRole
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
font = QFont(self.font())
|
||||||
|
font.setPointSize(14)
|
||||||
|
self.setFont(font)
|
||||||
|
|
||||||
|
self.clicked.connect(self.activated)
|
||||||
|
|
||||||
|
def setNavItems(self, items: list[NavItem]):
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
for navItem in items:
|
||||||
|
if navItem.icon:
|
||||||
|
listWidgetItem = QListWidgetItem(QIcon(navItem.icon), navItem.text())
|
||||||
|
else:
|
||||||
|
listWidgetItem = QListWidgetItem(navItem.text())
|
||||||
|
listWidgetItem.setData(self.NavItemRole, navItem)
|
||||||
|
listWidgetItem.setTextAlignment(
|
||||||
|
Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter
|
||||||
|
)
|
||||||
|
|
||||||
|
self.addItem(listWidgetItem)
|
||||||
|
|
||||||
|
def selectNavItem(self, navItemId: str):
|
||||||
|
navItemIds = [
|
||||||
|
self.item(r).data(self.NavItemRole).id for r in range(self.count())
|
||||||
|
]
|
||||||
|
index = navItemIds.index(navItemId)
|
||||||
|
self.setCurrentIndex(self.model().index(index, 0))
|
||||||
|
|
||||||
|
|
||||||
|
class NavigationWidget(QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.navHost = navHost
|
||||||
|
|
||||||
|
self.verticalLayout = QVBoxLayout(self)
|
||||||
|
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
self.backButton = QPushButton(QIcon(":/icons/back.svg"), "")
|
||||||
|
self.backButton.setFlat(True)
|
||||||
|
self.backButton.setFixedHeight(20)
|
||||||
|
self.verticalLayout.addWidget(self.backButton)
|
||||||
|
|
||||||
|
self.navListWidget = NavItemListWidget(self)
|
||||||
|
self.verticalLayout.addWidget(self.navListWidget)
|
||||||
|
|
||||||
|
def setNavigationItems(self, items: list[NavItem]):
|
||||||
|
self.navListWidget.setNavItems(items)
|
||||||
|
|
||||||
|
|
||||||
|
class NavigationSideBar(QWidget):
|
||||||
|
navItemActivated = Signal(NavItem)
|
||||||
|
|
||||||
|
def __init__(self, parent=None, navHost=navHost):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.navHost = None
|
||||||
|
self.navigateUpKeyboardShortcut = QShortcut(
|
||||||
|
QKeySequence(Qt.Modifier.ALT | Qt.Key.Key_Left), self, lambda: True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.verticalLayout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
self.navigateUpButton = QPushButton(QIcon(":/icons/back.svg"), "")
|
||||||
|
self.navigateUpButton.setFlat(True)
|
||||||
|
self.navigateUpButton.setFixedHeight(20)
|
||||||
|
self.verticalLayout.addWidget(self.navigateUpButton)
|
||||||
|
|
||||||
|
self.slidingStackedWidget = SlidingStackedWidget(self)
|
||||||
|
self.slidingStackedWidget.animationFinished.connect(
|
||||||
|
self.endChangingNavItemListWidget
|
||||||
|
)
|
||||||
|
self.verticalLayout.addWidget(self.slidingStackedWidget)
|
||||||
|
|
||||||
|
navItemListWidget = NavItemListWidget(self)
|
||||||
|
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
|
||||||
|
):
|
||||||
|
currentNavItemParent = self.navHost.getNavItemRelatives(
|
||||||
|
currentNavItem.id
|
||||||
|
).parent
|
||||||
|
currentNavItems = self.navHost.getNavItemRelatives(
|
||||||
|
currentNavItemParent.id if currentNavItemParent else ""
|
||||||
|
)
|
||||||
|
listWidget.setNavItems(currentNavItems.children)
|
||||||
|
listWidget.selectNavItem(currentNavItem.id)
|
||||||
|
|
||||||
|
def reloadNavWidget(self):
|
||||||
|
self.fillNavItemListWidget(
|
||||||
|
self.navHost.currentNavItem, self.slidingStackedWidget.widget(0)
|
||||||
|
)
|
||||||
|
self.navItemChanged(self.navHost.currentNavItem, self.navHost.currentNavItem)
|
||||||
|
|
||||||
|
@Slot(NavItem, NavItem)
|
||||||
|
def navItemChanged(self, oldNavItem: NavItem, newNavItem: NavItem):
|
||||||
|
# update navigateUpButton text
|
||||||
|
if newNavItemParent := self.navHost.getNavItemRelatives(newNavItem.id).parent:
|
||||||
|
self.navigateUpButton.setText(newNavItemParent.text())
|
||||||
|
else:
|
||||||
|
self.navigateUpButton.setText("Arcaea Offline")
|
||||||
|
|
||||||
|
# update navItemListWidget
|
||||||
|
oldNavItemIdSplitted = self.navHost.getNavItemIdSplitted(oldNavItem.id)
|
||||||
|
newNavItemIdSplitted = self.navHost.getNavItemIdSplitted(newNavItem.id)
|
||||||
|
|
||||||
|
oldNavItemDepth = len(oldNavItemIdSplitted)
|
||||||
|
newNavItemDepth = len(newNavItemIdSplitted)
|
||||||
|
|
||||||
|
if oldNavItemDepth != newNavItemDepth:
|
||||||
|
# navItem depth changed, replace current NavItemListWidget
|
||||||
|
newNavItemListWidget = NavItemListWidget(self)
|
||||||
|
slidingDirection = (
|
||||||
|
self.slidingStackedWidget.slidingDirection.RightToLeft
|
||||||
|
if newNavItemDepth > oldNavItemDepth
|
||||||
|
else self.slidingStackedWidget.slidingDirection.LeftToRight
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fillNavItemListWidget(newNavItem, newNavItemListWidget)
|
||||||
|
newNavItemListWidget.activated.connect(self.navItemListWidgetActivatedProxy)
|
||||||
|
|
||||||
|
self.startChangingNavItemListWidget(newNavItemListWidget, slidingDirection)
|
||||||
|
|
||||||
|
def startChangingNavItemListWidget(
|
||||||
|
self, newNavItemListWidget: NavItemListWidget, slidingDirection
|
||||||
|
):
|
||||||
|
newIndex = self.slidingStackedWidget.addWidget(newNavItemListWidget)
|
||||||
|
[
|
||||||
|
self.slidingStackedWidget.widget(i).setEnabled(False)
|
||||||
|
for i in range(self.slidingStackedWidget.count())
|
||||||
|
]
|
||||||
|
self.navigateUpButton.setEnabled(False)
|
||||||
|
self.navigateUpKeyboardShortcut.setEnabled(False)
|
||||||
|
self.slidingStackedWidget.slideInIdx(newIndex, slidingDirection)
|
||||||
|
|
||||||
|
def endChangingNavItemListWidget(self):
|
||||||
|
oldWidget = self.slidingStackedWidget.widget(0)
|
||||||
|
self.slidingStackedWidget.removeWidget(oldWidget)
|
||||||
|
oldWidget.deleteLater()
|
||||||
|
|
||||||
|
newWidget = self.slidingStackedWidget.widget(0)
|
||||||
|
newWidget.setEnabled(True)
|
||||||
|
self.navigateUpButton.setEnabled(True)
|
||||||
|
self.navigateUpKeyboardShortcut.setEnabled(True)
|
59
ui/resources/icons/back.svg
Normal file
59
ui/resources/icons/back.svg
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||||
|
sodipodi:docname="back.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="24.40625"
|
||||||
|
inkscape:cx="15.323944"
|
||||||
|
inkscape:cy="10.632522"
|
||||||
|
inkscape:current-layer="layer1">
|
||||||
|
<inkscape:grid
|
||||||
|
id="grid4"
|
||||||
|
units="px"
|
||||||
|
originx="12"
|
||||||
|
originy="12"
|
||||||
|
spacingx="1"
|
||||||
|
spacingy="1"
|
||||||
|
empcolor="#0099e5"
|
||||||
|
empopacity="0.30196078"
|
||||||
|
color="#0099e5"
|
||||||
|
opacity="0.14901961"
|
||||||
|
empspacing="5"
|
||||||
|
dotted="false"
|
||||||
|
gridanglex="30"
|
||||||
|
gridanglez="30"
|
||||||
|
visible="true" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="图层 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 17,2 7,12 17,22"
|
||||||
|
id="path2" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
<file>fonts/GeosansLight.ttf</file>
|
<file>fonts/GeosansLight.ttf</file>
|
||||||
|
|
||||||
|
<file>icons/back.svg</file>
|
||||||
|
|
||||||
<file>images/icon.png</file>
|
<file>images/icon.png</file>
|
||||||
<file>images/logo.png</file>
|
<file>images/logo.png</file>
|
||||||
<file>images/jacket-placeholder.png</file>
|
<file>images/jacket-placeholder.png</file>
|
||||||
|
0
ui/widgets/__init__.py
Normal file
0
ui/widgets/__init__.py
Normal file
231
ui/widgets/slidingstackedwidget.py
Normal file
231
ui/widgets/slidingstackedwidget.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
"""
|
||||||
|
Adapted from https://github.com/Qt-Widgets/SlidingStackedWidget-1
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
Copyright (c) 2020 Tim Schneeberger (ThePBone) <tim.schneeberger(at)outlook.de>
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
from PySide6.QtCore import (
|
||||||
|
QAbstractAnimation,
|
||||||
|
QEasingCurve,
|
||||||
|
QParallelAnimationGroup,
|
||||||
|
QPoint,
|
||||||
|
QPropertyAnimation,
|
||||||
|
Signal,
|
||||||
|
)
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QGraphicsEffect,
|
||||||
|
QGraphicsOpacityEffect,
|
||||||
|
QStackedWidget,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SlidingDirection(IntEnum):
|
||||||
|
Auto = 0
|
||||||
|
LeftToRight = 1
|
||||||
|
RightToLeft = 2
|
||||||
|
TopToBottom = 3
|
||||||
|
BottomToTop = 4
|
||||||
|
|
||||||
|
|
||||||
|
class SlidingStackedWidget(QStackedWidget):
|
||||||
|
slidingDirection = SlidingDirection
|
||||||
|
|
||||||
|
animationFinished = Signal()
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.vertical = False
|
||||||
|
self.speedMs = 300
|
||||||
|
self.animationEasingCurve = QEasingCurve.Type.OutQuart
|
||||||
|
self.animationCurrentIndex = 0
|
||||||
|
self.animationNextIndex = 0
|
||||||
|
self.animationCurrentPoint = QPoint(0, 0)
|
||||||
|
self.animationRunning = False
|
||||||
|
|
||||||
|
self.wrap = False
|
||||||
|
self.opacityAnimation = False
|
||||||
|
|
||||||
|
def setVertical(self, vertical: bool):
|
||||||
|
self.vertical = vertical
|
||||||
|
|
||||||
|
def setSpeedMs(self, speedMs: int):
|
||||||
|
self.speedMs = speedMs
|
||||||
|
|
||||||
|
def setAnimationEasingCurve(self, easingCurve: QEasingCurve.Type):
|
||||||
|
self.animationEasingCurve = easingCurve
|
||||||
|
|
||||||
|
def setWrap(self, wrap: bool):
|
||||||
|
self.wrap = wrap
|
||||||
|
|
||||||
|
def setOpacityAnimation(self, value: bool):
|
||||||
|
self.opacityAnimation = value
|
||||||
|
|
||||||
|
def slideInNext(self) -> bool:
|
||||||
|
currentIndex = self.currentIndex()
|
||||||
|
if self.wrap or (currentIndex < self.count() - 1):
|
||||||
|
self.slideInIdx(currentIndex + 1)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def slideInPrev(self) -> bool:
|
||||||
|
currentIndex = self.currentIndex()
|
||||||
|
if self.wrap or (currentIndex > 0):
|
||||||
|
self.slideInIdx(currentIndex - 1)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def slideInIdx(self, idx: int, direction: SlidingDirection = SlidingDirection.Auto):
|
||||||
|
if idx > self.count() - 1:
|
||||||
|
direction = (
|
||||||
|
SlidingDirection.TopToBottom
|
||||||
|
if self.vertical
|
||||||
|
else SlidingDirection.RightToLeft
|
||||||
|
)
|
||||||
|
idx %= self.count()
|
||||||
|
elif idx < 0:
|
||||||
|
direction = (
|
||||||
|
SlidingDirection.BottomToTop
|
||||||
|
if self.vertical
|
||||||
|
else SlidingDirection.LeftToRight
|
||||||
|
)
|
||||||
|
idx = (idx + self.count()) % self.count()
|
||||||
|
|
||||||
|
self.slideInWgt(self.widget(idx), direction)
|
||||||
|
|
||||||
|
def slideInWgt(self, newwidget: QWidget, direction: SlidingDirection):
|
||||||
|
if self.animationRunning:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.animationRunning = True
|
||||||
|
|
||||||
|
autoDirection = SlidingDirection.LeftToRight
|
||||||
|
|
||||||
|
currentIndex = self.currentIndex()
|
||||||
|
nextIndex = self.indexOf(newwidget)
|
||||||
|
if currentIndex == nextIndex:
|
||||||
|
self.animationRunning = False
|
||||||
|
return
|
||||||
|
elif currentIndex < nextIndex:
|
||||||
|
autoDirection = (
|
||||||
|
SlidingDirection.TopToBottom
|
||||||
|
if self.vertical
|
||||||
|
else SlidingDirection.RightToLeft
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
autoDirection = (
|
||||||
|
SlidingDirection.BottomToTop
|
||||||
|
if self.vertical
|
||||||
|
else SlidingDirection.LeftToRight
|
||||||
|
)
|
||||||
|
|
||||||
|
if direction == SlidingDirection.Auto:
|
||||||
|
direction = autoDirection
|
||||||
|
|
||||||
|
offsetX = self.frameRect().width()
|
||||||
|
offsetY = self.frameRect().height()
|
||||||
|
|
||||||
|
self.widget(nextIndex).setGeometry(0, 0, offsetX, offsetY)
|
||||||
|
if direction == SlidingDirection.BottomToTop:
|
||||||
|
offsetX = 0
|
||||||
|
offsetY = -offsetY
|
||||||
|
elif direction == SlidingDirection.TopToBottom:
|
||||||
|
offsetX = 0
|
||||||
|
elif direction == SlidingDirection.RightToLeft:
|
||||||
|
offsetX = -offsetX
|
||||||
|
offsetY = 0
|
||||||
|
elif direction == SlidingDirection.LeftToRight:
|
||||||
|
offsetY = 0
|
||||||
|
|
||||||
|
nextPoint = self.widget(nextIndex).pos()
|
||||||
|
currentPoint = self.widget(currentIndex).pos()
|
||||||
|
self.animationCurrentPoint = currentPoint
|
||||||
|
self.widget(nextIndex).move(nextPoint.x() - offsetX, nextPoint.y() - offsetY)
|
||||||
|
|
||||||
|
self.widget(nextIndex).show()
|
||||||
|
self.widget(nextIndex).raise_()
|
||||||
|
|
||||||
|
currentWidgetAnimation = self.widgetPosAnimation(currentIndex)
|
||||||
|
currentWidgetAnimation.setStartValue(QPoint(currentPoint.x(), currentPoint.y()))
|
||||||
|
currentWidgetAnimation.setEndValue(
|
||||||
|
QPoint(offsetX + currentPoint.x(), offsetY + currentPoint.y())
|
||||||
|
)
|
||||||
|
|
||||||
|
nextWidgetAnimation = self.widgetPosAnimation(nextIndex)
|
||||||
|
nextWidgetAnimation.setStartValue(
|
||||||
|
QPoint(-offsetX + nextPoint.x(), offsetY + nextPoint.y())
|
||||||
|
)
|
||||||
|
nextWidgetAnimation.setEndValue(QPoint(nextPoint.x(), nextPoint.y()))
|
||||||
|
|
||||||
|
animationGroup = QParallelAnimationGroup(self)
|
||||||
|
animationGroup.addAnimation(currentWidgetAnimation)
|
||||||
|
animationGroup.addAnimation(nextWidgetAnimation)
|
||||||
|
|
||||||
|
if self.opacityAnimation:
|
||||||
|
currentWidgetOpacityEffect = QGraphicsOpacityEffect()
|
||||||
|
currentWidgetOpacityEffectAnimation = self.widgetOpacityAnimation(
|
||||||
|
currentIndex, currentWidgetOpacityEffect, 1, 0
|
||||||
|
)
|
||||||
|
|
||||||
|
nextWidgetOpacityEffect = QGraphicsOpacityEffect()
|
||||||
|
nextWidgetOpacityEffect.setOpacity(0)
|
||||||
|
nextWidgetOpacityEffectAnimation = self.widgetOpacityAnimation(
|
||||||
|
nextIndex, nextWidgetOpacityEffect, 0, 1
|
||||||
|
)
|
||||||
|
|
||||||
|
animationGroup.addAnimation(currentWidgetOpacityEffectAnimation)
|
||||||
|
animationGroup.addAnimation(nextWidgetOpacityEffectAnimation)
|
||||||
|
|
||||||
|
animationGroup.finished.connect(self.animationDoneSlot)
|
||||||
|
self.animationNextIndex = nextIndex
|
||||||
|
self.animationCurrentIndex = currentIndex
|
||||||
|
self.animationRunning = True
|
||||||
|
animationGroup.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
|
|
||||||
|
def widgetPosAnimation(self, widgetIndex: int):
|
||||||
|
result = QPropertyAnimation(self.widget(widgetIndex), b"pos")
|
||||||
|
result.setDuration(self.speedMs)
|
||||||
|
result.setEasingCurve(self.animationEasingCurve)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def widgetOpacityAnimation(
|
||||||
|
self, widgetIndex: int, graphicEffect: QGraphicsEffect, startValue, endValue
|
||||||
|
):
|
||||||
|
self.widget(widgetIndex).setGraphicsEffect(graphicEffect)
|
||||||
|
result = QPropertyAnimation(graphicEffect, b"opacity")
|
||||||
|
result.setDuration(round(self.speedMs / 2))
|
||||||
|
result.setStartValue(startValue)
|
||||||
|
result.setEndValue(endValue)
|
||||||
|
result.finished.connect(
|
||||||
|
lambda: graphicEffect.deleteLater() if graphicEffect is not None else ...
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def animationDoneSlot(self):
|
||||||
|
self.setCurrentIndex(self.animationNextIndex)
|
||||||
|
self.widget(self.animationCurrentIndex).hide()
|
||||||
|
self.widget(self.animationCurrentIndex).move(self.animationCurrentPoint)
|
||||||
|
self.animationRunning = False
|
||||||
|
self.animationFinished.emit()
|
Reference in New Issue
Block a user