From fd11e1b6629197c3d9e5cea86b2e29e6995d2448 Mon Sep 17 00:00:00 2001
From: Nils <>
Date: Mon, 8 Jun 2020 21:05:13 +0200
Subject: [PATCH] Add settings dialog with option to add custom executable
paths and a white- and blacklist for the program launcher. This should make
custom scenarios much easier without compromising the 'everything must be in
the PATH' rule
---
engine/api.py | 17 +++-
engine/findprograms.py | 85 +++++++++--------
qtgui/addclientprompt.py | 45 +++++----
qtgui/designer/mainwindow.py | 9 +-
qtgui/designer/mainwindow.ui | 6 ++
qtgui/designer/newsession.py | 2 +-
qtgui/designer/newsession.ui | 2 +-
qtgui/designer/settings.py | 82 +++++++++++++++++
qtgui/designer/settings.ui | 142 +++++++++++++++++++++++++++++
qtgui/mainwindow.py | 24 +++--
qtgui/resources/translations/de.qm | Bin 10648 -> 10651 bytes
qtgui/resources/translations/de.ts | 32 +------
qtgui/settings.py | 116 +++++++++++++++++++++++
13 files changed, 457 insertions(+), 105 deletions(-)
create mode 100644 qtgui/designer/settings.py
create mode 100644 qtgui/designer/settings.ui
create mode 100644 qtgui/settings.py
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 eb5428a389bc5f0528c25c89e8f4c5131dbd06c5..c07d65025972e43ea1d08271ba2d20772ca8f1cb 100644
GIT binary patch
delta 500
zcmXAlUr3Wt7{;G-zI|uzbDO!^zRa=Hs5PxfM<|Sth=}Z>tF7=NZEK9aX2gmO3@err
zd87Gpx=
zUZm{K_u~52N6HZsqj&EEtz1ma5;^qD+45)wi1?hNr851ms6H;)k{aYuR#A7jjMa>;
z^#&lH7}-}rK+GH2>Io_!ZM=9)guNZc>c}~ATrl2LW2Dzqjmu+)n}5$u0l_0~Q=X?>
zOK$V|C188OePiM;^>Ew$VaQ7qzsI#z1a_=>+ULl{an*C&aR}Jeu6i`d>gtRZZ5{Bg
zr26Zq$1HU@kdQf~k-;$Xqy^e6MA6wq%|g(0tBpX!l}l%a
N^XWpyD!a^kzJGL$gPQ;V
delta 497
zcmXAlT}V@L7{;If|LmNtYC@HA_e$+_@{lp
zX}`w5U$+B}^Ga~u3+SJfc+?MgW@UyQW|o{~CR5}J6Gw9Dm&v!3w?q9AeG1gpX<;=%
zT_&{F4E=6@r(InCKwTQOksG&xYOdw(QbMq3cR!c|!fyLWwnX=rGRg&;mOb2JGjfcJ
z=J$GJsRD>cdg4U@(29DZe24(_>dzmNLw#6Zyl|Wn3;L^aGwC_G!bMwC)t^ER2<$gZ
zaf1GuHq2*dfX!*+(#;LxaLrimb&-q5xoJH^d={MB3Y20?I}h3#fvq*NMG
- Platzhalter
@@ -138,55 +137,46 @@ Für Notizen, TODO, Referenzen, Quellen etcâ¦
- Platzhalter
- Platzhalter
- Platzhalter
- Platzhalter
- Platzhalter
- Platzhalter
- Platzhalter
- Platzhalter
- Platzhalter
@@ -197,7 +187,6 @@ Für Notizen, TODO, Referenzen, Quellen etcâ¦
- Nicht in benutzungArbeitet
@@ -208,13 +197,11 @@ Für Notizen, TODO, Referenzen, Quellen etcâ¦
- Platzhalter
- Platzhalter
@@ -225,7 +212,6 @@ Für Notizen, TODO, Referenzen, Quellen etcâ¦
- Keine Shortcuts übersetzen
@@ -246,7 +232,6 @@ Für Notizen, TODO, Referenzen, Quellen etcâ¦
- Keine Shortcuts übersetzen
@@ -257,13 +242,11 @@ Für Notizen, TODO, Referenzen, Quellen etcâ¦
- Keine Shortcuts übersetzen
- Keine Shortcuts übersetzen
@@ -274,13 +257,11 @@ Für Notizen, TODO, Referenzen, Quellen etcâ¦
- Keine Shortcuts übersetzen
- Keine Shortcuts übersetzen
@@ -291,7 +272,6 @@ Für Notizen, TODO, Referenzen, Quellen etcâ¦
- Keine Shortcuts übersetzen
@@ -302,7 +282,6 @@ Für Notizen, TODO, Referenzen, Quellen etcâ¦
- Keine Shortcuts übersetzen
@@ -313,7 +292,6 @@ Für Notizen, TODO, Referenzen, Quellen etcâ¦
- Keine Shortcuts übersetzen
@@ -324,7 +302,6 @@ Für Notizen, TODO, Referenzen, Quellen etcâ¦
- Keine Shortcuts übersetzen
@@ -335,7 +312,6 @@ Für Notizen, TODO, Referenzen, Quellen etcâ¦
- Keine Shortcuts übersetzen
@@ -346,7 +322,6 @@ Für Notizen, TODO, Referenzen, Quellen etcâ¦
- Keine Shortcuts übersetzen
@@ -372,7 +347,6 @@ Für Notizen, TODO, Referenzen, Quellen etcâ¦
- Keine Shortcuts übersetzen
@@ -381,7 +355,6 @@ Für Notizen, TODO, Referenzen, Quellen etcâ¦
- Platzhalter
@@ -392,8 +365,8 @@ Für Notizen, TODO, Referenzen, Quellen etcâ¦
- 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â¦
- 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)