Browse Source

Redesign hidden start: Now it is a command line option that can be combined with the new 'continue last session' one

master
Nils 4 years ago
parent
commit
f4854a1ea7
  1. 89
      engine/api.py
  2. 41
      engine/start.py
  3. 71
      qtgui/mainwindow.py

89
engine/api.py

@ -43,19 +43,19 @@ class Callbacks(object):
Or whatever parallel representations we run."""
def __init__(self):
self.message = []
self.message = []
#Session Management
self.sessionOpenReady = []
self.sessionOpenLoading = []
self.sessionClosed = []
self.sessionOpenReady = []
self.sessionOpenLoading = []
self.sessionClosed = []
self.sessionsChanged = [] #update in the file structure. redraw list of sessions.
self.sessionLocked = [] # incremental update. Sends the name of the session project and a bool if locked
self.sessionFileChanged = [] #incremental update. Reports the session name, not the session file
self.clientStatusChanged = [] #every status including GUI and dirty
self.singleInstanceActivateWindow = [] #this is for the single-instance feature. Show the GUI window and activate it when this signal comes.
self.dataClientNamesChanged = []
self.dataClientDescriptionChanged = []
self.dataClientNamesChanged = []
self.dataClientDescriptionChanged = []
def _dataClientNamesChanged(self, data):
"""If there is a dataclient in the session it will allow us to read and write metadata.
@ -67,8 +67,8 @@ class Callbacks(object):
dataclient can join and leave at every time, we keep the GUI informed. """
for func in self.dataClientNamesChanged:
func(data)
def _dataClientDescriptionChanged(self, data):
def _dataClientDescriptionChanged(self, data):
"""see _dataClientNamesChanged.
In short: str for data, None if nsm-data leaves session"""
for func in self.dataClientDescriptionChanged:
@ -105,12 +105,12 @@ class Callbacks(object):
Always sends a full update of everything, with no indication of what changed."""
listOfProjectDicts = nsmServerControl.exportSessionsAsDicts()
for func in self.sessionsChanged:
for func in self.sessionsChanged:
func(listOfProjectDicts)
return listOfProjectDicts
def _sessionLocked(self, name:str, status:bool):
"""Called by the Watcher through the event loop
Name is "nsmSessionName" from _nsmServerControl.exportSessionsAsDicts
@ -124,7 +124,7 @@ class Callbacks(object):
Called by the Watcher through the event loop.
Name is "nsmSessionName" from _nsmServerControl.exportSessionsAsDicts.
timestamp has same format as nsmServerControl.exportSessionsAsDicts"""
timestamp has same format as nsmServerControl.exportSessionsAsDicts"""
for func in self.sessionFileChanged:
func(name, timestamp)
@ -136,12 +136,12 @@ class Callbacks(object):
func(clientInfoDict)
def startEngine():
def startEngine():
logger.info("Start Engine")
global eventLoop
assert eventLoop
global nsmServerControl
nsmServerControl = NsmServerControl(
sessionOpenReadyHook=callbacks._sessionOpenReady,
@ -153,22 +153,22 @@ def startEngine():
dataClientDescriptionHook=callbacks._dataClientDescriptionChanged,
parameterNsmOSCUrl=PATHS["url"],
sessionRoot=PATHS["sessionRoot"],
)
#Watch session tree for changes.
global sessionWatcher
sessionWatcher = Watcher(nsmServerControl)
sessionWatcher.timeStampHook = callbacks._sessionFileChanged
sessionWatcher.lockFileHook = callbacks._sessionLocked
sessionWatcher.sessionsChangedHook = callbacks._sessionsChanged #This is the main callback that informs of new or updated sessions
callbacks.sessionClosed.append(sessionWatcher.resume) #Watcher only active in "Choose a session mode"
)
#Watch session tree for changes.
global sessionWatcher
sessionWatcher = Watcher(nsmServerControl)
sessionWatcher.timeStampHook = callbacks._sessionFileChanged
sessionWatcher.lockFileHook = callbacks._sessionLocked
sessionWatcher.sessionsChangedHook = callbacks._sessionsChanged #This is the main callback that informs of new or updated sessions
callbacks.sessionClosed.append(sessionWatcher.resume) #Watcher only active in "Choose a session mode"
callbacks.sessionOpenReady.append(sessionWatcher.suspend)
eventLoop.slowConnect(sessionWatcher.process)
eventLoop.slowConnect(sessionWatcher.process)
#Start Event Loop Processing
eventLoop.fastConnect(nsmServerControl.process)
eventLoop.slowConnect(nsmServerControl.processSingleInstance)
#Send initial data
#The decision if we are already in a session on startup or in "choose a session mode" is handled by callbacks
#This is not to actually gather the data, but only to inform the GUI.
@ -179,8 +179,11 @@ def startEngine():
#of a session-on-load:
if PATHS["startupSession"]:
logger.info(f"Got start-session as command line parameter. Opening: {PATHS['startupSession']}")
sessionOpen(PATHS["startupSession"])
sessionOpen(PATHS["startupSession"])
PATHS["continueLastSession"] = None #just in case
#Info
def sessionRoot():
return nsmServerControl.sessionRoot
@ -191,12 +194,12 @@ def currentSession():
def sessionList()->list:
"""Updates the list each call. Use only this from a GUI for active query.
Otherwise sessionRemove and sessionCopy will not have updated the list"""
r = nsmServerControl.exportSessionsAsDicts()
r = nsmServerControl.exportSessionsAsDicts()
return [s["nsmSessionName"] for s in r]
def buildSystemPrograms():
"""Build a list of dicts with the .desktop files (or similar) of all NSM compatible programs
present on the system"""
present on the system"""
programDatabase.build()
def systemProgramsSetWhitelist(executableNames:tuple):
@ -211,14 +214,14 @@ def systemProgramsSetBlacklist(executableNames:tuple):
def getSystemPrograms()->list:
"""Returns the cached database from buildProgramDatabase. No automatic update. Empty on program
start"""
start"""
return programDatabase.programs
def setSystemsPrograms(listOfDicts:list):
programDatabase.loadPrograms(listOfDicts)
def setSystemsPrograms(listOfDicts:list):
programDatabase.loadPrograms(listOfDicts)
def getNsmExecutables()->set:
"""Cached access fort fast membership tests. Is this program in the PATH?"""
"""Cached access fort fast membership tests. Is this program in the PATH?"""
return programDatabase.nsmExecutables
def getUnfilteredExecutables()->list:
@ -233,7 +236,7 @@ def getUnfilteredExecutables()->list:
#No project running
#There is no callback for _sessionsChanged because we poll that in the event loop.
def sessionNew(newName:str, startClients:list=[]):
def sessionNew(newName:str, startClients:list=[]):
nsmServerControl.new(newName, startClients)
def sessionRename(nsmSessionName:str, newName:str):
@ -242,21 +245,21 @@ def sessionRename(nsmSessionName:str, newName:str):
def sessionCopy(nsmSessionName:str, newName:str):
"""Create a copy of the session. Removes the lockfile, if any.
Has some safeguards inside so it will not crash."""
Has some safeguards inside so it will not crash."""
nsmServerControl.copySession(nsmSessionName, newName)
def sessionOpen(nsmSessionName:str):
"""Saves the current session and loads a different existing session."""
"""Saves the current session and loads a different existing session."""
nsmServerControl.open(nsmSessionName)
def sessionQuery(nsmSessionName:str):
"""For the occasional out-of-order information query.
Exports a single session project in the format of nsmServerControl.exportSessionsAsDicts"""
Exports a single session project in the format of nsmServerControl.exportSessionsAsDicts"""
return nsmServerControl.sessionAsDict(nsmSessionName)
def sessionForceLiftLock(nsmSessionName:str):
nsmServerControl.forceLiftLock(nsmSessionName)
callbacks._sessionLocked(nsmSessionName, False)
callbacks._sessionLocked(nsmSessionName, False)
def sessionDelete(nsmSessionName:str):
nsmServerControl.deleteSession(nsmSessionName)
@ -268,7 +271,7 @@ def sessionSave():
def sessionClose(blocking=False):
"""Saves and closes the current session."""
nsmServerControl.close(blocking)
nsmServerControl.close(blocking)
def sessionAbort(blocking=False):
"""Close without saving the current session."""
@ -295,8 +298,8 @@ def clientResume(clientId:str):
nsmServerControl.clientResume(clientId)
def clientRemove(clientId:str):
"""Client must be already stopped! We will do that without further question.
Remove from the session. Will not delete the save-files, but make them inaccesible"""
"""Client must be already stopped! We will do that without further question.
Remove from the session. Will not delete the save-files, but make them inaccesible"""
nsmServerControl.clientRemove(clientId)
def clientSave(clientId:str):
@ -327,8 +330,8 @@ def executableInSession(executable:str)->dict:
else returns a dict with its export-data.
If multiple clients with this exe are in the session only one is returned, whatever Python
thinks is good"""
for clientId, dic in nsmServerControl.internalState["clients"].items():
if executable == dic["executable"]:
for clientId, dic in nsmServerControl.internalState["clients"].items():
if executable == dic["executable"]:
return dic
else:
return None

