mirror of
https://github.com/283375/arcaea-offline-pyside-ui.git
synced 2025-07-01 04:16:26 +00:00
wip: navigating system
This commit is contained in:
0
ui/components/mainwindow/__init__.py
Normal file
0
ui/components/mainwindow/__init__.py
Normal file
177
ui/components/mainwindow/navsidebar.py
Normal file
177
ui/components/mainwindow/navsidebar.py
Normal file
@ -0,0 +1,177 @@
|
||||
from PySide6.QtCore import QModelIndex, Qt, Slot
|
||||
from PySide6.QtGui import QFont, QIcon, QKeySequence, QShortcut
|
||||
from PySide6.QtWidgets import (
|
||||
QListWidget,
|
||||
QListWidgetItem,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from ui.navigation.navhost import 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):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.navHost = navHost
|
||||
|
||||
navHost.navItemsChanged.connect(self.reloadNavWidget)
|
||||
navHost.activated.connect(self.navItemActivated)
|
||||
|
||||
self.navigateUpKeyboardShortcut = QShortcut(
|
||||
QKeySequence(Qt.Modifier.ALT | Qt.Key.Key_Left),
|
||||
self,
|
||||
self.navHost.navigateUp,
|
||||
)
|
||||
|
||||
self.verticalLayout = QVBoxLayout(self)
|
||||
|
||||
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)
|
||||
self.slidingStackedWidget.animationFinished.connect(
|
||||
self.endChangingNavItemListWidget
|
||||
)
|
||||
self.verticalLayout.addWidget(self.slidingStackedWidget)
|
||||
|
||||
navItemListWidget = NavItemListWidget(self)
|
||||
navItemListWidget.activated.connect(self.navItemListWidgetActivatedProxy)
|
||||
self.slidingStackedWidget.addWidget(navItemListWidget)
|
||||
|
||||
self.reloadNavWidget()
|
||||
|
||||
@Slot(QModelIndex)
|
||||
def navItemListWidgetActivatedProxy(self, index: QModelIndex):
|
||||
self.navHost.navigate(index.data(NavItemListWidget.NavItemRole).id)
|
||||
|
||||
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.navItemActivated(self.navHost.currentNavItem, self.navHost.currentNavItem)
|
||||
|
||||
@Slot(NavItem, NavItem)
|
||||
def navItemActivated(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)
|
0
ui/navigation/__init__.py
Normal file
0
ui/navigation/__init__.py
Normal file
138
ui/navigation/navhost.py
Normal file
138
ui/navigation/navhost.py
Normal file
@ -0,0 +1,138 @@
|
||||
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.__currentNavItem: NavItem = None
|
||||
|
||||
@property
|
||||
def navItems(self) -> list[NavItem]:
|
||||
return self.__navItems
|
||||
|
||||
@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.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()
|
14
ui/navigation/navitem.py
Normal file
14
ui/navigation/navitem.py
Normal file
@ -0,0 +1,14 @@
|
||||
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")
|
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>icons/back.svg</file>
|
||||
|
||||
<file>images/icon.png</file>
|
||||
<file>images/logo.png</file>
|
||||
<file>images/jacket-placeholder.png</file>
|
||||
|
0
ui/widgets/__init__.py
Normal file
0
ui/widgets/__init__.py
Normal file
@ -48,6 +48,8 @@ class SlidingDirection(IntEnum):
|
||||
|
||||
|
||||
class SlidingStackedWidget(QStackedWidget):
|
||||
slidingDirection = SlidingDirection
|
||||
|
||||
animationFinished = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
@ -58,10 +60,12 @@ class SlidingStackedWidget(QStackedWidget):
|
||||
self.animationEasingCurve = QEasingCurve.Type.OutQuart
|
||||
self.animationCurrentIndex = 0
|
||||
self.animationNextIndex = 0
|
||||
self.wrap = False
|
||||
self.animationCurrentPoint = QPoint(0, 0)
|
||||
self.animationRunning = False
|
||||
|
||||
self.wrap = False
|
||||
self.opacityAnimation = False
|
||||
|
||||
def setVertical(self, vertical: bool):
|
||||
self.vertical = vertical
|
||||
|
||||
@ -74,6 +78,9 @@ class SlidingStackedWidget(QStackedWidget):
|
||||
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):
|
||||
@ -166,16 +173,6 @@ class SlidingStackedWidget(QStackedWidget):
|
||||
QPoint(offsetX + currentPoint.x(), offsetY + currentPoint.y())
|
||||
)
|
||||
|
||||
currentWidgetOpacityEffect = QGraphicsOpacityEffect()
|
||||
currentWidgetOpacityEffectAnimation = self.widgetOpacityAnimation(
|
||||
currentIndex, currentWidgetOpacityEffect, 1, 0
|
||||
)
|
||||
|
||||
nextWidgetOpacityEffect = QGraphicsOpacityEffect()
|
||||
nextWidgetOpacityEffect.setOpacity(0)
|
||||
nextWidgetOpacityEffectAnimation = self.widgetOpacityAnimation(
|
||||
nextIndex, nextWidgetOpacityEffect, 0, 1
|
||||
)
|
||||
nextWidgetAnimation = self.widgetPosAnimation(nextIndex)
|
||||
nextWidgetAnimation.setStartValue(
|
||||
QPoint(-offsetX + nextPoint.x(), offsetY + nextPoint.y())
|
||||
@ -185,8 +182,21 @@ class SlidingStackedWidget(QStackedWidget):
|
||||
animationGroup = QParallelAnimationGroup(self)
|
||||
animationGroup.addAnimation(currentWidgetAnimation)
|
||||
animationGroup.addAnimation(nextWidgetAnimation)
|
||||
animationGroup.addAnimation(currentWidgetOpacityEffectAnimation)
|
||||
animationGroup.addAnimation(nextWidgetOpacityEffectAnimation)
|
||||
|
||||
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
|
||||
|
Reference in New Issue
Block a user