diff --git a/Makefile b/Makefile index 2531cc2..8752b31 100644 --- a/Makefile +++ b/Makefile @@ -173,12 +173,22 @@ test3: test4: python pysimplegui.py + ## test5: native pyqt, tkrun style test5: - python pyqt.py testfile + python pyqt.py testfile.sh +#bash format with more variables test6: - python pyqt.py testfile2 + python pyqt.py testfile2.sh + +#python format +test7: + python pyqt.py testfile3.py + +#csh format +test8: + python pyqt.py testfile4.csh ## test7: qooey, athena style test7: diff --git a/pyqt.py b/pyqt.py index 4bdf51b..c684716 100644 --- a/pyqt.py +++ b/pyqt.py @@ -1,53 +1,51 @@ import sys -from PyQt5 import QtCore, QtGui, QtWidgets +import os +from PyQt5 import QtCore, QtWidgets import argparse import re +import subprocess class MainWindow(QtWidgets.QMainWindow): - def __init__(self, parameters): + def __init__(self, parameters, param_file, filetype): super(MainWindow, self).__init__() - self.groups = groups + self.groups = parameters self.radio_groups = [] - self.sliderlabels = [] + self.param_file = param_file + self.param_file_type = filetype + self.sliderMultiplier = [] + self.sliders = [] self.initUI() def initUI(self): self.pagelayout = QtWidgets.QVBoxLayout() #page layout - self.dbtnlayout = QtWidgets.QHBoxLayout() #layout for the default buttons - self.elmtlayout = QtWidgets.QVBoxLayout() #layout for the added widgets, stacks elements - - #add layouts to the page - self.pagelayout.addLayout(self.dbtnlayout) - self.pagelayout.addLayout(self.elmtlayout) - - #run, save, load, quit, help button - btn = QtWidgets.QPushButton(self) - btn.setText("run") - btn.clicked.connect(self.run) - self.dbtnlayout.addWidget(btn) - - btn = QtWidgets.QPushButton(self) - btn.setText("save") - btn.clicked.connect(self.save) - self.dbtnlayout.addWidget(btn) - - btn = QtWidgets.QPushButton(self) - btn.setText("load") - btn.clicked.connect(self.load) - self.dbtnlayout.addWidget(btn) - - btn = QtWidgets.QPushButton(self) - btn.setText("quit") - btn.clicked.connect(self.quit) - self.dbtnlayout.addWidget(btn) - - btn = QtWidgets.QPushButton(self) - btn.setText("help") - btn.clicked.connect(self.help) - self.dbtnlayout.addWidget(btn) + + #run, save, load, quit, help buttons -> located in a toolbar + toolbar = self.addToolBar("ToolBar") + + run_action = QtWidgets.QAction('Run', self) + save_action = QtWidgets.QAction('Save', self) + load_action = QtWidgets.QAction('Load', self) + quit_action = QtWidgets.QAction('Quit', self) + help_action = QtWidgets.QAction('Help', self) + + toolbar.addAction(run_action) + toolbar.addSeparator() + toolbar.addAction(save_action) + toolbar.addSeparator() + toolbar.addAction(load_action) + toolbar.addSeparator() + toolbar.addAction(quit_action) + toolbar.addSeparator() + toolbar.addAction(help_action) + + run_action.triggered.connect(self.run) + save_action.triggered.connect(self.save) + load_action.triggered.connect(self.load) + quit_action.triggered.connect(self.quit) + help_action.triggered.connect(self.help) #set the main page layout widget = QtWidgets.QWidget() @@ -55,20 +53,69 @@ def initUI(self): scroll = QtWidgets.QScrollArea() #add scrollbar scroll.setWidgetResizable(True) scroll.setWidget(widget) - self.setGeometry(500, 100, 700, 500) self.setCentralWidget(scroll) self.createWidgetsFromGroups() def run(self): - print('run') + contents = self.gatherData() + param = "" + for line in contents: + for key, value in line.items(): + param += f"{key}={value} " + print(param.split()) + subprocess.run([self.param_file_type, self.param_file] + param.split()) def save(self): - print('save') + contents = self.gatherData() + default_file_path = self.param_file + ".key" + file_path, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save File", default_file_path, "All Files (*)") + if file_path: + with open(file_path, "w") as file: + for line in contents: + for key, value in line.items(): + file.write(f"{key}={value}") + file.write("\n") + print("saved to " + file_path) def load(self): - print('load') - + options = QtWidgets.QFileDialog.Options() + load_file = self.param_file +".key" if os.path.exists(self.param_file +".key") else "" + file, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Choose a File", load_file, "All Files (*)", options=options) + + #parse the loaded file + if file: + #turn the parameters in the file into a dictionary + default_values = {} + with open(file, "r") as f: + for line in f: + if self.param_file_type == 'csh': + line = re.sub("set","",line,count=1) + label, value = line.strip().split("=") + if value.startswith('"') and value.endswith('"') or value.startswith("'") and value.endswith("'"): + value = value[1:-1] + value = value.split(",") if "," in value else [value] + default_values[label] = value + + #go through the elements in the widget and alter it to the values specified + for elements in range(self.pagelayout.count()): + element = self.pagelayout.itemAt(elements).layout() + if element is not None: + for widget_index in range(element.count()): + widget = element.itemAt(widget_index).widget() + + if isinstance(widget, QtWidgets.QLineEdit): + widget.setText(''.join(default_values[widget.objectName()])) + + elif isinstance(widget, QtWidgets.QRadioButton) or isinstance(widget, QtWidgets.QCheckBox): + if widget.text() in default_values[widget.objectName()]: + widget.setChecked(True) + + elif isinstance(widget, QtWidgets.QSlider): + multiplier = self.sliderMultiplier.pop(0) + widget.setValue(int(float(''.join(default_values[widget.objectName()]))*multiplier)) + self.sliderMultiplier.append(multiplier) + def quit(self): self.close() print('quit') @@ -86,117 +133,211 @@ def createWidgetsFromGroups(self): self.radio_groups.append(new_group) group_layout = QtWidgets.QHBoxLayout() label = QtWidgets.QLabel(group_name+":") + label.setToolTip(help) group_layout.addWidget(label) for option in options: option = option.strip() radio_button = QtWidgets.QRadioButton(option) + radio_button.setObjectName(group_name) new_group.addButton(radio_button) group_layout.addWidget(radio_button) if option in default_option: radio_button.setChecked(True) - self.elmtlayout.addLayout(group_layout) + self.pagelayout.addLayout(group_layout) - elif group_type == "IFILE" or group_type == "OFILE" or group_type == "IDIR": + elif group_type == "IFILE" or group_type == "OFILE" or group_type == "IDIR" or group_type == "ODIR": print("browse files button created") group_layout = QtWidgets.QHBoxLayout() label = QtWidgets.QLabel(group_name+":") + label.setToolTip(help) group_layout.addWidget(label) btn = QtWidgets.QPushButton(self) btn.setText("browse...") - def browse(): - file = QtWidgets.QFileDialog.getOpenFileNames(self, "Select File", "") - print(file) - btn.clicked.connect(browse) + txt = QtWidgets.QLineEdit(self) + txt.setText(default_option) + txt.setObjectName(group_name) + if group_type == "OFILE" or group_type == "IFILE": + btn.clicked.connect(lambda edit=txt: self.browse("FILE", edit)) + else: + btn.clicked.connect(lambda edit=txt: self.browse("DIR", edit)) group_layout.addWidget(btn) group_layout.addWidget(txt) - self.elmtlayout.addLayout(group_layout) + self.pagelayout.addLayout(group_layout) elif group_type == "CHECK": print("checkbox created") group_layout = QtWidgets.QHBoxLayout() label = QtWidgets.QLabel(group_name+":") + label.setToolTip(help) group_layout.addWidget(label) for option in options: option = option.strip() checkbox = QtWidgets.QCheckBox(option, self) + checkbox.setObjectName(group_name) group_layout.addWidget(checkbox) if option in default_option: checkbox.setChecked(True) - self.elmtlayout.addLayout(group_layout) + self.pagelayout.addLayout(group_layout) elif group_type == "ENTRY": print("textbox created") group_layout = QtWidgets.QHBoxLayout() label = QtWidgets.QLabel(group_name+":") + label.setToolTip(help) group_layout.addWidget(label) txt = QtWidgets.QLineEdit(self) + txt.setObjectName(group_name) txt.setText(default_option) group_layout.addWidget(txt) - self.elmtlayout.addLayout(group_layout) + self.pagelayout.addLayout(group_layout) elif group_type == "SCALE": group_layout = QtWidgets.QHBoxLayout() label = QtWidgets.QLabel(group_name+":") + label.setToolTip(help) group_layout.addWidget(label) options = ''.join(options) options = options.split(':') print("slider created") - #creates a horizontal slider + #creates a horizontal decimal slider + decimals = len(str(options[2]).split('.')[1]) if '.' in str(options[2]) else 0 + multiplier = 10**decimals slider = QtWidgets.QSlider(self) slider.setOrientation(QtCore.Qt.Horizontal) - slider.setSingleStep(int(float(options[2])*100)) - slider.setPageStep(int(float(options[2])*100)) #moves the slider when clicking or up/down - slider.setRange(int(options[0])*100, int(options[1])*100) - slider.setValue(int(float(default_option[0])*100)) + slider.setSingleStep(int(float(options[2])*multiplier)) + slider.setPageStep(int(float(options[2])*multiplier)) #moves the slider when clicking or up/down + slider.setRange(int(options[0])*multiplier, int(options[1])*multiplier) + slider.setValue(int(float(default_option[0])*multiplier)) - label_slider = QtWidgets.QLabel(str(default_option[0])) - slider.valueChanged.connect(lambda value, lbl=label_slider: self.updateLabel(lbl, value)) - - group_layout.addWidget(label_slider) + slider_label = QtWidgets.QLabel(f"{slider.value()/multiplier}", self) + slider.valueChanged.connect(lambda: self.updateLabel()) + slider.setObjectName(group_name) + self.sliderMultiplier.append(multiplier) + self.sliders.append((slider, slider_label, multiplier)) + group_layout.addWidget(slider_label) group_layout.addWidget(slider) - self.elmtlayout.addLayout(group_layout) + self.pagelayout.addLayout(group_layout) - def updateLabel(self, label, value): - label.setText(str(value/100)) + # Create a visual separator (horizontal line) + separator = QtWidgets.QFrame() + separator.setFrameShape(QtWidgets.QFrame.HLine) + separator.setFrameShadow(QtWidgets.QFrame.Sunken) + separator.setFixedHeight(1) # Set a fixed height for the separator + self.pagelayout.addWidget(separator) -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="Dynamic GUI Builder") - parser.add_argument("param_file", help="Path to the text file containing parameters") - args = parser.parse_args() + def updateLabel(self): + for slider, label, multiplier in self.sliders: + label.setText(f"{slider.value()/multiplier}") + + def browse(self, gtype, txt): + options = QtWidgets.QFileDialog.Options() + file = None + dir = None + + if gtype == 'FILE': + file, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Choose a File", "", "All Files (*)", options=options) + if gtype == 'DIR': + dir = QtWidgets.QFileDialog.getExistingDirectory(self, "Select Directory") + if file: + txt.setText(file) + print(file + " selected") + if dir: + print(f"{dir} selected") - with open(args.param_file, "r") as file: - # content = file.read() - lines = file.readlines() + def gatherData(self): + layout_data = [] + + for hbox_layout_index in range(self.pagelayout.count()): + hbox_layout = self.pagelayout.itemAt(hbox_layout_index).layout() + + if hbox_layout is not None: + defaults = {} + key = None + + for widget_index in range(hbox_layout.count()): + widget = hbox_layout.itemAt(widget_index).widget() + + if widget_index == 0 and isinstance(widget, QtWidgets.QLabel): + key = widget.text().split(':')[0] + if self.param_file_type == "csh": + key = "set " + key + defaults[key] = [] + + elif isinstance(widget, QtWidgets.QLineEdit): + value = widget.text() + defaults[key].append(value) + + elif isinstance(widget, QtWidgets.QRadioButton) or isinstance(widget, QtWidgets.QCheckBox): + if widget.isChecked(): + value = widget.text() + defaults[key].append(value) + + elif isinstance(widget, QtWidgets.QSlider): + multiplier = self.sliderMultiplier.pop(0) + value = str(widget.value()/multiplier) + defaults[key].append(value) + self.sliderMultiplier.append(multiplier) + + values = defaults[key] + values = ','.join(values) + + if self.param_file_type == "python": + defaults[key] = "'" + values + "'" + else: + defaults[key] = values + layout_data.append(defaults) + return layout_data + +def parsefile(file): + filetype = "sh" + with open(file, "r") as f: + lines = f.readlines() groups = [] - # matches = re.findall(r'#>\s+(\w+)\s+(\w+)=(.+?)(?:\n|#>|$)', content, re.DOTALL) - # for match in matches: - # group_type = match[0] - # group_name = match[1] - # options = match[2].split(' ', 1) - # default_option = [] - # if len(options) > 1: - # default_option = options[0].split(',') - # options = options[1].split(',') - pattern = r"\s*(\w+)\s*=\s*([^\s#]+)\s*#\s*([^\#]+)\s*#\s*>\s*(\w+)(?:\s+(\S+))?" + # group 1 = set or None + # group 2 = name of widget + # group 3 = default values, may have "" around + # group 4 = # help or None + # group 5 = widget type + # group 6 (unused) = name=value if old format, otherwise None + # group 7 = widget parameters or None + pattern = '^\s*(set\s+)?([^#]+)\s*=([^#]+)(#.*)?#>\s+([^\s]+)(.*=[^\s]*)?(.+)?$' for line in lines: match = re.match(pattern, line) if match: - group_type = match.group(4) - group_name = match.group(1) - default_option = match.group(2) - options = match.group(5).split(',') if match.group(5) else "" - help = match.group(3) - + if match.group(1): + filetype = "csh" + group_type = match.group(5).strip() + group_name = match.group(2).strip() + default_option = match.group(3).strip() + #check for quotations + if (default_option[0] == '"' and default_option[-1] == '"') or (default_option[0] == "'" and default_option[-1] == "'"): + default_option = default_option[1:-1] + filetype = "python" + options = match.group(7).split(',') if match.group(7) else "" + help = match.group(4).split('#')[1].strip() if match.group(4) else "" groups.append((group_type, group_name, options, default_option, help)) + + return groups, filetype + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description="Dynamic GUI Builder") + parser.add_argument("param_file", help="Path to the text file containing parameters") + args = parser.parse_args() + + groups, filetype = parsefile(args.param_file) app = QtWidgets.QApplication(sys.argv) - w = MainWindow(groups) - w.show() + w = MainWindow(groups, args.param_file, filetype) + w.inputFile = args.param_file + w.adjustSize() #adjust to fit elements accordingly - print(sys.argv) + #sets a minimum window size + w.setMinimumWidth(500) + w.setMinimumHeight(200) + w.show() try: print('opening window') diff --git a/testfile b/testfile.sh similarity index 98% rename from testfile rename to testfile.sh index 1410365..29355c8 100644 --- a/testfile +++ b/testfile.sh @@ -7,7 +7,7 @@ dir3=fum # help for input dir3 #> IDIR dir4=baz # help for output dir4 #> ODIR hello=world # help for text entry hello #> ENTRY - a=1 # help for a, between 0 and 2 #> SCALE 0:2:0.1 + a=1 # help for a, between 0 and 2 #> SCALE 0:100:50 b=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 c=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c diff --git a/testfile2 b/testfile2 deleted file mode 100644 index e9f38dc..0000000 --- a/testfile2 +++ /dev/null @@ -1,39 +0,0 @@ -#! /usr/bin/env bash -# -# new 2023 style bash - - file1=foo # help for input file1 #> IFILE - file2=bar # help for output file2 #> OFILE - dir3=fum # help for input dir3 #> IDIR - dir4=baz # help for output dir4 #> ODIR - hello=world # help for text entry hello #> ENTRY - a=1 # help for a, between 0 and 2 #> SCALE 0:2:0.1 - a=3 # help for a, between 0 and 2 #> SCALE 0:10:0.1 - a=15 # help for a, between 0 and 2 #> SCALE 10:20:1 - b=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 - c=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c - b=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 - c=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c - b=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 - c=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c - b=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 - c=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c - b=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 - c=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c - b=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 - c=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c - b=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 - c=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c - b=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 - c=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c - b=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 - c=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c - b=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 - c=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c - b=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 - c=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c - b=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 - c=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c - - b=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 - c=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c \ No newline at end of file diff --git a/testfile2.sh b/testfile2.sh new file mode 100644 index 0000000..457210a --- /dev/null +++ b/testfile2.sh @@ -0,0 +1,37 @@ +#! /usr/bin/env bash +# +# new 2023 style bash + + file1=foo # help for input file1 #> IFILE + file2=bar # help for output file2 #> OFILE + dir3=fum # help for input dir3 #> IDIR + dir4=baz # help for output dir4 #> ODIR + hello=world # help for text entry hello #> ENTRY +slide1=1 # help for a, between 0 and 2 #> SCALE 0:2:0.1 +slide2=2 # help for a, between 0 and 2 #> SCALE 0:10:2 +slide3=0 # help for a, between 0 and 2 #> SCALE 0:1:0.01 +slide4=0.001 # help for a, between 0 and 2 #> SCALE 0:1:0.001 + c=15 # help for a, between 0 and 2 #> SCALE 10:20:1 + d=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 + e=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c + f=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 + g=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c + h=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 + i=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c + j=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 + k=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c + l=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 + m=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c + n=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 + o=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c + p=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 + q=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c + r=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 + s=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c + t=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 + u=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c + v=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 + w=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c + x=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 + y=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c + z=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 \ No newline at end of file diff --git a/testfile3.py b/testfile3.py new file mode 100644 index 0000000..6e4ca93 --- /dev/null +++ b/testfile3.py @@ -0,0 +1,8 @@ +file1="foo" #> IFILE + file2="bar" # help for output file2 #> OFILE + dir3="fum" # help for input dir3 #> IDIR + dir4="baz" # help for output dir4 #> ODIR + hello="world" # help for text entry hello #> ENTRY + a=1 # help for a, between 0 and 2 #> SCALE 0:100:50 + b=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 + c="3,c" # help for c, check any of 6 #> CHECK 0,1,2,a,b,c \ No newline at end of file diff --git a/testfile4.csh b/testfile4.csh new file mode 100644 index 0000000..9f91d67 --- /dev/null +++ b/testfile4.csh @@ -0,0 +1,8 @@ +set file1=foo # help for input file1 #> IFILE +set file2=bar # help for output file2 #> OFILE +set dir3=fum # help for input dir3 #> IDIR +set dir4=baz # help for output dir4 #> ODIR +set hello=world # help for text entry hello #> ENTRY +set a=1 # help for a, between 0 and 2 #> SCALE 0:100:50 +set b=2 # help for b, pick 1, 2 or 3 #> RADIO 0,1,2 +set c=3,c # help for c, check any of 6 #> CHECK 0,1,2,a,b,c \ No newline at end of file