41
engine/start.py

@ -42,7 +42,9 @@ parser.add_argument("-V", "--verbose", action='store_true', help="(Development)
parser.add_argument("-u", "--url", action='store', dest="url", help="Force URL for the session. If there is already a running session we will connect to it. Otherwise we will start one there. Default is local host with random port. Example: osc.udp://myhost.localdomain:14294/")
parser.add_argument("--nsm-url", action='store', dest="url", help="Same as --url.")
parser.add_argument("-s", "--session", action='store', dest="session", help="Session to open on startup.")
parser.add_argument("-s", "--session", action='store', dest="session", help="Session to open on startup. Overrides --continue")
parser.add_argument("-c", "--continue", action='store_true', dest="continueLastSession", help="Autostart last active session.")
parser.add_argument("-i", "--hide", action='store_true', dest="starthidden", help="Start GUI hidden in tray, only if tray available on system.")
parser.add_argument("--session-root", action='store', dest="sessionRoot", help="Root directory of all sessions. Defaults to '$HOME/NSM Sessions'")
args = parser.parse_args()
@ -59,7 +61,7 @@ else:
logger = logging.getLogger(__name__)
logger.info("import")
"""set up python search path before the program starts and cbox gets imported.
"""set up python search path before the program starts
We need to be earliest, so let's put it here.
This is influence during compiling by creating a temporary file "compiledprefix.py".
Nuitka complies that in, when make is finished we delete it.
@ -82,8 +84,6 @@ except ModuleNotFoundError as e:
logger.info("Compiled version: {}".format(compiledVersion))
cboxSharedObjectVersionedName = "lib"+METADATA["shortName"]+".so." + METADATA["version"]
if compiledVersion:
PATHS={ #this gets imported
"root": "",
@ -95,27 +95,12 @@ if compiledVersion:
"sessionRoot": args.sessionRoot,
"url": args.url,
"startupSession": args.session,
#"lib": os.path.join(prefix, "lib", METADATA["shortName"]), #cbox is found via the PYTHONPATH
"startHidden": args.starthidden, #bool
"continueLastSession": args.continueLastSession, #bool
}
cboxSharedObjectPath = os.path.join(prefix, "lib", METADATA["shortName"], cboxSharedObjectVersionedName)
_root = os.path.dirname(__file__)
_root = os.path.abspath(os.path.join(_root, ".."))
fallback_cboxSharedObjectPath = os.path.join(_root, "site-packages", cboxSharedObjectVersionedName)
#Local version has higher priority
if os.path.exists(fallback_cboxSharedObjectPath): #we are not yet installed, look in the source site-packages dir
os.environ["CALFBOXLIBABSPATH"] = fallback_cboxSharedObjectPath
elif os.path.exists(cboxSharedObjectPath): #we are installed
os.environ["CALFBOXLIBABSPATH"] = cboxSharedObjectPath
else:
pass
#no support for system-wide cbox in compiled mode. Error handling at the bottom of the file
else:
_root = os.path.dirname(__file__)
_root = os.path.abspath(os.path.join(_root, ".."))
@ -129,17 +114,9 @@ else:
"sessionRoot": args.sessionRoot,
"url": args.url,
"startupSession": args.session,
#"lib": "", #use only system paths
"startHidden": args.starthidden, #bool
"continueLastSession": args.continueLastSession, #bool
}
if os.path.exists (os.path.join(_root, "site-packages", cboxSharedObjectVersionedName)):
os.environ["CALFBOXLIBABSPATH"] = os.path.join(_root, "site-packages", cboxSharedObjectVersionedName)
#else use system-wide.
if os.path.exists (os.path.join(_root, "site-packages", "calfbox", "cbox.py")):
#add to the front to have higher priority than system site-packages
logger.info("Will attempt to start with local calfbox python module: {}".format(os.path.join(_root, "site-packages", "calfbox", "cbox.py")))
sys.path.insert(0, os.path.join(os.path.join(_root, "site-packages")))
#else try to use system-wide calfbox. Check for this and if the .so exists at the end of this file.
logger.info("PATHS: {}".format(PATHS))

