class ScriptWindow(QDialog): def __init__(self, parent=None): super(ScriptWindow, self).__init__(parent) self.setGeometry(100, 100, 800, 600) self.parent = parent font = QFontDatabase.systemFont(QFontDatabase.FixedFont) font.setFixedPitch(True) font.setPointSize(10) self.editor = QPlainTextEdit() self.editor.setFont(font) self.editor.setTabStopWidth(self.editor.fontMetrics().width(" ") * 4) self.highlight = PythonHighlighter(self.editor.document()) self.editor.setPlainText("# self.parent is the main GUI handle") self.runScriptButton = QPushButton("Run script") self.layout = QVBoxLayout() self.layout.addWidget(self.editor) self.layout.addWidget(self.runScriptButton) self.setLayout(self.layout) self.setWindowTitle("Script editor") self.setup_button_behavior() self.show() def setup_button_behavior(self): self.runScriptButton.clicked.connect(self.runscript) def runscript(self): print("Script is executed.") print("Testing parent data accessibility") text2execute = self.editor.toPlainText() exec(text2execute)
class DefaultWidget(QWidget): """ The default widget for a repeating element. It's a simple line edit with a label and an option required astrix. """ def __init__( self, label="", line_name="na", required=False, placeholder_text=None, spellings=True, parent=None, ): """ Parameters ---------- label : str The label that appears next to the text edit line_name : str The name to use for the text edit required : boolean Whethere to display the little blue required asterix parent : QWidget """ QWidget.__init__(self, parent=parent) self.layout = QHBoxLayout() self.qlbl = QLabel(label, self) self.added_line = QPlainTextEdit() max_line_height = self.added_line.fontMetrics().height() + 10 self.added_line.setMaximumHeight(max_line_height) self.added_line.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.added_line.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.added_line.mouseMoveEvent = self.mouse_move self.added_line.setLineWrapMode(QPlainTextEdit.NoWrap) self.added_line.textChanged.connect(self.remove_returns) if spellings: self.highlighter = Highlighter(self.added_line.document()) if not placeholder_text is None: self.added_line.setPlaceholderText(placeholder_text) self.added_line.setObjectName(line_name) self.layout.addWidget(self.qlbl) self.layout.addWidget(self.added_line) if required: self.required_label = QLabel(self) font = QFont() font.setFamily("Arial") font.setPointSize(9) font.setBold(False) font.setItalic(False) font.setWeight(50) self.required_label.setFont(font) self.required_label.setScaledContents(True) self.required_label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) self.required_label.setIndent(0) self.required_label.setText( QtCore.QCoreApplication.translate( "", '<html><head/><body><p align="center">' '<span style=" font-size:18pt; color:#55aaff;">*' "</span></p></body></html>", )) self.layout.addWidget(self.required_label) self.layout.setContentsMargins(1, 1, 1, 1) self.layout.setSpacing(10) self.setLayout(self.layout) def mouse_move(self, e): """ over ride the mouse move event to ignore mouse movement (scrolling), but only in the vertical dimension. Makes this plain text edit look and feel like a line edit. Parameters ---------- e : pyqt event Returns ------- None """ if e.y() < self.added_line.height() - 3 and e.x( ) < self.added_line.width() - 3: QPlainTextEdit.mouseMoveEvent(self.added_line, e) self.added_line.verticalScrollBar().setValue( self.added_line.verticalScrollBar().minimum()) def remove_returns(self): """ After changes to the line, we need to make sure that the user has not entered any line returns, since the single line is not intended to be a multiline string. Any line returns "\n" will be replace with a ' ' Returns ------- None """ self.added_line.textChanged.disconnect() old_position = self.added_line.textCursor().position() curtext = self.added_line.toPlainText() newtext = curtext.replace("\n", " ") self.added_line.setPlainText(newtext) cursor = self.added_line.textCursor() cursor.setPosition(old_position) self.added_line.setTextCursor(cursor) self.added_line.textChanged.connect(self.remove_returns) def setText(self, text): utils.set_text(self.added_line, text) cursor = self.added_line.textCursor() cursor.setPosition(0) self.added_line.setTextCursor(cursor) def text(self): return self.added_line.toPlainText()
class DefaultWidget(QWidget): """ The default widget for a repeating element. It's a simple line edit with a label and an option required astrix. """ def __init__(self, label='', line_name='na', required=False, placeholder_text=None, spellings=True, parent=None): """ Parameters ---------- label : str The label that appears next to the text edit line_name : str The name to use for the text edit required : boolean Whethere to display the little blue required asterix parent : QWidget """ QWidget.__init__(self, parent=parent) self.layout = QHBoxLayout() self.qlbl = QLabel(label, self) self.added_line = QPlainTextEdit() max_line_height = self.added_line.fontMetrics().height() + 10 self.added_line.setMaximumHeight(max_line_height) self.added_line.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.added_line.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.added_line.setLineWrapMode(QPlainTextEdit.NoWrap) self.added_line.textChanged.connect(self.remove_returns) if spellings: self.highlighter = Highlighter(self.added_line.document()) if not placeholder_text is None: self.added_line.setPlaceholderText(placeholder_text) self.added_line.setObjectName(line_name) self.layout.addWidget(self.qlbl) self.layout.addWidget(self.added_line) if required: self.required_label = QLabel(self) font = QFont() font.setFamily("Arial") font.setPointSize(9) font.setBold(False) font.setItalic(False) font.setWeight(50) self.required_label.setFont(font) self.required_label.setScaledContents(True) self.required_label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) self.required_label.setIndent(0) self.required_label.setText( QtCore.QCoreApplication.translate( "", "<html><head/><body><p align=\"center\">" "<span style=\" font-size:18pt; color:#55aaff;\">*" "</span></p></body></html>")) self.layout.addWidget(self.required_label) self.layout.setContentsMargins(1, 1, 1, 1) self.layout.setSpacing(10) self.setLayout(self.layout) def remove_returns(self): self.added_line.textChanged.disconnect() cursor = self.added_line.textCursor() curtext = self.added_line.toPlainText() newtext = curtext.replace('\n', ' ') self.added_line.setPlainText(newtext) self.added_line.setTextCursor(cursor) self.added_line.textChanged.connect(self.remove_returns) def setText(self, text): utils.set_text(self.added_line, text) cursor = self.added_line.textCursor() cursor.setPosition(0) self.added_line.setTextCursor(cursor) def text(self): return self.added_line.toPlainText()
class WeatherInput(QWidget): """ Weather parameter input for GUI. Contains selector on whether to scrape, import, or ignore with corresponding windows for each based on current selection e.g. parameter input fields if scraping. """ def __init__(self): super().__init__() self.__init_weather() self.__init_design() def __init_weather(self): self.layout = QVBoxLayout() self.__scrape() self.__import() self.__none() self.__revolver() self.__selector() self.layout.addWidget(self.selector) self.layout.addWidget(self.revolver) self.layout.addWidget(QLabel("Powered by the <a href=\"https://www.weatherbit.io/\">Weatherbit API</a>")) self.setLayout(self.layout) def __scrape(self): input_fields = [("City", "text"), ("State", "text"), ("Country", "text"), ("Days", "text"), ("weatherbit.io Key", "text")] input_defaults = {"City": "Cambridge", "State": "MA", "Country": "US", "Days": "14"} self.scrape_layout, self.scrape_fields = basic_form_creator(input_fields, input_defaults) # adding "Imperial Units" and "Save Weather" options as checkboxes in same row. imperial = QCheckBox("Imperial Units") imperial.setChecked(True) # default is checked save = QCheckBox("Save") self.scrape_fields["Imperial Units"] = imperial self.scrape_fields["Save Weather"] = save self.checkbox_layout = QHBoxLayout() self.checkbox_layout.addWidget(imperial) self.checkbox_layout.addWidget(save) self.checkbox_layout.addStretch() checkbox = QWidget() checkbox.setLayout(self.checkbox_layout) self.scrape_layout.addWidget(checkbox) self.scrape = QWidget(self) self.scrape.setLayout(self.scrape_layout) def __import(self): self.import_layout = QVBoxLayout() button = QPushButton("Browse") self.import_path = QPlainTextEdit() font_height = self.import_path.fontMetrics().height() num_rows = 5 # size of browse window, plenty of space to display full path self.import_path.setFixedHeight(int(num_rows * font_height)) self.import_layout.addWidget(button, 0, Qt.AlignTop | Qt.AlignLeft) # align top-left corner self.import_layout.addWidget(self.import_path, 0, Qt.AlignTop | Qt.AlignLeft) button.clicked.connect(lambda: self.import_path.setPlainText(QFileDialog.getOpenFileName()[0])) self.import_ = QWidget(self) self.import_.setLayout(self.import_layout) def __selector(self): self.selector_layout = QHBoxLayout() self.selector_button_group = QButtonGroup() self.selector_button_group.setExclusive(True) self.none_button = QRadioButton("None") self.scrape_button = QRadioButton("Scrape") self.import_button = QRadioButton("Import") self.selector_button_group.addButton(self.none_button, 1) self.selector_button_group.addButton(self.scrape_button, 2) # need 1-index because 0 means not found self.selector_button_group.addButton(self.import_button, 3) self.selector_layout.addWidget(self.none_button) self.selector_layout.addWidget(self.scrape_button) self.selector_layout.addWidget(self.import_button) self.selector_layout.addStretch() self.scrape_button.setChecked(True) self.selector = QWidget(self) self.selector.setLayout(self.selector_layout) def __none(self): self.none_ = QWidget() def __revolver(self): self.revolver = QStackedWidget() self.revolver.addWidget(self.none_) self.revolver.addWidget(self.scrape) self.revolver.addWidget(self.import_) self.revolver.setCurrentIndex(1) # default setting is scrape def switch_window(self, button): """ Switch input fields based on current selection. :param button: radio button of current selection :return: nothing, mutates object """ self.revolver.setCurrentIndex(self.selector_button_group.id(button) - 1) # make up for 1-index def __init_design(self): self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) self.scrape_layout.setContentsMargins(0, 3, 11, 0) # effort to line up to TwilightInput self.scrape_layout.setSpacing(5) self.import_layout.setSpacing(10) self.import_layout.addStretch(1) self.selector_layout.setContentsMargins(0, 0, 0, 11) self.checkbox_layout.setContentsMargins(0, 0, 0, 0) self.scrape_layout.addStretch(25) # stretch large enough to move checkboxes close to other fields self.default_border = self.scrape_fields["City"].styleSheet() # just get from any existing line edit self.error_border = "border: 1px solid red;"
class ScriptWriterDialog(QDialog): def __init__(self, parent=None): super(ScriptWriterDialog, self).__init__(parent) self.setGeometry(200, 100, 650, 700) font = QFontDatabase.systemFont(QFontDatabase.FixedFont) font.setFixedPitch(True) font.setPointSize(11) self.editor = QPlainTextEdit() self.editor.setFont(font) self.editor.setPlainText(templatestr) self.editor.setTabStopWidth(self.editor.fontMetrics().width(" ") * 4) # access the 'text' of QPlainTextEdit by self.highlight = ScriptHighlighter(self.editor.document()) # replace system stdout with custom object to hijack it sys.stdout = Stream(newText=self.onUpdateText) self.stdoutbox = QTextEdit() self.stdoutbox.moveCursor(QTextCursor.Start) self.stdoutbox.setFixedHeight(150) self.loadScriptButton = QPushButton("Load script") self.checkSpacesButton = QPushButton("Convert tabs") self.saveScriptButton = QPushButton("Save script") self.runScriptButton = QPushButton("Run Script") self.buttonsLayout = QHBoxLayout() self.buttonsLayout.addWidget(self.loadScriptButton) self.buttonsLayout.addWidget(self.checkSpacesButton) self.buttonsLayout.addWidget(self.saveScriptButton) self.buttonsLayout.addWidget(self.runScriptButton) self.layout = QVBoxLayout() self.currentfilelabel = QLabel("No scripts loaded") self.layout.addWidget(self.currentfilelabel) self.layout.addWidget(self.editor) self.layout.addLayout(self.buttonsLayout) self.layout.addWidget(QLabel("status message:")) self.layout.addWidget(self.stdoutbox) self.setLayout(self.layout) self.setWindowTitle("easyAMPS Script editor v0.1") self.setup_button_behavior() def setup_button_behavior(self): self.loadScriptButton.clicked.connect(self.loadscript) self.checkSpacesButton.clicked.connect(self.converttabs) self.saveScriptButton.clicked.connect(self.savescript) self.runScriptButton.clicked.connect(self.runscript) def loadscript(self): options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog fileName, fileType = QFileDialog.getOpenFileName( self, "Open AMPS yaml configuration script", "", "YAML file (*.yaml)", options=options, ) if fileName: self.currentfilepath = Path(fileName) self.currentfilelabel.setText(str(self.currentfilepath)) # raw text is loaded too with open(self.currentfilepath, "rt") as fhd: scriptbody = "".join(fhd.readlines()) self.editor.setPlainText(scriptbody) print("Script is loaded.") else: return False pass def converttabs(self): scriptbody = self.editor.toPlainText() scriptbody = scriptbody.replace("\t", " ") print("Tabs have been replaced by spaces, check the script") # get current cursor position cursor = self.editor.textCursor() cursor.beginEditBlock() # load the replaced script self.editor.setPlainText(scriptbody) cursor.endEditBlock() def savescript(self): options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog fileName, fileType = QFileDialog.getSaveFileName( self, "Save AMPS yaml configuration script", "", "YAML file (*.yaml)", options=options, ) if fileName: targetfile = Path(fileName).with_suffix(".yaml") # get text body scriptbody = self.editor.toPlainText() # replace tabs with spaces scriptbody = scriptbody.replace("\t", " ") with open(targetfile, "wt") as fhd: fhd.write(scriptbody) print(f"Saved to {targetfile}") @safecall def runscript(self): """ Execute script from editor """ scriptbody = self.editor.toPlainText() scriptbody = scriptbody.replace("\t", " ") scriptconfig = yaml.load(scriptbody, Loader=yaml.FullLoader) print("Running script ...") inputdir = Path(scriptconfig["directories"]["input"]) outdir = Path(scriptconfig["directories"]["output"]) print(f"Reading input files from : {inputdir}") print(f"Processed files will be saved to : {outdir}") if not outdir.exists(): outdir.mkdir(exist_ok=True, parents=True) experiments = { name: Path(inputdir / filename) for name, filename in scriptconfig["experiments"].items() } # load blank file blank = read_detail(experiments["blank"]) # go through each experiment for expt_key in experiments.keys(): if expt_key != "blank": print(f"Experiment : {expt_key}") if expt_key not in scriptconfig.keys(): # skip experiment that has no configurations print( f"Could not find configuration for '{expt_key}' experiment. Skipping it.'" ) continue else: alldata = read_detail(experiments[expt_key], label_rows=False) timepoints = alldata["Read #"].unique().tolist() lastread = timepoints[-1] print(f"Found {len(timepoints)} read # :") print(timepoints) data = alldata subtract_blank_fluorescence(data, blank) datadict = process_experiment(data, scriptconfig, expt_key, transpose_block=False) for name, dataframe in datadict.items(): output_filename = f"{name}.csv" output_target = outdir / output_filename dataframe.to_csv(f"{str(output_target)}", index=False) def onUpdateText(self, text): cursor = self.stdoutbox.textCursor() cursor.movePosition(QTextCursor.End) cursor.insertText(text) self.stdoutbox.setTextCursor(cursor) self.stdoutbox.ensureCursorVisible() def __del__(self): sys.stdout = sys.__stdout__