diff --git a/engine/api.py b/engine/api.py index 906f899..03ac3eb 100644 --- a/engine/api.py +++ b/engine/api.py @@ -152,7 +152,7 @@ def startEngine(): dataClientNamesHook=callbacks._dataClientNamesChanged, dataClientDescriptionHook=callbacks._dataClientDescriptionChanged, parameterNsmOSCUrl=PATHS["url"], - sessionRoot=PATHS["sessionRoot"], + sessionRoot=PATHS["sessionRoot"], ) #Watch session tree for changes. @@ -198,7 +198,17 @@ 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 systemProgramsSetWhitelist(executableNames:tuple): + """will replace the current list""" + programDatabase.userWhitelist = tuple(executableNames) + #Needs rebuild through the GUI. We have no callback for this. + +def systemProgramsSetBlacklist(executableNames:tuple): + """will replace the current list""" + programDatabase.userBlacklist = tuple(executableNames) + #Needs rebuild through the GUI. We have no callback for this. + def getSystemPrograms()->list: """Returns the cached database from buildProgramDatabase. No automatic update. Empty on program start""" @@ -214,7 +224,8 @@ def getNsmExecutables()->set: 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""" + itself""" + programDatabase.unfilteredExecutables = programDatabase.buildCache_unfilteredExecutables() #quick call return programDatabase.unfilteredExecutables diff --git a/engine/findprograms.py b/engine/findprograms.py index 9179a1c..4c82d85 100644 --- a/engine/findprograms.py +++ b/engine/findprograms.py @@ -48,7 +48,7 @@ class SupportedProgramsDatabase(object): 'name': 'Patroneo', 'startupnotify': 'false', 'terminal': 'false', - 'type': 'Application', + 'type': 'Application', 'x-nsm-capable': 'true'} In case there is a file in PATH or database but has no .desktop we create our own entry with @@ -57,16 +57,19 @@ class SupportedProgramsDatabase(object): def __init__(self): self.blacklist = ("nsmd", "non-daw", "carla") - self.morePrograms = ("thisdoesnotexisttest", "carla-rack", "carla-patchbay", "carla-jack-multi", "ardour5", "ardour6", "nsm-data", "nsm-jack") #only programs not found by buildCache_grepExecutablePaths because they are just shellscripts and do not contain /nsm/server/announce. + self.morePrograms = ("thisdoesnotexisttest", "carla-rack", "carla-patchbay", "carla-jack-multi", "ardour5", "ardour6", "nsm-data", "nsm-jack") #only programs not found by buildCache_grepExecutablePaths because they are just shellscripts and do not contain /nsm/server/announce. + self.userWhitelist = () #added dynamically to morePrograms + self.userBlacklist = () #added dynamically to blacklist self.knownDesktopFiles = { #shortcuts to the correct desktop files. Reverse lookup binary->desktop creates false entries, for example ZynAddSubFx and Carla. "zynaddsubfx": "zynaddsubfx-jack.desktop", #value will later get replaced with the .desktop entry - "carla-jack-multi" : "carla.desktop", - } + "carla-jack-multi" : "carla.desktop", + } self.programs = [] #list of dicts. guaranteed keys: argodejoExec, name, argodejoFullPath. And probably others, like description and version. self.nsmExecutables = set() #set of executables for fast membership, if a GUI wants to know if they are available. Needs to be build "manually" with self.programs. no auto-property for a list. at least we don't want to do the work. #.build needs to be called from the api/GUI. - self.unfilteredExecutables = self.buildCache_unfilteredExecutables() #This doesn't take too long. we can start that every time. It will get updated in build as well. - #self.build() #fills self.programs and + #self.unfilteredExecutables = self.buildCache_unfilteredExecutables() #This doesn't take too long. we can start that every time. It will get updated in build as well. + self.unfilteredExecutables = None #in build() + #self.build() #fills self.programs and def buildCache_grepExecutablePaths(self): """return a list of executable names in the path @@ -74,24 +77,28 @@ class SupportedProgramsDatabase(object): -s silent. No errors, eventhough subprocess uses stdout only -R recursive with symlinks. We don't want to look in subdirs because that is not allowed by PATH and nsm, but we want to follow symlinks + + If you have a custom user path that does not mean that all its executables will + automatically show up here. They still need to contain /nsm/server/announce + Your binaries will be in unfilteredExecutables though """ result = [] - executablePaths = [pathlib.Path(p) for p in os.environ["PATH"].split(":")] + executablePaths = [pathlib.Path(p) for p in os.environ["PATH"].split(os.pathsep)] for path in executablePaths: command = f"grep -iRsnl {path} -e /nsm/server/announce" #Py>=3.7 completedProcess = subprocess.run(command, capture_output=True, text=True, shell=True) completedProcess = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True) #universal_newlines is an alias for text, which was deprecated in 3.7 because text is more understandable. capture_output replaces the two PIPEs in 3.7 for fullPath in completedProcess.stdout.split(): - exe = pathlib.Path(fullPath).relative_to(path) - if not str(exe) in self.blacklist: - result.append((str(exe), str(fullPath))) + exe = pathlib.Path(fullPath).relative_to(path) + if not str(exe) in self.blacklist + self.userBlacklist: + result.append((str(exe), str(fullPath))) - for prg in self.morePrograms: + for prg in self.morePrograms + self.userWhitelist: for path in executablePaths: - if pathlib.Path(path, prg).is_file(): + if pathlib.Path(path, prg).is_file(): result.append((str(prg), str(pathlib.Path(path, prg)))) break #inner loop - + return result def buildCache_DesktopEntries(self): @@ -109,7 +116,7 @@ class SupportedProgramsDatabase(object): if f.is_file() and f.suffix == ".desktop": config.clear() config.read(f) - entryDict = dict(config._sections["Desktop Entry"]) + entryDict = dict(config._sections["Desktop Entry"]) if f.name == "zynaddsubfx-jack.desktop": self.knownDesktopFiles["zynaddsubfx"] = entryDict elif f.name == "carla.desktop": @@ -124,7 +131,7 @@ class SupportedProgramsDatabase(object): self.nsmExecutables = set(d["argodejoExec"] for d in self.programs) def build(self): - """Can be called at any time by the user to update after installing new programs""" + """Can be called at any time by the user to update after installing new programs""" logger.info("Building launcher database. This might take a minute") self.programs = self._build() self.unfilteredExecutables = self.buildCache_unfilteredExecutables() @@ -137,40 +144,40 @@ class SupportedProgramsDatabase(object): Convert one exe (not full path!) to one dict entry. """ if exe in self.knownDesktopFiles: #Shortcut through internal database entry = self.knownDesktopFiles[exe] - else: #Reverse Search desktop files. + else: #Reverse Search desktop files. for entry in self.desktopEntries: - if "exec" in entry and exe.lower() in entry["exec"].lower(): + if "exec" in entry and exe.lower() in entry["exec"].lower(): return entry - #else: #Foor loop ended. Did not find any matching desktop file + #else: #Foor loop ended. Did not find any matching desktop file return None - - def _build(self): - self.executables = self.buildCache_grepExecutablePaths() + + def _build(self): + self.executables = self.buildCache_grepExecutablePaths() self.desktopEntries = self.buildCache_DesktopEntries() leftovers = set(self.executables) matches = [] #list of dicts for exe, fullPath in self.executables: entry = self._exeToDesktopEntry(exe) - if entry: #Found match! + if entry: #Found match! entry["argodejoFullPath"] = fullPath #We don't want .desktop syntax like "qmidiarp %F" entry["argodejoExec"] = exe matches.append(entry) - + try: leftovers.remove((exe,fullPath)) except KeyError: pass #Double entries like zyn-jack zyn-alsa etc. - + """ if exe in self.knownDesktopFiles: #Shortcut through internal database entry = self.knownDesktopFiles[exe] - else: #Reverse Search desktop files. + else: #Reverse Search desktop files. for entry in self.desktopEntries: - if "exec" in entry and exe.lower() in entry["exec"].lower(): + if "exec" in entry and exe.lower() in entry["exec"].lower(): break #found entry. break inner loop to keep it else: #Foor loop ended. Did not find any matching desktop file #If we omit continue it will just write exe and fullPath in any desktop file. @@ -181,28 +188,28 @@ class SupportedProgramsDatabase(object): #We don't want .desktop syntax like "qmidiarp %F" entry["argodejoExec"] = exe matches.append(entry) - + try: leftovers.remove((exe,fullPath)) except KeyError: pass #Double entries like zyn-jack zyn-alsa etc. """ - + for exe,fullPath in leftovers: pseudoEntry = {"name": exe.title(), "argodejoExec":exe, "argodejoFullPath":fullPath} matches.append(pseudoEntry) return matches - + def buildCache_unfilteredExecutables(self): def isexe(path): """executable by owner""" - return path.is_file() and stat.S_IXUSR & os.stat(path)[stat.ST_MODE] == 64 - + return path.is_file() and stat.S_IXUSR & os.stat(path)[stat.ST_MODE] == 64 + result = [] - executablePaths = [pathlib.Path(p) for p in os.environ["PATH"].split(":")] + executablePaths = [pathlib.Path(p) for p in os.environ["PATH"].split(os.pathsep)] for path in executablePaths: result += [str(pathlib.Path(f).relative_to(path)) for f in path.glob("*") if isexe(f)] - + return sorted(list(set(result))) def _buildWhitelist(self): @@ -210,19 +217,19 @@ class SupportedProgramsDatabase(object): It will be populated from a template-list of well-working clients and then all binaries not in the path are filtered out. This can be presented to the user without worries.""" #Assumes to be called only from self.build - startexecutables = set(("doesnotexist", "laborejo2", "patroneo", "vico", "fluajho", "carla-rack", "carla-patchbay", "carla-jack-multi", "ardour6", "drumkv1_jack", "synthv1_jack", "padthv1_jack", "samplv1_jack", "zynaddsubfx", "ADLplug", "OPNplug", "non-mixer", "non-sequencer", "non-timeline")) - for prog in self.programs: + startexecutables = set(("doesnotexist", "laborejo", "patroneo", "vico", "fluajho", "carla-rack", "carla-patchbay", "carla-jack-multi", "ardour6", "drumkv1_jack", "synthv1_jack", "padthv1_jack", "samplv1_jack", "zynaddsubfx", "ADLplug", "OPNplug", "non-mixer", "non-sequencer", "non-timeline") + self.userWhitelist) + for prog in self.programs: prog["whitelist"] = prog["argodejoExec"] in startexecutables - + """ - matches = [] + matches = [] for exe in startexecutables: entry = self._exeToDesktopEntry(exe) - if entry: #Found match! + if entry: #Found match! #We don't want .desktop syntax like "qmidiarp %F" entry["argodejoExec"] = exe - matches.append(entry) + matches.append(entry) return matches """ diff --git a/qtgui/addclientprompt.py b/qtgui/addclientprompt.py index 41f830e..3294e49 100644 --- a/qtgui/addclientprompt.py +++ b/qtgui/addclientprompt.py @@ -37,7 +37,10 @@ class PromptWidget(QtWidgets.QDialog): def __init__(self, parent): super().__init__(parent) layout = QtWidgets.QFormLayout() - #layout = QtWidgets.QVBoxLayout() + #layout = QtWidgets.QVBoxLayout() + + updateWordlist() #this is a fast index update, we can call that every time to be sure. Otherwise newly added executable-PATHs will not be reflected in the dialog without a program-database update, which takes too long to be convenient. + self.setLayout(layout) self.setWindowFlag(QtCore.Qt.Popup, True) @@ -45,26 +48,26 @@ class PromptWidget(QtWidgets.QDialog): self.comboBox.setEditable(True) if PromptWidget.wordlist: - completer = QtWidgets.QCompleter(PromptWidget.wordlist) + completer = QtWidgets.QCompleter(PromptWidget.wordlist) completer.setModelSorting(QtWidgets.QCompleter.CaseInsensitivelySortedModel) - completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) - completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion) + completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) + completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion) completer.activated.connect(self.process) #To avoid double-press enter to select one we hook into the activated signal and trigger process self.comboBox.setCompleter(completer) - self.comboBox.setMinimumContentsLength(PromptWidget.minlen) + self.comboBox.setMinimumContentsLength(PromptWidget.minlen) labelString = QtCore.QCoreApplication.translate("PromptWidget", "Type in the name of an executable file on your system.") else: labelString = QtCore.QCoreApplication.translate("PromptWidget", "No program database found. Please update through Control menu.") - - label = QtWidgets.QLabel(labelString) + + label = QtWidgets.QLabel(labelString) layout.addWidget(label) - self.comboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToMinimumContentsLength) - - layout.addWidget(self.comboBox) + self.comboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToMinimumContentsLength) + + layout.addWidget(self.comboBox) errorString = QtCore.QCoreApplication.translate("PromptWidget", "Command not accepted!
Parameters, --switches and relative paths are not allowed.
Use nsm-proxy or write a starter-script instead.") errorString = "" + errorString + "" - self.errorLabel = QtWidgets.QLabel(errorString) + self.errorLabel = QtWidgets.QLabel(errorString) layout.addWidget(self.errorLabel) self.errorLabel.hide() #shown in process @@ -72,10 +75,8 @@ class PromptWidget(QtWidgets.QDialog): buttonBox.accepted.connect(self.process) buttonBox.rejected.connect(self.reject) layout.addWidget(buttonBox) - - self.exec() #blocks until the dialog gets closed - + self.exec() #blocks until the dialog gets closed def abortHandler(self): pass @@ -85,8 +86,12 @@ class PromptWidget(QtWidgets.QDialog): Make sure your objects exists and your syntax is correct""" assert PromptWidget.wordlist assert PromptWidget.minlen - + curText = self.comboBox.currentText() + if not curText or curText == " ": #TODO: qt weirdness. This is a case when focus is lost from a valid entry. The field is filled from a chosen value, from the list. But it says " ". + #Do not show the errorLabel. This is a qt bug or so. + return + if curText in PromptWidget.wordlist: api.clientAdd(curText) logger.info(f"Prompt accepted {curText} and will send it to clientAdd") @@ -94,8 +99,8 @@ class PromptWidget(QtWidgets.QDialog): else: logger.info(f"Prompt did not accept {curText}.Showing info to the user.") self.errorLabel.show() - - + + def updateWordlist(): """in case programs are installed while the session is running the user can manually call a database update""" @@ -104,6 +109,6 @@ def updateWordlist(): PromptWidget.minlen = len(max(PromptWidget.wordlist, key=len)) else: logger.error("Executable list came back empty! Most likely an error in application database build. Not trivial!") - -def askForExecutable(parent): - PromptWidget(parent) + +def askForExecutable(parent): + PromptWidget(parent) diff --git a/qtgui/designer/mainwindow.py b/qtgui/designer/mainwindow.py index aacf657..1cbf085 100644 --- a/qtgui/designer/mainwindow.py +++ b/qtgui/designer/mainwindow.py @@ -2,9 +2,10 @@ # Form implementation generated from reading ui file 'mainwindow.ui' # -# Created by: PyQt5 UI code generator 5.14.2 +# Created by: PyQt5 UI code generator 5.15.0 # -# WARNING! All changes made in this file will be lost! +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. from PyQt5 import QtCore, QtGui, QtWidgets @@ -310,8 +311,11 @@ class Ui_MainWindow(object): self.actionRebuild_Program_Database.setObjectName("actionRebuild_Program_Database") self.actionClientRename = QtWidgets.QAction(MainWindow) self.actionClientRename.setObjectName("actionClientRename") + self.actionSettings = QtWidgets.QAction(MainWindow) + self.actionSettings.setObjectName("actionSettings") self.menuControl.addAction(self.actionRebuild_Program_Database) self.menuControl.addAction(self.actionHide_in_System_Tray) + self.menuControl.addAction(self.actionSettings) self.menuControl.addAction(self.actionMenuQuit) self.menuSession.addAction(self.actionSessionAddClient) self.menuSession.addAction(self.actionSessionSave) @@ -405,3 +409,4 @@ class Ui_MainWindow(object): self.actionRebuild_Program_Database.setText(_translate("MainWindow", "Rebuild Program Database")) self.actionClientRename.setText(_translate("MainWindow", "Rename")) self.actionClientRename.setShortcut(_translate("MainWindow", "F2")) + self.actionSettings.setText(_translate("MainWindow", "Settings")) diff --git a/qtgui/designer/mainwindow.ui b/qtgui/designer/mainwindow.ui index 947e326..28e2849 100644 --- a/qtgui/designer/mainwindow.ui +++ b/qtgui/designer/mainwindow.ui @@ -636,6 +636,7 @@ + @@ -796,6 +797,11 @@ F2 + + + Settings + + diff --git a/qtgui/designer/newsession.py b/qtgui/designer/newsession.py index 2bb2624..dfce928 100644 --- a/qtgui/designer/newsession.py +++ b/qtgui/designer/newsession.py @@ -47,6 +47,6 @@ class Ui_NewSession(object): NewSession.setWindowTitle(_translate("NewSession", "Dialog")) self.nameGroupBox.setTitle(_translate("NewSession", "New Session Name")) self.checkBoxJack.setText(_translate("NewSession", "Save JACK Connections\n" -"(adds clients \'nsm-jack\')")) +"(adds clients \'jackpatch\')")) self.checkBoxData.setText(_translate("NewSession", "Client Renaming and Session Notes\n" "(adds client \'nsm-data\')")) diff --git a/qtgui/designer/newsession.ui b/qtgui/designer/newsession.ui index 17ee03e..db901e1 100644 --- a/qtgui/designer/newsession.ui +++ b/qtgui/designer/newsession.ui @@ -42,7 +42,7 @@ Save JACK Connections -(adds clients 'nsm-jack') +(adds clients 'jackpatch') true diff --git a/qtgui/designer/settings.py b/qtgui/designer/settings.py new file mode 100644 index 0000000..396111c --- /dev/null +++ b/qtgui/designer/settings.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'settings.ui' +# +# Created by: PyQt5 UI code generator 5.15.0 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(626, 387) + self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) + self.verticalLayout.setObjectName("verticalLayout") + self.SettingsTab = QtWidgets.QTabWidget(Dialog) + self.SettingsTab.setTabPosition(QtWidgets.QTabWidget.North) + self.SettingsTab.setTabShape(QtWidgets.QTabWidget.Rounded) + self.SettingsTab.setObjectName("SettingsTab") + self.tab_2 = QtWidgets.QWidget() + self.tab_2.setObjectName("tab_2") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.tab_2) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.label_help_launcher_whitelist = QtWidgets.QLabel(self.tab_2) + self.label_help_launcher_whitelist.setWordWrap(True) + self.label_help_launcher_whitelist.setObjectName("label_help_launcher_whitelist") + self.verticalLayout_3.addWidget(self.label_help_launcher_whitelist) + self.launcherWhitelistPlainTextEdit = QtWidgets.QPlainTextEdit(self.tab_2) + self.launcherWhitelistPlainTextEdit.setObjectName("launcherWhitelistPlainTextEdit") + self.verticalLayout_3.addWidget(self.launcherWhitelistPlainTextEdit) + self.label_help_launcher_blacklist = QtWidgets.QLabel(self.tab_2) + self.label_help_launcher_blacklist.setWordWrap(True) + self.label_help_launcher_blacklist.setObjectName("label_help_launcher_blacklist") + self.verticalLayout_3.addWidget(self.label_help_launcher_blacklist) + self.launcherBlacklistPlainTextEdit = QtWidgets.QPlainTextEdit(self.tab_2) + self.launcherBlacklistPlainTextEdit.setObjectName("launcherBlacklistPlainTextEdit") + self.verticalLayout_3.addWidget(self.launcherBlacklistPlainTextEdit) + self.SettingsTab.addTab(self.tab_2, "") + self.tab = QtWidgets.QWidget() + self.tab.setObjectName("tab") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.tab) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.label_help_programstart = QtWidgets.QLabel(self.tab) + self.label_help_programstart.setWordWrap(True) + self.label_help_programstart.setObjectName("label_help_programstart") + self.verticalLayout_2.addWidget(self.label_help_programstart) + self.label_help_path_rules = QtWidgets.QLabel(self.tab) + self.label_help_path_rules.setWordWrap(True) + self.label_help_path_rules.setObjectName("label_help_path_rules") + self.verticalLayout_2.addWidget(self.label_help_path_rules) + self.programPathsPlainTextEdit = QtWidgets.QPlainTextEdit(self.tab) + self.programPathsPlainTextEdit.setObjectName("programPathsPlainTextEdit") + self.verticalLayout_2.addWidget(self.programPathsPlainTextEdit) + self.SettingsTab.addTab(self.tab, "") + self.verticalLayout.addWidget(self.SettingsTab) + self.buttonBox = QtWidgets.QDialogButtonBox(Dialog) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout.addWidget(self.buttonBox) + self.buttonBox.raise_() + self.SettingsTab.raise_() + + self.retranslateUi(Dialog) + self.SettingsTab.setCurrentIndex(0) + self.buttonBox.accepted.connect(Dialog.accept) + self.buttonBox.rejected.connect(Dialog.reject) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "Settings")) + self.label_help_launcher_whitelist.setText(_translate("Dialog", "Whitelist - Add executable names (not paths) to the program launcher. One executable per line.")) + self.label_help_launcher_blacklist.setText(_translate("Dialog", "Blacklist - Exclude executable names (not paths) from the program launcher. One executable per line.")) + self.SettingsTab.setTabText(self.SettingsTab.indexOf(self.tab_2), _translate("Dialog", "Launcher")) + self.label_help_programstart.setText(_translate("Dialog", "For advanced users only! Add executable paths to the environment, just for Argodejo and NSM. Changes need a program restart afterwards. If you want your programs in the application launcher use the launcher tab.")) + self.label_help_path_rules.setText(_translate("Dialog", "Add one absolute path to a directory (e.g. /home/user/audio-bin) per line. No wildcards. Trailing slashes/ don\'t matter.")) + self.SettingsTab.setTabText(self.SettingsTab.indexOf(self.tab), _translate("Dialog", "$PATH")) diff --git a/qtgui/designer/settings.ui b/qtgui/designer/settings.ui new file mode 100644 index 0000000..b200fac --- /dev/null +++ b/qtgui/designer/settings.ui @@ -0,0 +1,142 @@ + + + Dialog + + + + 0 + 0 + 626 + 387 + + + + Settings + + + + + + QTabWidget::North + + + QTabWidget::Rounded + + + 0 + + + + Launcher + + + + + + Whitelist - Add executable names (not paths) to the program launcher. One executable per line. + + + true + + + + + + + + + + Blacklist - Exclude executable names (not paths) from the program launcher. One executable per line. + + + true + + + + + + + + + + + $PATH + + + + + + For advanced users only! Add executable paths to the environment, just for Argodejo and NSM. Changes need a program restart afterwards. If you want your programs in the application launcher use the launcher tab. + + + true + + + + + + + Add one absolute path to a directory (e.g. /home/user/audio-bin) per line. No wildcards. Trailing slashes/ don't matter. + + + true + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + buttonBox + SettingsTab + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/qtgui/mainwindow.py b/qtgui/mainwindow.py index d85f49b..367f63f 100644 --- a/qtgui/mainwindow.py +++ b/qtgui/mainwindow.py @@ -48,6 +48,7 @@ from .projectname import ProjectNameWidget from .addclientprompt import askForExecutable, updateWordlist from .waitdialog import WaitDialog from .resources import * +from .settings import SettingsDialog api.eventLoop = EventLoop() @@ -115,20 +116,18 @@ 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 + #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() - self.programIcons = {} #executableName:QIcon. Filled by self.updateProgramDatabase - - #Menu - self.ui.actionRebuild_Program_Database.triggered.connect(self.updateProgramDatabase) - + #Api Callbacks api.callbacks.sessionClosed.append(self.reactCallback_sessionClosed) api.callbacks.sessionOpenReady.append(self.reactCallback_sessionOpen) @@ -316,7 +315,6 @@ class MainWindow(QtWidgets.QMainWindow): self.storeWindowSettings() self._callSysExit() - def closeEvent(self, event): """Window manager close. Ignore. We use it to send the GUI into hiding.""" @@ -325,9 +323,17 @@ class MainWindow(QtWidgets.QMainWindow): 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. diff --git a/qtgui/resources/translations/de.qm b/qtgui/resources/translations/de.qm index eb5428a..c07d650 100644 Binary files a/qtgui/resources/translations/de.qm and b/qtgui/resources/translations/de.qm differ diff --git a/qtgui/resources/translations/de.ts b/qtgui/resources/translations/de.ts index 84db4d8..ce789d9 100644 --- a/qtgui/resources/translations/de.ts +++ b/qtgui/resources/translations/de.ts @@ -77,7 +77,6 @@ Für Notizen, TODO, Referenzen, Quellen etc… Session Name Goes Here - Platzhalter @@ -138,55 +137,46 @@ Für Notizen, TODO, Referenzen, Quellen etc… version and running - Platzhalter NSM Server Mode - Platzhalter Self-started, connected to, environment var - Platzhalter NSM Url - Platzhalter osc.upd ip port - Platzhalter Session Root - Platzhalter /home/usr/NSM Sessions - Platzhalter Program Database - Platzhalter Last Updated - Platzhalter @@ -197,7 +187,6 @@ Für Notizen, TODO, Referenzen, Quellen etc… Processing - Nicht in benutzung Arbeitet @@ -208,13 +197,11 @@ Für Notizen, TODO, Referenzen, Quellen etc… SessionName - Platzhalter ClientNameId - Platzhalter @@ -225,7 +212,6 @@ Für Notizen, TODO, Referenzen, Quellen etc… Ctrl+Shift+Q - Keine Shortcuts übersetzen @@ -246,7 +232,6 @@ Für Notizen, TODO, Referenzen, Quellen etc… Ctrl+Q - Keine Shortcuts übersetzen @@ -257,13 +242,11 @@ Für Notizen, TODO, Referenzen, Quellen etc… A - Keine Shortcuts übersetzen Ctrl+S - Keine Shortcuts übersetzen @@ -274,13 +257,11 @@ Für Notizen, TODO, Referenzen, Quellen etc… Ctrl+Shift+S - Keine Shortcuts übersetzen Ctrl+W - Keine Shortcuts übersetzen @@ -291,7 +272,6 @@ Für Notizen, TODO, Referenzen, Quellen etc… Ctrl+Shift+W - Keine Shortcuts übersetzen @@ -302,7 +282,6 @@ Für Notizen, TODO, Referenzen, Quellen etc… Alt+O - Keine Shortcuts übersetzen @@ -313,7 +292,6 @@ Für Notizen, TODO, Referenzen, Quellen etc… Alt+R - Keine Shortcuts übersetzen @@ -324,7 +302,6 @@ Für Notizen, TODO, Referenzen, Quellen etc… Alt+S - Keine Shortcuts übersetzen @@ -335,7 +312,6 @@ Für Notizen, TODO, Referenzen, Quellen etc… Alt+X - Keine Shortcuts übersetzen @@ -346,7 +322,6 @@ Für Notizen, TODO, Referenzen, Quellen etc… Alt+T - Keine Shortcuts übersetzen @@ -372,7 +347,6 @@ Für Notizen, TODO, Referenzen, Quellen etc… F2 - Keine Shortcuts übersetzen @@ -381,7 +355,6 @@ Für Notizen, TODO, Referenzen, Quellen etc… Dialog - Platzhalter @@ -392,8 +365,8 @@ Für Notizen, TODO, Referenzen, Quellen etc… Save JACK Connections -(adds clients 'nsm-jack') - Speichere JACK-Verbindungen (mit Client 'nsm-jack') +(adds clients 'jackpatch') + Speichere JACK-Verbindungen (mit Client 'jackpatch') @@ -470,7 +443,6 @@ Für Notizen, TODO, Referenzen, Quellen etc… Form - Platzhalter diff --git a/qtgui/settings.py b/qtgui/settings.py new file mode 100644 index 0000000..b3b94bd --- /dev/null +++ b/qtgui/settings.py @@ -0,0 +1,116 @@ +#! /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 . +""" + +import logging; logger = logging.getLogger(__name__); logger.info("import") + + +#Standard Library +import pathlib +import os + +#Third Party +from PyQt5 import QtCore, QtWidgets + +#Engine +import engine.api as api +from engine.config import METADATA #includes METADATA only. No other environmental setup is executed. + +#QtGui +from .designer.settings import Ui_Dialog + +class SettingsDialog(QtWidgets.QDialog): + + def __init__(self, mainWindow): + super().__init__(mainWindow) + self.ui = Ui_Dialog() + self.ui.setupUi(self) + self.mainWindow = mainWindow + + self.success = False + + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + if settings.contains("launcherBlacklistPlainTextEdit"): + self.ui.launcherBlacklistPlainTextEdit.setPlainText(settings.value("launcherBlacklistPlainTextEdit", type=str)) + else: + self.ui.launcherBlacklistPlainTextEdit.setPlainText("") + + if settings.contains("launcherWhitelistPlainTextEdit"): + self.ui.launcherWhitelistPlainTextEdit.setPlainText(settings.value("launcherWhitelistPlainTextEdit", type=str)) + else: + self.ui.launcherWhitelistPlainTextEdit.setPlainText("") + + if settings.contains("programPathsPlainTextEdit"): + self.ui.programPathsPlainTextEdit.setPlainText(settings.value("programPathsPlainTextEdit", type=str)) + else: + self.ui.programPathsPlainTextEdit.setPlainText("") + + #self.ui.name.textEdited.connect(self.check) #not called when text is changed programatically + + self.ui.buttonBox.accepted.connect(self.process) + self.ui.buttonBox.rejected.connect(self.reject) + + self.setWindowFlag(QtCore.Qt.Popup, True) + self.setModal(True) + self.setFocus(True) + self.exec_() + + @staticmethod + def loadFromSettingsAndSendToEngine(): + """Called on program start and in self.process, which has a bit overhead because + it is saving to file and then reloading from file (qsettings)""" + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + if settings.contains("launcherBlacklistPlainTextEdit"): + bl = settings.value("launcherBlacklistPlainTextEdit", type=str) + else: + bl = None + + if settings.contains("launcherWhitelistPlainTextEdit"): + wl = settings.value("launcherWhitelistPlainTextEdit", type=str) + else: + wl = None + + if settings.contains("programPathsPlainTextEdit"): + pth = settings.value("programPathsPlainTextEdit", type=str) + else: + pth = None + + blacklist = bl.split("\n") if bl else [] + whitelist = wl.split("\n") if wl else [] + + api.systemProgramsSetBlacklist(blacklist) + api.systemProgramsSetWhitelist(whitelist) + + #Depends on SettingsDialog: More executable paths for the engine. We do this in mainwindow because it has access to the qsettings safe file and is started before engine, program-database or nsmd. + additionalExecutablePaths = pth.split("\n") if pth else [] + if additionalExecutablePaths: + os.environ["PATH"] = os.pathsep.join(additionalExecutablePaths) + os.pathsep + os.environ["PATH"] + + def process(self): + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + + settings.setValue("launcherBlacklistPlainTextEdit", self.ui.launcherBlacklistPlainTextEdit.toPlainText()) + settings.setValue("launcherWhitelistPlainTextEdit", self.ui.launcherWhitelistPlainTextEdit.toPlainText()) + settings.setValue("programPathsPlainTextEdit", self.ui.programPathsPlainTextEdit.toPlainText()) + + SettingsDialog.loadFromSettingsAndSendToEngine() + + self.success = True + self.done(True)