#! /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
#Our Modules
from engine . start import PATHS
from . nsmservercontrol import NsmServerControl
from . watcher import Watcher
from . findprograms import programDatabase
class Callbacks ( object ) :
""" GUI methods register themselves here.
These methods get called by us , the engine .
None of these methods produce any return value .
The lists may be unordered .
We need the lists for audio feedbacks in parallel to GUI updates .
Or whatever parallel representations we run . """
def __init__ ( self ) :
self . message = [ ]
#Session Management
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 = [ ]
def _dataClientNamesChanged ( self , data ) :
""" If there is a dataclient in the session it will allow us to read and write metadata.
The GUI instructs us to send a write instruction over OSC , we wait for the OSC answer
and forward that to the GUI .
If the dataClient joins the session it will trigger an unrequested callback ( from the GUIs
perspectvive ) . If the client leaves the callback will send a single None . This is a sign
for the GUI to reset all data to the nsmd state . We do not mix nsmd data and dataClient . A
dataclient can join and leave at every time , we keep the GUI informed . """
for func in self . dataClientNamesChanged :
func ( data )
def _dataClientDescriptionChanged ( self , data ) :
""" see _dataClientNamesChanged.
In short : str for data , None if nsm - data leaves session """
for func in self . dataClientDescriptionChanged :
func ( data )
def _singleInstanceActivateWindow ( self ) :
for func in self . singleInstanceActivateWindow :
func ( )
def _sessionOpenReady ( self , nsmSessionExportDict ) :
""" A project got opened, most likely by ourselves, but also by another party.
This will also fire if we start and detect that a session is already open and running .
"""
for func in self . sessionOpenReady :
func ( nsmSessionExportDict )
def _sessionOpenLoading ( self , nsmSessionExportDict ) :
"""
A session begins loading . Show a spinning clock or so . . .
"""
for func in self . sessionOpenLoading :
func ( nsmSessionExportDict )
def _sessionClosed ( self ) :
""" The current session got closed. Present a list of sessions to choose from again.
This is also send at GUI start if there is no session open presently . """
for func in self . sessionClosed :
func ( )
def _sessionsChanged ( self ) :
""" The project list changed.
This can happen through internal changes like deletion or duplication
but also through external changes through a filemanager .
Always sends a full update of everything , with no indication of what changed . """
listOfProjectDicts = nsmServerControl . exportSessionsAsDicts ( )
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
Sends True if project is locked """
for func in self . sessionLocked :
func ( name , status )
def _sessionFileChanged ( self , name : str , timestamp : str ) :
"""
This happens everytime the session gets saved .
Called by the Watcher through the event loop .
Name is " nsmSessionName " from _nsmServerControl . exportSessionsAsDicts .
timestamp has same format as nsmServerControl . exportSessionsAsDicts """
for func in self . sessionFileChanged :
func ( name , timestamp )
def _clientStatusChanged ( self , clientInfoDict : dict ) :
""" A single function for all client changes. Adding and deletion is also included.
GUI hide / show and dirty is also here .
A GUI needs to check if it already knows the clientId or not . """
for func in self . clientStatusChanged :
func ( clientInfoDict )
def startEngine ( ) :
logger . info ( " Start Engine " )
global eventLoop
assert eventLoop
global nsmServerControl
nsmServerControl = NsmServerControl (
sessionOpenReadyHook = callbacks . _sessionOpenReady ,
sessionOpenLoadingHook = callbacks . _sessionOpenLoading ,
sessionClosedHook = callbacks . _sessionClosed ,
clientStatusHook = callbacks . _clientStatusChanged ,
singleInstanceActivateWindowHook = callbacks . _singleInstanceActivateWindow ,
dataClientNamesHook = callbacks . _dataClientNamesChanged ,
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"
callbacks . sessionOpenReady . append ( sessionWatcher . suspend )
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.
callbacks . _sessionsChanged ( )
#nsmServerControl blocks until it has a connection to nsmd. That means at this point we are ready to send commands.
#Until we return from startEngine a GUI will also not create its mainwindow. This can be used to create the appearance
#of a session-on-load:
if PATHS [ " startupSession " ] :
logger . info ( f " Got start-session as command line parameter. Opening: { PATHS [ ' startupSession ' ] } " )
sessionOpen ( PATHS [ " startupSession " ] )
#Info
def sessionRoot ( ) :
return nsmServerControl . sessionRoot
def currentSession ( ) :
return nsmServerControl . internalState [ " 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 ( )
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 """
programDatabase . build ( )
def getSystemPrograms ( ) - > list :
""" Returns the cached database from buildProgramDatabase. No automatic update. Empty on program
start """
return programDatabase . programs
def setSystemsPrograms ( listOfDicts : list ) :
programDatabase . loadPrograms ( listOfDicts )
def getNsmExecutables ( ) - > set :
""" Cached access fort fast membership tests. Is this program in the PATH? """
return programDatabase . nsmExecutables
def getUnfilteredExecutables ( ) - > list :
""" Return a list of unique names without paths or directories of all exectuables in users $PATH.
This is intended for a program starter prompt . GUI needs to supply tab completition or search
itself """
return programDatabase . unfilteredExecutables
#Session Control
#No project running
#There is no callback for _sessionsChanged because we poll that in the event loop.
def sessionNew ( newName : str , startClients : list = [ ] ) :
nsmServerControl . new ( newName , startClients )
def sessionRename ( nsmSessionName : str , newName : str ) :
""" only for non-open sessions """
nsmServerControl . renameSession ( nsmSessionName , newName )
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 . """
nsmServerControl . copySession ( nsmSessionName , newName )
def sessionOpen ( nsmSessionName : str ) :
""" 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 """
return nsmServerControl . sessionAsDict ( nsmSessionName )
def sessionForceLiftLock ( nsmSessionName : str ) :
nsmServerControl . forceLiftLock ( nsmSessionName )
callbacks . _sessionLocked ( nsmSessionName , False )
def sessionDelete ( nsmSessionName : str ) :
nsmServerControl . deleteSession ( nsmSessionName )
#While Project is open
def sessionSave ( ) :
""" Saves the current session. """
nsmServerControl . save ( )
def sessionClose ( blocking = False ) :
""" Saves and closes the current session. """
nsmServerControl . close ( blocking )
def sessionAbort ( blocking = False ) :
""" Close without saving the current session. """
nsmServerControl . abort ( blocking )
def sessionSaveAs ( nsmSessionName : str ) :
""" Duplicate in NSM terms. Make a copy, close the current one, open the new one.
However , it will NOT send the session clossed signal , just a session changed one . """
nsmServerControl . duplicate ( nsmSessionName )
def setDescription ( text : str ) :
nsmServerControl . setDescription ( text )
#Client Handling
def clientAdd ( executableName ) :
nsmServerControl . clientAdd ( executableName )
#status hook triggers clientStatusChanged callback
def clientStop ( clientId : str ) :
nsmServerControl . clientStop ( clientId )
def clientResume ( clientId : str ) :
""" Opposite of clientStop """
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 """
nsmServerControl . clientRemove ( clientId )
def clientSave ( clientId : str ) :
""" Saves only the given client """
nsmServerControl . clientSave ( clientId )
def clientToggleVisible ( clientId : str ) :
""" Works only if client announced itself with this feature """
nsmServerControl . clientToggleVisible ( clientId )
def clientHideAll ( ) :
nsmServerControl . allClientsHide ( )
def clientShowAll ( ) :
nsmServerControl . allClientsShow ( )
def clientNameOverride ( clientId : str , name : str ) :
""" An argodejo-specific function that requires the client nsm-data in the session.
If nsm - data is not present this function will write nothing , not touch any data .
It will still send a callback to revert any GUI changes back to the original name .
We accept empty string as a name to remove the name override
"""
nsmServerControl . clientNameOverride ( clientId , name )
def executableInSession ( executable : str ) - > dict :
""" Returns None if no client with this executable is in the session,
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 " ] :
return dic
else :
return None
#Global Datastructures, set in startEngine
nsmServerControl = None
eventLoop = None
sessionWatcher = None
callbacks = Callbacks ( ) #This needs to be defined before startEngine() so a GUI can register its callbacks. The UI will then startEngine and wait to reveice the initial round of callbacks