Music production session manager https://www.laborejo.org
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

590 Zeilen
26 KiB

vor 4 Jahren
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
vor 2 Jahren
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
vor 4 Jahren
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
vor 4 Jahren
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 exit as sysexit
#Third Party
from PyQt5 import QtCore, QtGui, QtWidgets
logger.info(f"PyQt Version: {QtCore.PYQT_VERSION_STR}")
vor 4 Jahren
#Engine
from engine.config import METADATA #includes METADATA only. No other environmental setup is executed.
from engine.start import PATHS, qtApp
vor 4 Jahren
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 .usermanual import UserManual
from .changelog import Changelog
vor 4 Jahren
from .helper import setPaletteAndFont
from .helper import iconFromString
from .sessiontreecontroller import SessionTreeController
from .opensessioncontroller import OpenSessionController
from .projectname import ProjectNameWidget
from .addclientprompt import askForExecutable, updateWordlist
from .waitdialog import WaitDialog
from .resources import *
from .settings import SettingsDialog
from .jacktransport import JackTransportControls
from .movesessionroot import xdgVersionChange
from .startchooserunningnsmd import checkForRunningNsmd
vor 4 Jahren
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 ./agordejo
vor 4 Jahren
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
vor 4 Jahren
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"""
vor 4 Jahren
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)
vor 4 Jahren
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"""
vor 4 Jahren
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
vor 4 Jahren
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
vor 4 Jahren
self.qtApp.setApplicationName(f"{METADATA['name']}")
self.qtApp.setApplicationDisplayName(f"{METADATA['name']}")
self.qtApp.setOrganizationName("Laborejo Software Suite")
self.qtApp.setOrganizationDomain("laborejo.org")
self.qtApp.setApplicationVersion(METADATA["version"])
QtGui.QIcon.setThemeName("hicolor") #audio applications can be found here. We have no need for other icons.
vor 4 Jahren
logger.info("Init MainWindow")
#QtGui.QIcon.setFallbackThemeName("hicolor") #only one, not a list. This is the fallback if the theme can't be found. Not if icons can't be found in a theme.
#iconPaths = QtGui.QIcon.themeSearchPaths()
#iconPaths += ["/usr/share/icons/hicolor", "/usr/share/pixmaps"]
#QtGui.QIcon.setThemeSearchPaths(iconPaths)
logger.info(f"Program icons path: {QtGui.QIcon.themeSearchPaths()}, {QtGui.QIcon.themeName()}")
QtCore.QT_TRANSLATE_NOOP("NOOPEngineStrings", "ERROR! Copied session data is different from source session. Please check you data!")
QtCore.QT_TRANSLATE_NOOP("NOOPEngineStrings", "Veryfying file-integrity. This may take a while...")
vor 4 Jahren
#Set up the user interface from Designer and other widgets
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.fPalBlue = setPaletteAndFont(self.qtApp)
assert self.ui.tabbyCat.currentIndex() == 0, self.ui.tabbyCat.currentIndex() # this is critical. If you left the Qt Designer with the wrong tab open this is the error that happens. It will trigger the tab changed later that will go wrong because setup is not complete yet and you'll get AttributeError
vor 4 Jahren
self.userManual = UserManual(mainWindow=self)
self.changelog = Changelog(mainWindow=self)
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(1)
self.programIcons = {} #executableName:QIcon. Filled by self._updateGUIWithCachedPrograms which calls _updateIcons
vor 4 Jahren
self.sessionController = SessionController(mainWindow=self)
self.systemTray = SystemTray(mainWindow=self)
self.connectMenu()
self.recentlyOpenedSessions = RecentlyOpenedSessions()
#Setup JackTransportControls Widget. It configures itself on init:
self.jackTransportControls = JackTransportControls(mainWindow = self) #not a widget, just an object
#self.ui.stack_loaded_session is only visible when there is a loaded session and the full view tab is active
#we link the session context menu to the session menu menu.
self.ui.stack_loaded_session.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.ui.stack_loaded_session.customContextMenuRequested.connect(self.customContextMenu)
#nsmd 1.6.0
xdgVersionChange(self.qtApp) #may present a blocking dialog, may do nothing.
checkForRunningNsmd(self.qtApp, PATHS) #may present a blocking dialog, may do nothing. Injects nsm url into PATHS
vor 4 Jahren
#Api Callbacks
api.callbacks.sessionClosed.append(self.reactCallback_sessionClosed)
api.callbacks.sessionOpenReady.append(self.reactCallback_sessionOpen)
api.callbacks.singleInstanceActivateWindow.append(self.activateAndRaise)
#Handle the application data cache.
#This must happen before engineStart. If a session is already running a set of initial
#client-callbacks will arrive immediately, even before the eventLoop starts.
#If not present instruct the engine to build one.
#This is also needed by the prompt in sessionController and the icons
logger.info("Trying to restore cached program database")
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
if settings.contains("engineCache"):
engineCache = settings.value("engineCache", type=dict)
api.setCache(engineCache)
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()
vor 4 Jahren
#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 --load-session=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.")
vor 4 Jahren
if not self.isVisible():
text = QtCore.QCoreApplication.translate("mainWindow", "Agordejo ready")
self.systemTray.showMessage("Agordejo", text, QtWidgets.QSystemTrayIcon.Information, 2000) #title, message, icon, timeout. #has messageClicked() signal.
vor 4 Jahren
vor 4 Jahren
logger.info("Ready for user input. Exec_ Qt.")
vor 4 Jahren
qtApp.exec_()
vor 4 Jahren
#No code after exec_ except atexit
vor 4 Jahren
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")
vor 4 Jahren
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("Agordejo", text, QtWidgets.QSystemTrayIcon.Information, 2000) #title, message, icon, timeout. #has messageClicked() signal.
vor 4 Jahren
def _updateGUIWithCachedPrograms(self):
logger.info("Updating entire program with cached program lists")
updateWordlist() #addclientprompt.py
self._updateIcons()
self.sessionController.openSessionController.launcherTable.buildPrograms()
def _updateIcons(self):
logger.info("Creating icon database")
engineCache = api.getCache()
assert engineCache
programs = engineCache["programs"]
vor 4 Jahren
self.programIcons.clear()
for entry in programs:
exe = entry["agordejoExec"]
vor 4 Jahren
icon = None
if "agordejoIconPath" in entry and entry["agordejoIconPath"]: #not null
icon = QtGui.QIcon(entry["agordejoIconPath"])
vor 4 Jahren
if not icon or icon.isNull(): #the DB cache could be wrong. Deinstalled a program and not updated the DB.
if "icon" in entry:
icon = QtGui.QIcon.fromTheme(entry["icon"])
else:
icon = QtGui.QIcon.fromTheme(exe)
if icon.isNull():
icon = QtGui.QIcon.fromTheme(exe)
if icon.isNull():
icon = iconFromString(exe)
assert not icon.isNull()
vor 4 Jahren
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.\nIf progress freezes please kill and restart the whole program.")
vor 4 Jahren
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
settings.remove("engineCache")
vor 4 Jahren
logger.info("Asking api to generate program and icon database while waiting")
diag = WaitDialog(self, text, api.buildSystemPrograms) #save in local var to keep alive
assert api.getCache()
settings.setValue("engineCache", api.getCache()) # dict
vor 4 Jahren
self._updateGUIWithCachedPrograms()
def reactCallback_sessionClosed(self):
self.setWindowTitle("")
def reactCallback_sessionOpen(self, nsmSessionExportDict):
self.setWindowTitle(nsmSessionExportDict["nsmSessionName"])
self.recentlyOpenedSessions.add(nsmSessionExportDict["nsmSessionName"])
vor 4 Jahren
def toggleVisible(self, force:bool=None):
if force is None:
vor 4 Jahren
newState = not self.isVisible()
else:
newState = force
vor 4 Jahren
if newState:
logger.info("Show")
self.restoreWindowSettings()
self.show()
self.setVisible(True)
vor 4 Jahren
else:
logger.info("Hide")
self.storeWindowSettings()
self.hide()
self.setVisible(False)
vor 4 Jahren
#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 toggleSplitSessionView(self, state:bool):
self.ui.actionSplit_Session_View_The_Other_Way.blockSignals(True)
if state:
orientation = QtCore.Qt.Vertical #this is weird. Is the Qt enum wrongly labeled? This splits in an upper and lower part.
else:
orientation = QtCore.Qt.Horizontal
self.ui.actionSplit_Session_View_The_Other_Way.setChecked(state) #This funtion can be called by functions, e.g. restoreWindowSettings on startup.
self.ui.hSplitterLauncherClients.setOrientation(orientation)
self.ui.actionSplit_Session_View_The_Other_Way.blockSignals(False)
vor 4 Jahren
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.ourOwnServer() and api.currentSession():
vor 4 Jahren
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.actionManual.triggered.connect(self.userManual.show)
self.ui.actionChangelog.triggered.connect(self.changelog.show)
self.ui.actionRebuild_Program_Database.triggered.connect(self.updateProgramDatabase)
self.ui.actionSettings.triggered.connect(self._reactMenu_settings)
self.ui.actionSplit_Session_View_The_Other_Way.toggled.connect(self.toggleSplitSessionView)
vor 4 Jahren
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 customContextMenu(self, qpoint):
pos = QtGui.QCursor.pos()
pos.setY(pos.y() + 5)
self.ui.menuSession.exec_(pos)
vor 4 Jahren
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
vor 4 Jahren
settings.setValue("recentlyOpenedSessions", self.recentlyOpenedSessions.get())
settings.setValue("tab", self.ui.tabbyCat.currentIndex())
settings.setValue("sessionSplitOrientationNonDefault", self.ui.actionSplit_Session_View_The_Other_Way.isChecked())
vor 4 Jahren
def restoreWindowSettings(self):
"""opposite of storeWindowSettings. Read there."""
vor 4 Jahren
logger.info("Restoring window settings, geometry and recently opened session")
vor 4 Jahren
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)),
"sessionSplitOrientationNonDefault": lambda b: self.toggleSplitSessionView(bool(b)),
vor 4 Jahren
}
types = {
"recentlyOpenedSessions": list,
"tab": int,
"sessionSplitOrientationNonDefault": bool,
}
vor 4 Jahren
for key in settings.allKeys():
if key in actions: #if not it doesn't matter. this is all uncritical.
if key in types:
vor 4 Jahren
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 Agordejo GUI hidden.
"""
vor 4 Jahren
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
"""
vor 4 Jahren
class SessionController(object):
"""Controls the StackWidget that contains the Session Tree and Opened Session/Client.
vor 4 Jahren
Can be controlled from up and down the hierarchy.
vor 4 Jahren
"""
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._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
vor 4 Jahren
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())
vor 4 Jahren
def _activeTabChanged(self, index:int):
"""index 0 is open session, 1 is info etc"""
if index == 0: #detailed
vor 4 Jahren
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.
vor 4 Jahren
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)