Music production session manager
https://www.laborejo.org
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
513 lines
22 KiB
513 lines
22 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2020, Nils Hilbricht, Germany ( https://www.hilbricht.net )
|
|
|
|
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ).
|
|
|
|
This application is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
import logging; logger = logging.getLogger(__name__); logger.info("import")
|
|
|
|
#Standard Library
|
|
from sys import argv as sysargv
|
|
from sys import exit as sysexit
|
|
|
|
#Third Party
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
logger.info(f"PyQt Version: {QtCore.PYQT_VERSION_STR}")
|
|
|
|
#Engine
|
|
from engine.config import METADATA #includes METADATA only. No other environmental setup is executed.
|
|
from engine.start import PATHS, qtApp
|
|
import engine.api as api #This loads the engine and starts a session.
|
|
|
|
#Qt
|
|
from .systemtray import SystemTray
|
|
from .eventloop import EventLoop
|
|
from .designer.mainwindow import Ui_MainWindow
|
|
from .helper import setPaletteAndFont
|
|
from .helper import iconFromString
|
|
from .sessiontreecontroller import SessionTreeController
|
|
from .opensessioncontroller import OpenSessionController
|
|
from .quicksessioncontroller import QuickSessionController
|
|
from .quickopensessioncontroller import QuickOpenSessionController
|
|
from .projectname import ProjectNameWidget
|
|
from .addclientprompt import askForExecutable, updateWordlist
|
|
from .waitdialog import WaitDialog
|
|
from .resources import *
|
|
from .settings import SettingsDialog
|
|
|
|
api.eventLoop = EventLoop()
|
|
|
|
#Setup the translator before classes are set up. Otherwise we can't use non-template translation.
|
|
#to test use LANGUAGE=de_DE.UTF-8 . not LANG=
|
|
language = QtCore.QLocale().languageToString(QtCore.QLocale().language())
|
|
logger.info("{}: Language set to {}".format(METADATA["name"], language))
|
|
if language in METADATA["supportedLanguages"]:
|
|
translator = QtCore.QTranslator()
|
|
translator.load(METADATA["supportedLanguages"][language], ":/translations/") #colon to make it a resource URL
|
|
qtApp.installTranslator(translator)
|
|
else:
|
|
"""silently fall back to English by doing nothing"""
|
|
|
|
|
|
def nothing(*args):
|
|
pass
|
|
|
|
class RecentlyOpenedSessions(object):
|
|
"""Class to make it easier handle recently opened session with qt settings, type conversions
|
|
limiting the size of the list and uniqueness"""
|
|
|
|
def __init__(self):
|
|
self.data = []
|
|
|
|
def load(self, dataFromQtSettings):
|
|
"""Handle qt settings load.
|
|
triggered by restoreWindowSettings in mainWindow init"""
|
|
if dataFromQtSettings:
|
|
for name in dataFromQtSettings:
|
|
self.add(name)
|
|
|
|
def add(self, nsmSessionName:str):
|
|
if nsmSessionName in self.data:
|
|
#Just sort
|
|
self.data.remove(nsmSessionName)
|
|
self.data.append(nsmSessionName)
|
|
return
|
|
|
|
self.data.append(nsmSessionName)
|
|
if len(self.data) > 3:
|
|
self.data.pop(0)
|
|
assert len(self.data) <= 3, len(self.data)
|
|
|
|
def get(self)->list:
|
|
"""List of nsmSessionName strings"""
|
|
sessionList = api.sessionList()
|
|
self.data = [n for n in self.data if n in sessionList]
|
|
return self.data
|
|
|
|
def last(self)->str:
|
|
"""Return the last active session.
|
|
Useful for continue-mode command line arg.
|
|
"""
|
|
if self.data:
|
|
return self.get()[-1]
|
|
else:
|
|
return None
|
|
|
|
|
|
class MainWindow(QtWidgets.QMainWindow):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.qtApp = qtApp
|
|
self.qtApp.setWindowIcon(QtGui.QIcon(":icon.png")) #non-template part of the program
|
|
self.qtApp.setApplicationName(f"{METADATA['name']}")
|
|
self.qtApp.setApplicationDisplayName(f"{METADATA['name']}")
|
|
logger.info("Init MainWindow")
|
|
|
|
#Set up the user interface from Designer and other widgets
|
|
self.ui = Ui_MainWindow()
|
|
self.ui.setupUi(self)
|
|
self.fPalBlue = setPaletteAndFont(self.qtApp)
|
|
|
|
self.ui.mainPageSwitcher.setCurrentIndex(0) #1 is messageLabel 0 is the tab widget
|
|
|
|
SettingsDialog.loadFromSettingsAndSendToEngine() #set blacklist, whitelist for programdatabase and addtional executable paths for environment
|
|
|
|
#TODO: Hide information tab until the feature is ready
|
|
self.ui.tabbyCat.removeTab(2)
|
|
self.programIcons = {} #executableName:QIcon. Filled by self.updateProgramDatabase
|
|
self.sessionController = SessionController(mainWindow=self)
|
|
self.systemTray = SystemTray(mainWindow=self)
|
|
self.connectMenu()
|
|
self.recentlyOpenedSessions = RecentlyOpenedSessions()
|
|
|
|
#Api Callbacks
|
|
api.callbacks.sessionClosed.append(self.reactCallback_sessionClosed)
|
|
api.callbacks.sessionOpenReady.append(self.reactCallback_sessionOpen)
|
|
api.callbacks.singleInstanceActivateWindow.append(self.activateAndRaise)
|
|
|
|
#Starting the engine sends initial GUI data. Every window and widget must be ready to receive callbacks here
|
|
api.eventLoop.start()
|
|
api.startEngine()
|
|
self.restoreWindowSettings() #populates recentlyOpenedSessions
|
|
|
|
if PATHS["startHidden"] and self.systemTray.available:
|
|
logger.info("Starting hidden")
|
|
self.toggleVisible(force=False)
|
|
else:
|
|
logger.info("Starting visible")
|
|
self.toggleVisible(force=True)
|
|
|
|
if PATHS["continueLastSession"]: #will be None if --sesion NAME was given as command line parameter and --continue on top.
|
|
continueSession = self.recentlyOpenedSessions.last()
|
|
if continueSession:
|
|
logger.info(f"Got continue session as command line parameter. Opening: {continueSession}")
|
|
api.sessionOpen(continueSession)
|
|
else:
|
|
logger.info(f"Got continue session as command line parameter but there is no session available.")
|
|
|
|
#Handle the application data cache. If not present instruct the engine to build one.
|
|
#This is also needed by the prompt in sessionController
|
|
logger.info("Trying to restore cached program database")
|
|
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
|
|
if settings.contains("programDatabase"):
|
|
listOfDicts = settings.value("programDatabase", type=list)
|
|
api.setSystemsPrograms(listOfDicts)
|
|
logger.info("Restored program database from qt cache to engine")
|
|
self._updateGUIWithCachedPrograms()
|
|
else: #First or fresh start
|
|
#A single shot timer with 0 durations is executed only after the app starts, thus the main window is ready.
|
|
logger.info("First run. Instructing engine to build program database")
|
|
QtCore.QTimer.singleShot(0, self.updateProgramDatabase) #includes self._updateGUIWithCachedPrograms()
|
|
|
|
logger.info("Deciding if we run as tray-icon or window")
|
|
if not self.isVisible():
|
|
text = QtCore.QCoreApplication.translate("mainWindow", "Argodejo ready")
|
|
self.systemTray.showMessage("Argodejo", text, QtWidgets.QSystemTrayIcon.Information, 2000) #title, message, icon, timeout. #has messageClicked() signal.
|
|
|
|
logger.info("Ready for user input. Exec_ Qt.")
|
|
qtApp.exec_()
|
|
#No code after exec_ except atexit
|
|
|
|
|
|
def tabtest(self):
|
|
import subprocess
|
|
from time import sleep
|
|
#xdotool search --name xeyes
|
|
#xdotool search --pid 12345
|
|
subprocess.Popen(["patchage"], shell=True, stdin=None, stdout=None, stderr=None, close_fds=True) #parameters are for not waiting
|
|
sleep(1)
|
|
result = subprocess.run(["xdotool", "search", "--name", "patchage"], stdout=subprocess.PIPE).stdout.decode('utf-8')
|
|
if "\n" in result:
|
|
windowID = int(result.split("\n")[0])
|
|
else:
|
|
windowID = int(result)
|
|
window = QtGui.QWindow.fromWinId(int(windowID))
|
|
window.setFlags(QtCore.Qt.FramelessWindowHint)
|
|
widget = QtWidgets.QWidget.createWindowContainer(window)
|
|
self.ui.tabbyCat.addTab(widget, "Patchage")
|
|
|
|
def hideEvent(self, event):
|
|
if self.systemTray.available:
|
|
super().hideEvent(event)
|
|
else:
|
|
event.ignore()
|
|
|
|
def activateAndRaise(self):
|
|
self.toggleVisible(force=True)
|
|
getattr(self, "raise")() #raise is python syntax. Can't use that directly
|
|
self.activateWindow()
|
|
text = QtCore.QCoreApplication.translate("mainWindow", "Another GUI tried to launch.")
|
|
self.systemTray.showMessage("Argodejo", text, QtWidgets.QSystemTrayIcon.Information, 2000) #title, message, icon, timeout. #has messageClicked() signal.
|
|
|
|
def _updateGUIWithCachedPrograms(self):
|
|
logger.info("Updating entire program with cached program lists")
|
|
updateWordlist() #addclientprompt.py
|
|
self._updateIcons()
|
|
self.sessionController.openSessionController.launcherTable.buildPrograms()
|
|
self.sessionController.quickOpenSessionController.buildCleanStarterClients(nsmSessionExportDict={}) #wants a dict parameter for callback compatibility, but doesn't use it
|
|
|
|
def _updateIcons(self):
|
|
logger.info("Creating icon database")
|
|
programs = api.getSystemPrograms()
|
|
self.programIcons.clear()
|
|
for entry in programs:
|
|
exe = entry["argodejoExec"]
|
|
if "icon" in entry:
|
|
icon = QtGui.QIcon.fromTheme(entry["icon"])
|
|
else:
|
|
icon = QtGui.QIcon.fromTheme(exe)
|
|
if icon.isNull():
|
|
icon = iconFromString(exe)
|
|
self.programIcons[exe] = icon
|
|
|
|
def updateProgramDatabase(self):
|
|
"""Display a progress-dialog that waits for the database to be build.
|
|
Automatically called on first start or when instructed by the user"""
|
|
text = QtCore.QCoreApplication.translate("mainWindow", "Updating Program Database.\nThank you for your patience.")
|
|
|
|
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
|
|
settings.remove("programDatabase")
|
|
|
|
logger.info("Asking api to getSystemPrograms while waiting")
|
|
diag = WaitDialog(self, text, api.buildSystemPrograms) #save in local var to keep alive
|
|
|
|
settings.setValue("programDatabase", api.getSystemPrograms())
|
|
self._updateGUIWithCachedPrograms()
|
|
|
|
def reactCallback_sessionClosed(self):
|
|
self.setWindowTitle("")
|
|
|
|
def reactCallback_sessionOpen(self, nsmSessionExportDict):
|
|
self.setWindowTitle(nsmSessionExportDict["nsmSessionName"])
|
|
self.recentlyOpenedSessions.add(nsmSessionExportDict["nsmSessionName"])
|
|
|
|
def toggleVisible(self, force:bool=None):
|
|
if force is None:
|
|
newState = not self.isVisible()
|
|
else:
|
|
newState = force
|
|
|
|
if newState:
|
|
logger.info("Show")
|
|
self.restoreWindowSettings()
|
|
self.show()
|
|
self.setVisible(True)
|
|
else:
|
|
logger.info("Hide")
|
|
self.storeWindowSettings()
|
|
self.hide()
|
|
self.setVisible(False)
|
|
#self.systemTray.buildContextMenu() #Don't. This crashes Qt through some delayed execution of who knows what. Workaround: tray context menu now say "Show/Hide" and not only the actual state.
|
|
|
|
def _askBeforeQuit(self, nsmSessionName):
|
|
"""If you quit while in a session ask what to do.
|
|
The TrayIcon context menu uses different functions and directly acts, without a question"""
|
|
text = QtCore.QCoreApplication.translate("AskBeforeQuit", "About to quit but session {} still open").format(nsmSessionName)
|
|
informativeText = QtCore.QCoreApplication.translate("AskBeforeQuit", "Do you want to save?")
|
|
title = QtCore.QCoreApplication.translate("AskBeforeQuit", "About to quit")
|
|
|
|
box = QtWidgets.QMessageBox()
|
|
box.setWindowFlag(QtCore.Qt.Popup, True)
|
|
box.setIcon(box.Warning)
|
|
box.setText(text)
|
|
box.setWindowTitle(title)
|
|
box.setInformativeText(informativeText)
|
|
|
|
stay = box.addButton(QtCore.QCoreApplication.translate("AskBeforeQuit", "Don't Quit"), box.RejectRole) #0
|
|
box.addButton(QtCore.QCoreApplication.translate("AskBeforeQuit", "Save"), box.YesRole) #1
|
|
box.addButton(QtCore.QCoreApplication.translate("AskBeforeQuit", "Discard Changes"), box.DestructiveRole) #2
|
|
box.setDefaultButton(stay)
|
|
ret = box.exec() #Return values are NOT the button roles.
|
|
|
|
if ret == 2:
|
|
logger.info("Quit: Don't save.")
|
|
api.sessionAbort(blocking=True)
|
|
return True
|
|
elif ret == 1:
|
|
logger.info("Quit: Close and Save. Waiting for clients to close.")
|
|
api.sessionClose(blocking=True)
|
|
return True
|
|
else: #Escape, window close through WM etc.
|
|
logger.info("Quit: Changed your mind, stay in session.")
|
|
return False
|
|
|
|
def abortAndQuit(self):
|
|
"""For the context menu. A bit
|
|
A bit redundant, but that is ok :)"""
|
|
api.sessionAbort(blocking=True)
|
|
self._callSysExit()
|
|
|
|
def closeAndQuit(self):
|
|
api.sessionClose(blocking=True)
|
|
self._callSysExit()
|
|
|
|
def _callSysExit(self):
|
|
"""The process of quitting
|
|
After sysexit the atexit handler gets called.
|
|
That closes nsmd, if we started ourselves.
|
|
"""
|
|
self.storeWindowSettings()
|
|
sysexit(0) #directly afterwards @atexit is handled, but this function does not return.
|
|
logging.error("Code executed after sysexit. This message should not have been visible.")
|
|
|
|
def menuRealQuit(self):
|
|
"""Called by the menu.
|
|
The TrayIcon provides another method of quitting that does not call this function,
|
|
but it will call _actualQuit.
|
|
"""
|
|
if api.currentSession():
|
|
result = self._askBeforeQuit(api.currentSession())
|
|
else:
|
|
result = True
|
|
|
|
if result:
|
|
self.storeWindowSettings()
|
|
self._callSysExit()
|
|
|
|
def closeEvent(self, event):
|
|
"""Window manager close.
|
|
Ignore. We use it to send the GUI into hiding."""
|
|
event.ignore()
|
|
self.toggleVisible(force=False)
|
|
|
|
def connectMenu(self):
|
|
#Control
|
|
self.ui.actionRebuild_Program_Database.triggered.connect(self.updateProgramDatabase)
|
|
self.ui.actionSettings.triggered.connect(self._reactMenu_settings)
|
|
self.ui.actionHide_in_System_Tray.triggered.connect(lambda: self.toggleVisible(force=False))
|
|
self.ui.actionMenuQuit.triggered.connect(self.menuRealQuit)
|
|
|
|
def _reactMenu_settings(self):
|
|
widget = SettingsDialog(self) #blocks until closed
|
|
if widget.success:
|
|
self.updateProgramDatabase()
|
|
|
|
def storeWindowSettings(self):
|
|
"""Window state is not saved in the real save file. That would lead to portability problems
|
|
between computers, like different screens and resolutions.
|
|
|
|
For convenience that means we just use the damned qt settings and save wherever qt wants.
|
|
|
|
bottom line: get a tiling window manager.
|
|
"""
|
|
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
|
|
settings.setValue("geometry", self.saveGeometry())
|
|
settings.setValue("windowState", self.saveState())
|
|
#settings.setValue("visible", self.isVisible()) Deprecated. see restoreWindowSettings
|
|
settings.setValue("recentlyOpenedSessions", self.recentlyOpenedSessions.get())
|
|
settings.setValue("tab", self.ui.tabbyCat.currentIndex())
|
|
|
|
def restoreWindowSettings(self):
|
|
"""opposite of storeWindowSettings. Read there."""
|
|
logger.info("Restoring window settings, geometry and recently opened session")
|
|
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
|
|
|
|
actions = {
|
|
"geometry":self.restoreGeometry,
|
|
"windowState":self.restoreState,
|
|
"recentlyOpenedSessions":self.recentlyOpenedSessions.load,
|
|
"tab": lambda i: self.ui.tabbyCat.setCurrentIndex(int(i)),
|
|
}
|
|
|
|
types = {
|
|
"recentlyOpenedSessions": list,
|
|
"tab": int,
|
|
}
|
|
|
|
for key in settings.allKeys():
|
|
if key in actions: #if not it doesn't matter. this is all uncritical.
|
|
if key in types:
|
|
actions[key](settings.value(key, type=types[key]))
|
|
else:
|
|
actions[key](settings.value(key))
|
|
|
|
#Deprecated. Always open the GUI when started normally, saving minimzed has little value.
|
|
#Instead we introduced a command line options and .desktop option to auto-load the last session and start Argodejo GUI hidden.
|
|
"""
|
|
if self.systemTray.available and settings.contains("visible") and settings.value("visible") == "false":
|
|
self.setVisible(False)
|
|
else:
|
|
self.setVisible(True) #This is also the default state if there is no config
|
|
"""
|
|
|
|
class SessionController(object):
|
|
"""Controls the StackWidget that contains the Session Tree, Open Session/Client and their
|
|
quick and easy variants.
|
|
Can be controlled from up and down the hierarchy.
|
|
|
|
While all tabs are open at the same time for simplicity we hide the menus when in quick-view.
|
|
"""
|
|
|
|
def __init__(self, mainWindow):
|
|
super().__init__()
|
|
self.mainWindow = mainWindow
|
|
self.ui = self.mainWindow.ui
|
|
self.sessionTreeController = SessionTreeController(mainWindow=mainWindow)
|
|
self.openSessionController = OpenSessionController(mainWindow=mainWindow)
|
|
|
|
self.quickSessionController = QuickSessionController(mainWindow=mainWindow)
|
|
self.quickOpenSessionController = QuickOpenSessionController(mainWindow=mainWindow)
|
|
self._connectMenu()
|
|
|
|
#Callbacks
|
|
#api.callbacks.sessionOpenReady.append(lambda nsmSessionExportDict: self._switch("open")) #When loading ist done. This takes a while when non-nsm clients are in the session
|
|
api.callbacks.sessionClosed.append(lambda: self._setMenuEnabled(None))
|
|
api.callbacks.sessionClosed.append(lambda: self._switch("choose")) #The rest is handled by the widget itself. It keeps itself updated, no matter if visible or not.
|
|
api.callbacks.sessionOpenReady.append(lambda nsmSessionExportDict: self._setMenuEnabled(nsmSessionExportDict))
|
|
api.callbacks.sessionOpenLoading.append(lambda nsmSessionExportDict: self._switch("open"))
|
|
|
|
#Convenience Signals to directly disable the client messages on gui instruction.
|
|
#This is purely for speed and preventing the user from sending a signal while the session is shutting down
|
|
self.mainWindow.ui.actionSessionAbort.triggered.connect(lambda: self._setMenuEnabled(None))
|
|
self.mainWindow.ui.actionSessionSaveAndClose.triggered.connect(lambda: self._setMenuEnabled(None))
|
|
|
|
#GUI signals
|
|
self.mainWindow.ui.tabbyCat.currentChanged.connect(self._activeTabChanged)
|
|
|
|
self._activeTabChanged(self.mainWindow.ui.tabbyCat.currentIndex())
|
|
|
|
def _activeTabChanged(self, index:int):
|
|
"""index 0 quick, 1 detailed, 2 info"""
|
|
if index == 1: #detailed
|
|
state = bool(api.currentSession())
|
|
else: #quick and information and future tabs
|
|
state = False
|
|
|
|
self.ui.menuClientNameId.menuAction().setVisible(state) #already deactivated
|
|
self.ui.menuSession.menuAction().setVisible(state) #already deactivated
|
|
|
|
#It is not enough to disable the menu itself. Shortcuts will still work. We need the children!
|
|
for action in self.ui.menuSession.actions():
|
|
action.setEnabled(state)
|
|
|
|
if state and not self.openSessionController.clientTabe.clientsTreeWidget.currentItem():
|
|
state = False #we wanted to activate, but there is no client selected.
|
|
for action in self.ui.menuClientNameId.actions():
|
|
action.setEnabled(state)
|
|
|
|
def _connectMenu(self):
|
|
#Session
|
|
#Only Active when a session is currently available
|
|
self.ui.actionSessionSave.triggered.connect(api.sessionSave)
|
|
self.ui.actionSessionAbort.triggered.connect(api.sessionAbort)
|
|
self.ui.actionSessionSaveAs.triggered.connect(self._reactMenu_SaveAs) #NSM "Duplicate"
|
|
self.ui.actionSessionSaveAndClose.triggered.connect(api.sessionClose)
|
|
self.ui.actionShow_All_Clients.triggered.connect(api.clientShowAll)
|
|
self.ui.actionHide_All_Clients.triggered.connect(api.clientHideAll)
|
|
self.ui.actionSessionAddClient.triggered.connect(lambda: askForExecutable(self.mainWindow)) #Prompt version
|
|
|
|
def _reactMenu_SaveAs(self):
|
|
"""Only when a session is open.
|
|
We could either check the session controller or the simple one for the name."""
|
|
currentName = api.currentSession()
|
|
assert currentName
|
|
widget = ProjectNameWidget(parent=self.mainWindow, startwith=currentName+"-new")
|
|
if widget.result:
|
|
api.sessionSaveAs(widget.result)
|
|
|
|
def _setMenuEnabled(self, nsmSessionExportDictOrNone):
|
|
"""We receive the sessionDict or None"""
|
|
state = bool(nsmSessionExportDictOrNone)
|
|
if state:
|
|
self.ui.menuSession.setTitle(nsmSessionExportDictOrNone["nsmSessionName"])
|
|
self.ui.menuSession.menuAction().setVisible(True)
|
|
self.ui.menuClientNameId.menuAction().setVisible(True) #session controller might disable that
|
|
else:
|
|
self.ui.menuSession.setTitle("Session")
|
|
self.ui.menuSession.menuAction().setVisible(False)
|
|
self.ui.menuClientNameId.menuAction().setVisible(False) #already deactivated
|
|
|
|
#self.ui.menuSession.setEnabled(state) #It is not enough to disable the menu itself. Shortcuts will still work.
|
|
for action in self.ui.menuSession.actions():
|
|
action.setEnabled(state)
|
|
|
|
#Maybe the tab state overrules everything
|
|
self._activeTabChanged(self.mainWindow.ui.tabbyCat.currentIndex())
|
|
|
|
def _switch(self, page:str):
|
|
"""Only called by the sub-controllers.
|
|
For example when an existing session gets opened"""
|
|
if page == "choose":
|
|
pageIndex = 0
|
|
elif page == "open":
|
|
pageIndex = 1
|
|
else:
|
|
raise ValueError(f"_switch accepts choose or open, not {page}")
|
|
|
|
self.mainWindow.ui.detailedStackedWidget.setCurrentIndex(pageIndex)
|
|
self.mainWindow.ui.quickStackedWidget.setCurrentIndex(pageIndex)
|
|
|