71
qtgui/mainwindow.py

@ -80,13 +80,17 @@ class RecentlyOpenedSessions(object):
self.data = []
def load(self, dataFromQtSettings):
"""Handle qt settings load, working around everything it has"""
"""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)
@ -94,12 +98,21 @@ class RecentlyOpenedSessions(object):
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):
@ -116,18 +129,17 @@ class MainWindow(QtWidgets.QMainWindow):
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
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.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)
@ -135,8 +147,23 @@ class MainWindow(QtWidgets.QMainWindow):
#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() #includes show/hide
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
@ -152,7 +179,7 @@ class MainWindow(QtWidgets.QMainWindow):
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")
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.
@ -161,12 +188,12 @@ class MainWindow(QtWidgets.QMainWindow):
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
#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')
@ -217,7 +244,7 @@ class MainWindow(QtWidgets.QMainWindow):
"""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")
@ -235,19 +262,21 @@ class MainWindow(QtWidgets.QMainWindow):
self.recentlyOpenedSessions.add(nsmSessionExportDict["nsmSessionName"])
def toggleVisible(self, force:bool=None):
if force:
newState = force
else:
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):
@ -329,11 +358,10 @@ class MainWindow(QtWidgets.QMainWindow):
self.ui.actionMenuQuit.triggered.connect(self.menuRealQuit)
def _reactMenu_settings(self):
widget = SettingsDialog(self) #blocks until closed
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.
@ -345,7 +373,7 @@ class MainWindow(QtWidgets.QMainWindow):
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
settings.setValue("geometry", self.saveGeometry())
settings.setValue("windowState", self.saveState())
settings.setValue("visible", self.isVisible())
#settings.setValue("visible", self.isVisible()) Deprecated. see restoreWindowSettings
settings.setValue("recentlyOpenedSessions", self.recentlyOpenedSessions.get())
settings.setValue("tab", self.ui.tabbyCat.currentIndex())
@ -367,17 +395,20 @@ class MainWindow(QtWidgets.QMainWindow):
}
for key in settings.allKeys():
if key in actions: #if not it doesn't matter. this is all uncritical.
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

Loading…
Cancel
Save