Browse Source

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

master
Nils 4 years ago
parent
commit
fd11e1b662
  1. 17
      engine/api.py
  2. 85
      engine/findprograms.py
  3. 45
      qtgui/addclientprompt.py
  4. 9
      qtgui/designer/mainwindow.py
  5. 6
      qtgui/designer/mainwindow.ui
  6. 2
      qtgui/designer/newsession.py
  7. 2
      qtgui/designer/newsession.ui
  8. 82
      qtgui/designer/settings.py
  9. 142
      qtgui/designer/settings.ui
  10. 24
      qtgui/mainwindow.py
  11. BIN
      qtgui/resources/translations/de.qm
  12. 32
      qtgui/resources/translations/de.ts
  13. 116
      qtgui/settings.py

17
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

85
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
"""

45
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!<br>Parameters, --switches and relative paths are not allowed.<br>Use nsm-proxy or write a starter-script instead.")
errorString = "<i>" + errorString + "</i>"
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)

9
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"))

6
qtgui/designer/mainwindow.ui

@ -636,6 +636,7 @@
</property>
<addaction name="actionRebuild_Program_Database"/>
<addaction name="actionHide_in_System_Tray"/>
<addaction name="actionSettings"/>
<addaction name="actionMenuQuit"/>
</widget>
<widget class="QMenu" name="menuSession">
@ -796,6 +797,11 @@
<string>F2</string>
</property>
</action>
<action name="actionSettings">
<property name="text">
<string>Settings</string>
</property>
</action>
</widget>
<resources/>
<connections/>

2
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\')"))

2
qtgui/designer/newsession.ui

@ -42,7 +42,7 @@
<widget class="QCheckBox" name="checkBoxJack">
<property name="text">
<string>Save JACK Connections
(adds clients 'nsm-jack')</string>
(adds clients 'jackpatch')</string>
</property>
<property name="checked">
<bool>true</bool>

82
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"))

142
qtgui/designer/settings.ui

@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>626</width>
<height>387</height>
</rect>
</property>
<property name="windowTitle">
<string>Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="SettingsTab">
<property name="tabPosition">
<enum>QTabWidget::North</enum>
</property>
<property name="tabShape">
<enum>QTabWidget::Rounded</enum>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Launcher</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_help_launcher_whitelist">
<property name="text">
<string>Whitelist - Add executable names (not paths) to the program launcher. One executable per line.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="launcherWhitelistPlainTextEdit"/>
</item>
<item>
<widget class="QLabel" name="label_help_launcher_blacklist">
<property name="text">
<string>Blacklist - Exclude executable names (not paths) from the program launcher. One executable per line.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="launcherBlacklistPlainTextEdit"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>$PATH</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label_help_programstart">
<property name="text">
<string>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.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_help_path_rules">
<property name="text">
<string>Add one absolute path to a directory (e.g. /home/user/audio-bin) per line. No wildcards. Trailing slashes/ don't matter.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="programPathsPlainTextEdit"/>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
<zorder>buttonBox</zorder>
<zorder>SettingsTab</zorder>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

24
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.

BIN
qtgui/resources/translations/de.qm

Binary file not shown.

32
qtgui/resources/translations/de.ts

@ -77,7 +77,6 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/mainwindow.py" line="346"/>
<source>Session Name Goes Here</source>
<translatorcomment>Platzhalter</translatorcomment>
<translation></translation>
</message>
<message>
@ -138,55 +137,46 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/mainwindow.py" line="362"/>
<source>version and running</source>
<translatorcomment>Platzhalter</translatorcomment>
<translation></translation>
</message>
<message>
<location filename="../../designer/mainwindow.py" line="363"/>
<source>NSM Server Mode</source>
<translatorcomment>Platzhalter</translatorcomment>
<translation></translation>
</message>
<message>
<location filename="../../designer/mainwindow.py" line="364"/>
<source>Self-started, connected to, environment var</source>
<translatorcomment>Platzhalter</translatorcomment>
<translation></translation>
</message>
<message>
<location filename="../../designer/mainwindow.py" line="365"/>
<source>NSM Url</source>
<translatorcomment>Platzhalter</translatorcomment>
<translation></translation>
</message>
<message>
<location filename="../../designer/mainwindow.py" line="366"/>
<source>osc.upd ip port</source>
<translatorcomment>Platzhalter</translatorcomment>
<translation></translation>
</message>
<message>
<location filename="../../designer/mainwindow.py" line="367"/>
<source>Session Root</source>
<translatorcomment>Platzhalter</translatorcomment>
<translation></translation>
</message>
<message>
<location filename="../../designer/mainwindow.py" line="368"/>
<source>/home/usr/NSM Sessions</source>
<translatorcomment>Platzhalter</translatorcomment>
<translation></translation>
</message>
<message>
<location filename="../../designer/mainwindow.py" line="369"/>
<source>Program Database</source>
<translatorcomment>Platzhalter</translatorcomment>
<translation></translation>
</message>
<message>
<location filename="../../designer/mainwindow.py" line="370"/>
<source>Last Updated</source>
<translatorcomment>Platzhalter</translatorcomment>
<translation></translation>
</message>
<message>
@ -197,7 +187,6 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/mainwindow.py" line="373"/>
<source>Processing</source>
<translatorcomment>Nicht in benutzung</translatorcomment>
<translation>Arbeitet</translation>
</message>
<message>
@ -208,13 +197,11 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/mainwindow.py" line="375"/>
<source>SessionName</source>
<translatorcomment>Platzhalter</translatorcomment>
<translation></translation>
</message>
<message>
<location filename="../../designer/mainwindow.py" line="376"/>
<source>ClientNameId</source>
<translatorcomment>Platzhalter</translatorcomment>
<translation></translation>
</message>
<message>
@ -225,7 +212,6 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/mainwindow.py" line="378"/>
<source>Ctrl+Shift+Q</source>
<translatorcomment>Keine Shortcuts übersetzen</translatorcomment>
<translation></translation>
</message>
<message>
@ -246,7 +232,6 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/mainwindow.py" line="382"/>
<source>Ctrl+Q</source>
<translatorcomment>Keine Shortcuts übersetzen</translatorcomment>
<translation></translation>
</message>
<message>
@ -257,13 +242,11 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/mainwindow.py" line="384"/>
<source>A</source>
<translatorcomment>Keine Shortcuts übersetzen</translatorcomment>
<translation></translation>
</message>
<message>
<location filename="../../designer/mainwindow.py" line="386"/>
<source>Ctrl+S</source>
<translatorcomment>Keine Shortcuts übersetzen</translatorcomment>
<translation></translation>
</message>
<message>
@ -274,13 +257,11 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/mainwindow.py" line="388"/>
<source>Ctrl+Shift+S</source>
<translatorcomment>Keine Shortcuts übersetzen</translatorcomment>
<translation></translation>
</message>
<message>
<location filename="../../designer/mainwindow.py" line="390"/>
<source>Ctrl+W</source>
<translatorcomment>Keine Shortcuts übersetzen</translatorcomment>
<translation></translation>
</message>
<message>
@ -291,7 +272,6 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/mainwindow.py" line="392"/>
<source>Ctrl+Shift+W</source>
<translatorcomment>Keine Shortcuts übersetzen</translatorcomment>
<translation></translation>
</message>
<message>
@ -302,7 +282,6 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/mainwindow.py" line="394"/>
<source>Alt+O</source>
<translatorcomment>Keine Shortcuts übersetzen</translatorcomment>
<translation></translation>
</message>
<message>
@ -313,7 +292,6 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/mainwindow.py" line="396"/>
<source>Alt+R</source>
<translatorcomment>Keine Shortcuts übersetzen</translatorcomment>
<translation></translation>
</message>
<message>
@ -324,7 +302,6 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/mainwindow.py" line="398"/>
<source>Alt+S</source>
<translatorcomment>Keine Shortcuts übersetzen</translatorcomment>
<translation></translation>
</message>
<message>
@ -335,7 +312,6 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/mainwindow.py" line="400"/>
<source>Alt+X</source>
<translatorcomment>Keine Shortcuts übersetzen</translatorcomment>
<translation></translation>
</message>
<message>
@ -346,7 +322,6 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/mainwindow.py" line="402"/>
<source>Alt+T</source>
<translatorcomment>Keine Shortcuts übersetzen</translatorcomment>
<translation></translation>
</message>
<message>
@ -372,7 +347,6 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/mainwindow.py" line="407"/>
<source>F2</source>
<translatorcomment>Keine Shortcuts übersetzen</translatorcomment>
<translation></translation>
</message>
</context>
@ -381,7 +355,6 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/newsession.py" line="47"/>
<source>Dialog</source>
<translatorcomment>Platzhalter</translatorcomment>
<translation></translation>
</message>
<message>
@ -392,8 +365,8 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/newsession.py" line="49"/>
<source>Save JACK Connections
(adds clients &apos;nsm-jack&apos;)</source>
<translation>Speichere JACK-Verbindungen (mit Client &apos;nsm-jack&apos;)</translation>
(adds clients &apos;jackpatch&apos;)</source>
<translation>Speichere JACK-Verbindungen (mit Client &apos;jackpatch&apos;)</translation>
</message>
<message>
<location filename="../../designer/newsession.py" line="51"/>
@ -470,7 +443,6 @@ Für Notizen, TODO, Referenzen, Quellen etc…</translation>
<message>
<location filename="../../designer/projectname.py" line="43"/>
<source>Form</source>
<translatorcomment>Platzhalter</translatorcomment>
<translation></translation>
</message>
<message>

116
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 <http://www.gnu.org/licenses/>.
"""
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)
Loading…
Cancel
Save