class CustomAutoCompleter(QCompleter): def __init__(self, parent=None): super(CustomAutoCompleter, self).__init__(parent) self.global_completion_prefix = "" self.source_model = QStandardItemModel() self.setCompletionMode(QCompleter.PopupCompletion) def setModel(self, model): self.source_model = model super(CustomAutoCompleter, self).setModel(self.source_model) def updateModel(self): # Create and Sort Item in a local AutoCompletion list self.local_completion_prefix = self.global_completion_prefix local_model = QStandardItemModel() sorting_model_list = self.source_model.findItems( self.local_completion_prefix, Qt.MatchStartsWith) i = 0 # This is not the cleanest idea, there should be a way to do this with a better optimization # But in python, who really care about opti'? ;) for mod in sorting_model_list: item = QStandardItem(mod) local_model.setItem(i, 0, item) i += 1 for mod in self.source_model.findItems(self.local_completion_prefix, Qt.MatchContains): if len(local_model.findItems(mod.text())) == 0: item = QStandardItem(mod) local_model.setItem(i, 0, item) i += 1 proxy_model = InnerProxyModel.InnerProxyModel(self) proxy_model.setSourceModel(local_model) super(CustomAutoCompleter, self).setModel(proxy_model) def splitPath(self, path): self.global_completion_prefix = path self.updateModel() return ""
class AircraftTypeList(QListView): page_index_changed = Signal(int) def __init__(self, air_wing: AirWing) -> None: super().__init__() self.setIconSize(QSize(91, 24)) self.setMinimumWidth(300) self.item_model = QStandardItemModel(self) self.setModel(self.item_model) self.selectionModel().setCurrentIndex(self.item_model.index(0, 0), QItemSelectionModel.Select) self.selectionModel().selectionChanged.connect( self.on_selection_changed) for aircraft in air_wing.squadrons: self.add_aircraft_type(aircraft) def remove_aircraft_type(self, aircraft: AircraftType): for item in self.item_model.findItems(aircraft.name): self.item_model.removeRow(item.row()) self.page_index_changed.emit( self.selectionModel().currentIndex().row()) def add_aircraft_type(self, aircraft: AircraftType): aircraft_item = QStandardItem(aircraft.name) icon = self.icon_for(aircraft) if icon is not None: aircraft_item.setIcon(icon) aircraft_item.setEditable(False) aircraft_item.setSelectable(True) self.item_model.appendRow(aircraft_item) def on_selection_changed(self, selected: QItemSelection, _deselected: QItemSelection) -> None: indexes = selected.indexes() if len(indexes) > 1: raise RuntimeError( "Aircraft list should not allow multi-selection") if not indexes: return self.page_index_changed.emit(indexes[0].row()) @staticmethod def icon_for(aircraft: AircraftType) -> Optional[QIcon]: name = aircraft.dcs_id if name in AIRCRAFT_ICONS: return QIcon(AIRCRAFT_ICONS[name]) return None
def updateModel(self): # Create and Sort Item in a local AutoCompletion list self.local_completion_prefix = self.global_completion_prefix local_model = QStandardItemModel() sorting_model_list = self.source_model.findItems( self.local_completion_prefix, Qt.MatchStartsWith) i = 0 # This is not the cleanest idea, there should be a way to do this with a better optimization # But in python, who really care about opti'? ;) for mod in sorting_model_list: item = QStandardItem(mod) local_model.setItem(i, 0, item) i += 1 for mod in self.source_model.findItems(self.local_completion_prefix, Qt.MatchContains): if len(local_model.findItems(mod.text())) == 0: item = QStandardItem(mod) local_model.setItem(i, 0, item) i += 1 proxy_model = InnerProxyModel.InnerProxyModel(self) proxy_model.setSourceModel(local_model) super(CustomAutoCompleter, self).setModel(proxy_model)
class ToolSpecificationWidget(QWidget): def __init__(self, toolbox, tool_specification=None): """A widget to query user's preferences for a new tool specification. Args: toolbox (ToolboxUI): QMainWindow instance tool_specification (ToolSpecification): If given, the form is pre-filled with this specification """ from ..ui.tool_specification_form import Ui_Form super().__init__(parent=toolbox, f=Qt.Window) # Inherit stylesheet from ToolboxUI # Setup UI from Qt Designer file self.ui = Ui_Form() self.ui.setupUi(self) # Class attributes self._toolbox = toolbox self._project = self._toolbox.project() # init models self.sourcefiles_model = QStandardItemModel() self.inputfiles_model = QStandardItemModel() self.inputfiles_opt_model = QStandardItemModel() self.outputfiles_model = QStandardItemModel() # Add status bar to form self.statusbar = QStatusBar(self) self.statusbar.setFixedHeight(20) self.statusbar.setSizeGripEnabled(False) self.statusbar.setStyleSheet(STATUSBAR_SS) self.ui.horizontalLayout_statusbar_placeholder.addWidget(self.statusbar) # init ui self.ui.treeView_sourcefiles.setModel(self.sourcefiles_model) self.ui.treeView_inputfiles.setModel(self.inputfiles_model) self.ui.treeView_inputfiles_opt.setModel(self.inputfiles_opt_model) self.ui.treeView_outputfiles.setModel(self.outputfiles_model) self.ui.treeView_sourcefiles.setStyleSheet(TREEVIEW_HEADER_SS) self.ui.treeView_inputfiles.setStyleSheet(TREEVIEW_HEADER_SS) self.ui.treeView_inputfiles_opt.setStyleSheet(TREEVIEW_HEADER_SS) self.ui.treeView_outputfiles.setStyleSheet(TREEVIEW_HEADER_SS) self.ui.comboBox_tooltype.addItem("Select type...") self.ui.comboBox_tooltype.addItems(TOOL_TYPES) # if a specification is given, fill the form with data from it if tool_specification: self.ui.lineEdit_name.setText(tool_specification.name) check_state = Qt.Checked if tool_specification.execute_in_work else Qt.Unchecked self.ui.checkBox_execute_in_work.setCheckState(check_state) self.ui.textEdit_description.setPlainText(tool_specification.description) self.ui.lineEdit_args.setText(" ".join(tool_specification.cmdline_args)) tool_types = [x.lower() for x in TOOL_TYPES] index = tool_types.index(tool_specification.tooltype) + 1 self.ui.comboBox_tooltype.setCurrentIndex(index) # Init lists self.main_program_file = "" self.sourcefiles = list(tool_specification.includes) if tool_specification else list() self.inputfiles = list(tool_specification.inputfiles) if tool_specification else list() self.inputfiles_opt = list(tool_specification.inputfiles_opt) if tool_specification else list() self.outputfiles = list(tool_specification.outputfiles) if tool_specification else list() self.def_file_path = tool_specification.def_file_path if tool_specification else None self.program_path = tool_specification.path if tool_specification else None self.definition = dict() # Get first item from sourcefiles list as the main program file try: self.main_program_file = self.sourcefiles.pop(0) self.ui.lineEdit_main_program.setText(os.path.join(self.program_path, self.main_program_file)) except IndexError: pass # sourcefiles list is empty # Populate lists (this will also create headers) self.populate_sourcefile_list(self.sourcefiles) self.populate_inputfiles_list(self.inputfiles) self.populate_inputfiles_opt_list(self.inputfiles_opt) self.populate_outputfiles_list(self.outputfiles) self.ui.lineEdit_name.setFocus() self.ui.label_mainpath.setText(self.program_path) # Add includes popup menu self.add_source_files_popup_menu = AddIncludesPopupMenu(self) self.ui.toolButton_add_source_files.setMenu(self.add_source_files_popup_menu) self.ui.toolButton_add_source_files.setStyleSheet('QToolButton::menu-indicator { image: none; }') # Add create new or add existing main program popup menu self.add_main_prgm_popup_menu = CreateMainProgramPopupMenu(self) self.ui.toolButton_add_main_program.setMenu(self.add_main_prgm_popup_menu) self.ui.toolButton_add_source_files.setStyleSheet('QToolButton::menu-indicator { image: none; }') self.ui.toolButton_add_cmdline_tag.setMenu(self._make_add_cmdline_tag_menu()) self.connect_signals() def connect_signals(self): """Connect signals to slots.""" self.ui.toolButton_add_source_files.clicked.connect(self.show_add_source_files_dialog) self.ui.toolButton_add_source_dirs.clicked.connect(self.show_add_source_dirs_dialog) self.ui.lineEdit_main_program.file_dropped.connect(self.set_main_program_path) self.ui.treeView_sourcefiles.files_dropped.connect(self.add_dropped_includes) self.ui.treeView_sourcefiles.doubleClicked.connect(self.open_includes_file) self.ui.toolButton_minus_source_files.clicked.connect(self.remove_source_files) self.ui.toolButton_plus_inputfiles.clicked.connect(self.add_inputfiles) self.ui.toolButton_minus_inputfiles.clicked.connect(self.remove_inputfiles) self.ui.toolButton_plus_inputfiles_opt.clicked.connect(self.add_inputfiles_opt) self.ui.toolButton_minus_inputfiles_opt.clicked.connect(self.remove_inputfiles_opt) self.ui.toolButton_plus_outputfiles.clicked.connect(self.add_outputfiles) self.ui.toolButton_minus_outputfiles.clicked.connect(self.remove_outputfiles) self.ui.pushButton_ok.clicked.connect(self.handle_ok_clicked) self.ui.pushButton_cancel.clicked.connect(self.close) # Enable removing items from QTreeViews by pressing the Delete key self.ui.treeView_sourcefiles.del_key_pressed.connect(self.remove_source_files_with_del) self.ui.treeView_inputfiles.del_key_pressed.connect(self.remove_inputfiles_with_del) self.ui.treeView_inputfiles_opt.del_key_pressed.connect(self.remove_inputfiles_opt_with_del) self.ui.treeView_outputfiles.del_key_pressed.connect(self.remove_outputfiles_with_del) def populate_sourcefile_list(self, items): """List source files in QTreeView. If items is None or empty list, model is cleared. """ self.sourcefiles_model.clear() self.sourcefiles_model.setHorizontalHeaderItem(0, QStandardItem("Additional source files")) # Add header if items is not None: for item in items: qitem = QStandardItem(item) qitem.setFlags(~Qt.ItemIsEditable) qitem.setData(QFileIconProvider().icon(QFileInfo(item)), Qt.DecorationRole) self.sourcefiles_model.appendRow(qitem) def populate_inputfiles_list(self, items): """List input files in QTreeView. If items is None or empty list, model is cleared. """ self.inputfiles_model.clear() self.inputfiles_model.setHorizontalHeaderItem(0, QStandardItem("Input files")) # Add header if items is not None: for item in items: qitem = QStandardItem(item) qitem.setData(QFileIconProvider().icon(QFileInfo(item)), Qt.DecorationRole) self.inputfiles_model.appendRow(qitem) def populate_inputfiles_opt_list(self, items): """List optional input files in QTreeView. If items is None or empty list, model is cleared. """ self.inputfiles_opt_model.clear() self.inputfiles_opt_model.setHorizontalHeaderItem(0, QStandardItem("Optional input files")) # Add header if items is not None: for item in items: qitem = QStandardItem(item) qitem.setData(QFileIconProvider().icon(QFileInfo(item)), Qt.DecorationRole) self.inputfiles_opt_model.appendRow(qitem) def populate_outputfiles_list(self, items): """List output files in QTreeView. If items is None or empty list, model is cleared. """ self.outputfiles_model.clear() self.outputfiles_model.setHorizontalHeaderItem(0, QStandardItem("Output files")) # Add header if items is not None: for item in items: qitem = QStandardItem(item) qitem.setData(QFileIconProvider().icon(QFileInfo(item)), Qt.DecorationRole) self.outputfiles_model.appendRow(qitem) @Slot(bool, name="browse_main_program") def browse_main_program(self, checked=False): """Open file browser where user can select the path of the main program file.""" # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QFileDialog.getOpenFileName(self, "Add existing main program file", APPLICATION_PATH, "*.*") file_path = answer[0] if not file_path: # Cancel button clicked return self.set_main_program_path(file_path) @Slot("QString", name="set_main_program_path") def set_main_program_path(self, file_path): """Set main program file and folder path.""" folder_path = os.path.split(file_path)[0] self.program_path = os.path.abspath(folder_path) # Update UI self.ui.lineEdit_main_program.setText(file_path) self.ui.label_mainpath.setText(self.program_path) @Slot() def new_main_program_file(self): """Creates a new blank main program file. Let's user decide the file name and path. Alternative version using only one getSaveFileName dialog. """ # noinspection PyCallByClass answer = QFileDialog.getSaveFileName(self, "Create new main program", APPLICATION_PATH) file_path = answer[0] if not file_path: # Cancel button clicked return # Remove file if it exists. getSaveFileName has asked confirmation for us. try: os.remove(file_path) except OSError: pass try: with open(file_path, "w"): pass except OSError: msg = "Please check directory permissions." # noinspection PyTypeChecker, PyArgumentList, PyCallByClass QMessageBox.information(self, "Creating file failed", msg) return main_dir = os.path.dirname(file_path) self.program_path = os.path.abspath(main_dir) # Update UI self.ui.lineEdit_main_program.setText(file_path) self.ui.label_mainpath.setText(self.program_path) @Slot(name="new_source_file") def new_source_file(self): """Let user create a new source file for this tool specification.""" path = self.program_path if self.program_path else APPLICATION_PATH # noinspection PyCallByClass, PyTypeChecker, PyArgumentList dir_path = QFileDialog.getSaveFileName(self, "Create source file", path, "*.*") file_path = dir_path[0] if file_path == '': # Cancel button clicked return # create file. NOTE: getSaveFileName does the 'check for existence' for us open(file_path, 'w').close() self.add_single_include(file_path) @Slot(bool, name="show_add_source_files_dialog") def show_add_source_files_dialog(self, checked=False): """Let user select source files for this tool specification.""" path = self.program_path if self.program_path else APPLICATION_PATH # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QFileDialog.getOpenFileNames(self, "Add source file", path, "*.*") file_paths = answer[0] if not file_paths: # Cancel button clicked return for path in file_paths: if not self.add_single_include(path): continue @Slot(bool, name="show_add_source_dirs_dialog") def show_add_source_dirs_dialog(self, checked=False): """Let user select a source directory for this tool specification. All files and sub-directories will be added to the source files. """ path = self.program_path if self.program_path else APPLICATION_PATH # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QFileDialog.getExistingDirectory(self, "Select a directory to add to source files", path) file_paths = list() for root, _, files in os.walk(answer): for file in files: file_paths.append(os.path.abspath(os.path.join(root, file))) for path in file_paths: if not self.add_single_include(path): continue @Slot("QVariant", name="add_dropped_includes") def add_dropped_includes(self, file_paths): """Adds dropped file paths to Source files list.""" for path in file_paths: if not self.add_single_include(path): continue def add_single_include(self, path): """Add file path to Source files list.""" dirname, file_pattern = os.path.split(path) # logging.debug("program path:{0}".format(self.program_path)) # logging.debug("{0}, {1}".format(dirname, file_pattern)) if not self.program_path: self.program_path = dirname self.ui.label_mainpath.setText(self.program_path) path_to_add = file_pattern else: # check if path is a descendant of main dir. common_prefix = os.path.commonprefix([os.path.abspath(self.program_path), os.path.abspath(path)]) # logging.debug("common_prefix:{0}".format(common_prefix)) if common_prefix != self.program_path: self.statusbar.showMessage( "Source file {0}'s location is invalid " "(should be in main directory)".format(file_pattern), 5000 ) return False path_to_add = os.path.relpath(path, self.program_path) if self.sourcefiles_model.findItems(path_to_add): self.statusbar.showMessage("Source file {0} already included".format(path_to_add), 5000) return False qitem = QStandardItem(path_to_add) qitem.setFlags(~Qt.ItemIsEditable) qitem.setData(QFileIconProvider().icon(QFileInfo(path_to_add)), Qt.DecorationRole) self.sourcefiles_model.appendRow(qitem) return True @busy_effect @Slot("QModelIndex", name="open_includes_file") def open_includes_file(self, index): """Open source file in default program.""" if not index: return if not index.isValid(): self._toolbox.msg_error.emit("Selected index not valid") return includes_file = self.sourcefiles_model.itemFromIndex(index).text() _, ext = os.path.splitext(includes_file) if ext in [".bat", ".exe"]: self._toolbox.msg_warning.emit( "Sorry, opening files with extension <b>{0}</b> not implemented. " "Please open the file manually.".format(ext) ) return url = "file:///" + os.path.join(self.program_path, includes_file) # noinspection PyCallByClass, PyTypeChecker, PyArgumentList res = QDesktopServices.openUrl(QUrl(url, QUrl.TolerantMode)) if not res: self._toolbox.msg_error.emit("Failed to open file: <b>{0}</b>".format(includes_file)) @Slot(name="remove_source_files_with_del") def remove_source_files_with_del(self): """Support for deleting items with the Delete key.""" self.remove_source_files() @Slot(bool, name="remove_source_files") def remove_source_files(self, checked=False): """Remove selected source files from include list. Do not remove anything if there are no items selected. """ indexes = self.ui.treeView_sourcefiles.selectedIndexes() if not indexes: # Nothing selected self.statusbar.showMessage("Please select the source files to remove", 3000) else: rows = [ind.row() for ind in indexes] rows.sort(reverse=True) for row in rows: self.sourcefiles_model.removeRow(row) if self.sourcefiles_model.rowCount() == 0: if self.ui.lineEdit_main_program.text().strip() == "": self.program_path = None self.ui.label_mainpath.clear() self.statusbar.showMessage("Selected source files removed", 3000) @Slot(bool, name="add_inputfiles") def add_inputfiles(self, checked=False): """Let user select input files for this tool specification.""" msg = ( "Add an input file or a directory required by your program. Wildcards " "<b>are not</b> supported.<br/><br/>" "Examples:<br/>" "<b>data.csv</b> -> File is copied to the same work directory as the main program.<br/>" "<b>input/data.csv</b> -> Creates subdirectory /input to work directory and " "copies file data.csv there.<br/>" "<b>output/</b> -> Creates an empty directory into the work directory.<br/><br/>" ) # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QInputDialog.getText(self, "Add input item", msg, flags=Qt.WindowTitleHint | Qt.WindowCloseButtonHint) file_name = answer[0] if not file_name: # Cancel button clicked return qitem = QStandardItem(file_name) qitem.setData(QFileIconProvider().icon(QFileInfo(file_name)), Qt.DecorationRole) self.inputfiles_model.appendRow(qitem) @Slot(name="remove_inputfiles_with_del") def remove_inputfiles_with_del(self): """Support for deleting items with the Delete key.""" self.remove_inputfiles() @Slot(bool, name="remove_inputfiles") def remove_inputfiles(self, checked=False): """Remove selected input files from list. Do not remove anything if there are no items selected. """ indexes = self.ui.treeView_inputfiles.selectedIndexes() if not indexes: # Nothing selected self.statusbar.showMessage("Please select the input files to remove", 3000) else: rows = [ind.row() for ind in indexes] rows.sort(reverse=True) for row in rows: self.inputfiles_model.removeRow(row) self.statusbar.showMessage("Selected input files removed", 3000) @Slot(bool, name="add_inputfiles_opt") def add_inputfiles_opt(self, checked=False): """Let user select optional input files for this tool specification.""" msg = ( "Add optional input files that may be utilized by your program. <br/>" "Wildcards are supported.<br/><br/>" "Examples:<br/>" "<b>data.csv</b> -> If found, file is copied to the same work directory as the main program.<br/>" "<b>*.csv</b> -> All found CSV files are copied to the same work directory as the main program.<br/>" "<b>input/data_?.dat</b> -> All found files matching the pattern 'data_?.dat' will be copied to <br/>" "input/ subdirectory under the same work directory as the main program.<br/><br/>" ) # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QInputDialog.getText( self, "Add optional input item", msg, flags=Qt.WindowTitleHint | Qt.WindowCloseButtonHint ) file_name = answer[0] if not file_name: # Cancel button clicked return qitem = QStandardItem(file_name) qitem.setData(QFileIconProvider().icon(QFileInfo(file_name)), Qt.DecorationRole) self.inputfiles_opt_model.appendRow(qitem) @Slot(name="remove_inputfiles_opt_with_del") def remove_inputfiles_opt_with_del(self): """Support for deleting items with the Delete key.""" self.remove_inputfiles_opt() @Slot(bool, name="remove_inputfiles_opt") def remove_inputfiles_opt(self, checked=False): """Remove selected optional input files from list. Do not remove anything if there are no items selected. """ indexes = self.ui.treeView_inputfiles_opt.selectedIndexes() if not indexes: # Nothing selected self.statusbar.showMessage("Please select the optional input files to remove", 3000) else: rows = [ind.row() for ind in indexes] rows.sort(reverse=True) for row in rows: self.inputfiles_opt_model.removeRow(row) self.statusbar.showMessage("Selected optional input files removed", 3000) @Slot(bool, name="add_outputfiles") def add_outputfiles(self, checked=False): """Let user select output files for this tool specification.""" msg = ( "Add output files that will be archived into the Tool results directory after the <br/>" "Tool specification has finished execution. Wildcards are supported.<br/><br/>" "Examples:<br/>" "<b>results.csv</b> -> File is copied from work directory into results.<br/> " "<b>*.csv</b> -> All CSV files will copied into results.<br/> " "<b>output/*.gdx</b> -> All GDX files from the work subdirectory /output will be copied into <br/>" "results /output subdirectory.<br/><br/>" ) # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QInputDialog.getText(self, "Add output item", msg, flags=Qt.WindowTitleHint | Qt.WindowCloseButtonHint) file_name = answer[0] if not file_name: # Cancel button clicked return qitem = QStandardItem(file_name) qitem.setData(QFileIconProvider().icon(QFileInfo(file_name)), Qt.DecorationRole) self.outputfiles_model.appendRow(qitem) @Slot(name="remove_outputfiles_with_del") def remove_outputfiles_with_del(self): """Support for deleting items with the Delete key.""" self.remove_outputfiles() @Slot(bool, name="remove_outputfiles") def remove_outputfiles(self, checked=False): """Remove selected output files from list. Do not remove anything if there are no items selected. """ indexes = self.ui.treeView_outputfiles.selectedIndexes() if not indexes: # Nothing selected self.statusbar.showMessage("Please select the output files to remove", 3000) else: rows = [ind.row() for ind in indexes] rows.sort(reverse=True) for row in rows: self.outputfiles_model.removeRow(row) self.statusbar.showMessage("Selected output files removed", 3000) @Slot() def handle_ok_clicked(self): """Checks that everything is valid, creates Tool spec definition dictionary and adds Tool spec to project.""" # Check that tool type is selected if self.ui.comboBox_tooltype.currentIndex() == 0: self.statusbar.showMessage("Tool type not selected", 3000) return self.definition["name"] = self.ui.lineEdit_name.text() self.definition["description"] = self.ui.textEdit_description.toPlainText() self.definition["tooltype"] = self.ui.comboBox_tooltype.currentText().lower() flags = Qt.MatchContains # Check that path of main program file is valid before saving it main_program = self.ui.lineEdit_main_program.text().strip() if not os.path.isfile(main_program): self.statusbar.showMessage("Main program file is not valid", 6000) return # Fix for issue #241 folder_path, file_path = os.path.split(main_program) self.program_path = os.path.abspath(folder_path) self.ui.label_mainpath.setText(self.program_path) self.definition["execute_in_work"] = self.ui.checkBox_execute_in_work.isChecked() self.definition["includes"] = [file_path] self.definition["includes"] += [i.text() for i in self.sourcefiles_model.findItems("", flags)] self.definition["inputfiles"] = [i.text() for i in self.inputfiles_model.findItems("", flags)] self.definition["inputfiles_opt"] = [i.text() for i in self.inputfiles_opt_model.findItems("", flags)] self.definition["outputfiles"] = [i.text() for i in self.outputfiles_model.findItems("", flags)] # Strip whitespace from args before saving it to JSON self.definition["cmdline_args"] = ToolSpecification.split_cmdline_args(self.ui.lineEdit_args.text()) for k in REQUIRED_KEYS: if not self.definition[k]: self.statusbar.showMessage("{} missing".format(k), 3000) return # Create new Tool specification short_name = self.definition["name"].lower().replace(" ", "_") self.def_file_path = os.path.join(self.program_path, short_name + ".json") if self.call_add_tool_specification(): self.close() def call_add_tool_specification(self): """Adds or updates Tool specification according to user's selections. If the name is the same as an existing tool specification, it is updated and auto-saved to the definition file. (User is editing an existing tool specification.) If the name is not in the tool specification model, creates a new tool specification and offer to save the definition file. (User is creating a new tool specification from scratch or spawning from an existing one). """ # Load tool specification path = self.program_path tool = self._project.load_tool_specification_from_dict(self.definition, path) if not tool: self.statusbar.showMessage("Adding Tool specification failed", 3000) return False # Check if a tool specification with this name already exists row = self._toolbox.tool_specification_model.tool_specification_row(tool.name) if row >= 0: # NOTE: Row 0 at this moment has 'No tool', but in the future it may change. Better be ready. old_tool = self._toolbox.tool_specification_model.tool_specification(row) def_file = old_tool.get_def_path() tool.set_def_path(def_file) if tool.__dict__ == old_tool.__dict__: # Nothing changed. We're done here. return True # logging.debug("Updating definition for tool specification '{}'".format(tool.name)) self._toolbox.update_tool_specification(row, tool) else: # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QFileDialog.getSaveFileName( self, "Save Tool specification file", self.def_file_path, "JSON (*.json)" ) if answer[0] == "": # Cancel button clicked return False def_file = os.path.abspath(answer[0]) tool.set_def_path(def_file) self._toolbox.add_tool_specification(tool) # Save path of main program file relative to definition file in case they differ def_path = os.path.dirname(def_file) if def_path != self.program_path: self.definition["includes_main_path"] = os.path.relpath(self.program_path, def_path) # Save file descriptor with open(def_file, "w") as fp: try: json.dump(self.definition, fp, indent=4) except ValueError: self.statusbar.showMessage("Error saving file", 3000) self._toolbox.msg_error.emit("Saving Tool specification file failed. Path:{0}".format(def_file)) return False return True def keyPressEvent(self, e): """Close Setup form when escape key is pressed. Args: e (QKeyEvent): Received key press event. """ if e.key() == Qt.Key_Escape: self.close() def closeEvent(self, event=None): """Handle close window. Args: event (QEvent): Closing event if 'X' is clicked. """ if event: event.accept() def _make_add_cmdline_tag_menu(self): """Constructs a popup menu for the '@@' button.""" menu = QMenu(self.ui.toolButton_add_cmdline_tag) action = menu.addAction(str(CmdlineTag.URL_INPUTS)) action.triggered.connect(self._add_cmdline_tag_url_inputs) action.setToolTip("Insert a tag that is replaced by all input database URLs.") action = menu.addAction(str(CmdlineTag.URL_OUTPUTS)) action.triggered.connect(self._add_cmdline_tag_url_outputs) action.setToolTip("Insert a tag that is replaced be all output database URLs.") action = menu.addAction(str(CmdlineTag.URL)) action.triggered.connect(self._add_cmdline_tag_data_store_url) action.setToolTip("Insert a tag that is replaced by the URL provided by Data Store '<data-store-name>'.") action = menu.addAction(str(CmdlineTag.OPTIONAL_INPUTS)) action.triggered.connect(self._add_cmdline_tag_optional_inputs) action.setToolTip("Insert a tag that is replaced by a list of optional input files.") return menu def _insert_spaces_around_tag_in_args_edit(self, tag_length, restore_cursor_to_tag_end=False): """ Inserts spaces before/after @@ around cursor position/selection Expects cursor to be at the end of the tag. """ args_edit = self.ui.lineEdit_args text = args_edit.text() cursor_position = args_edit.cursorPosition() if cursor_position == len(text) or (cursor_position < len(text) - 1 and not text[cursor_position].isspace()): args_edit.insert(" ") appended_spaces = 1 text = args_edit.text() else: appended_spaces = 0 tag_start = cursor_position - tag_length if tag_start > 1 and text[tag_start - 2 : tag_start] == CMDLINE_TAG_EDGE: args_edit.setCursorPosition(tag_start) args_edit.insert(" ") prepended_spaces = 1 else: prepended_spaces = 0 if restore_cursor_to_tag_end: args_edit.setCursorPosition(cursor_position + prepended_spaces) else: args_edit.setCursorPosition(cursor_position + appended_spaces + prepended_spaces) @Slot("QAction") def _add_cmdline_tag_url_inputs(self, _): """Inserts @@url_inputs@@ tag to command line arguments.""" tag = CmdlineTag.URL_INPUTS self.ui.lineEdit_args.insert(tag) self._insert_spaces_around_tag_in_args_edit(len(tag)) @Slot("QAction") def _add_cmdline_tag_url_outputs(self, _): """Inserts @@url_outputs@@ tag to command line arguments.""" tag = CmdlineTag.URL_OUTPUTS self.ui.lineEdit_args.insert(tag) self._insert_spaces_around_tag_in_args_edit(len(tag)) @Slot("QAction") def _add_cmdline_tag_data_store_url(self, _): """Inserts @@url:<data-store-name>@@ tag to command line arguments and selects '<data-store-name>'.""" args_edit = self.ui.lineEdit_args tag = CmdlineTag.URL args_edit.insert(tag) self._insert_spaces_around_tag_in_args_edit(len(tag), restore_cursor_to_tag_end=True) cursor_position = args_edit.cursorPosition() args_edit.setSelection(cursor_position - len(CMDLINE_TAG_EDGE + "<data-store_name>"), len("<data-store_name>")) @Slot("QAction") def _add_cmdline_tag_optional_inputs(self, _): """Inserts @@optional_inputs@@ tag to command line arguments.""" tag = CmdlineTag.OPTIONAL_INPUTS self.ui.lineEdit_args.insert(tag) self._insert_spaces_around_tag_in_args_edit(len(tag))
class CheckListEditor(QTableView): """A check list editor.""" def __init__(self, parent, tutor=None): """Initialize class.""" super().__init__(parent) self._tutor = tutor self._base_size = None self.model = QStandardItemModel(self) self.setModel(self.model) self.verticalHeader().hide() self.horizontalHeader().hide() self.setShowGrid(False) self.setMouseTracking(True) def keyPressEvent(self, event): """Toggles checked state if the user presses space.""" super().keyPressEvent(event) if event.key() == Qt.Key_Space: index = self.currentIndex() self.toggle_checked_state(index) def toggle_checked_state(self, index): """Toggles checked state of given index. Args: index (QModelIndex) """ item = self.model.itemFromIndex(index) if item.checkState() == Qt.Checked: item.setCheckState(Qt.Unchecked) else: item.setCheckState(Qt.Checked) def mouseMoveEvent(self, event): """Sets the current index to the one under mouse.""" index = self.indexAt(event.pos()) self.setCurrentIndex(index) def mousePressEvent(self, event): """Toggles checked state of pressed index.""" index = self.indexAt(event.pos()) self.toggle_checked_state(index) def set_data(self, items, checked_items): """Sets data and updates geometry. Args: items (Sequence(str)): All items. checked_items (Sequence(str)): Initially checked items. """ for item in items: qitem = QStandardItem(item) if item in checked_items: qitem.setCheckState(Qt.Checked) else: qitem.setCheckState(Qt.Unchecked) qitem.setFlags(~Qt.ItemIsEditable & ~Qt.ItemIsUserCheckable) qitem.setData(qApp.palette().window(), Qt.BackgroundRole) # pylint: disable=undefined-variable self.model.appendRow(qitem) self.selectionModel().select(self.model.index(0, 0), QItemSelectionModel.Select) def data(self): """Returns a comma separated list of checked items. Returns str """ data = [] for q in self.model.findItems('*', Qt.MatchWildcard): if q.checkState() == Qt.Checked: data.append(q.text()) return ",".join(data) def set_base_size(self, size): self._base_size = size def update_geometry(self): """Updates geometry. """ self.horizontalHeader().setDefaultSectionSize(self._base_size.width()) self.verticalHeader().setDefaultSectionSize(self._base_size.height()) total_height = self.verticalHeader().length() + 2 size = QSize(self._base_size.width(), total_height).boundedTo(self.parent().size()) self.resize(size) if self._tutor: self.move(self.pos() + self._tutor.mapTo(self.parent(), self._tutor.parent().pos())) # Adjust position if widget is outside parent's limits bottom_right = self.mapToGlobal(self.rect().bottomRight()) parent_bottom_right = self.parent().mapToGlobal( self.parent().rect().bottomRight()) x_offset = max(0, bottom_right.x() - parent_bottom_right.x()) y_offset = max(0, bottom_right.y() - parent_bottom_right.y()) self.move(self.pos() - QPoint(x_offset, y_offset))
class CollectionsHistoryTabs(QTabWidget, Ui_CollectionsHistoryTabs): set_item = Signal(Req) folder_icon = QIcon("folder.svg") def __init__(self): super(CollectionsHistoryTabs, self).__init__() self.setupUi(self) self.historyList.setContextMenuPolicy(Qt.CustomContextMenu) self.historyList.customContextMenuRequested.connect(self.historyMenu) self.historyModel = QStandardItemModel() self.historyList.setModel(self.historyModel) self.historyList.header().hide() self.historyList.expandToDepth(0) self.historyList.doubleClicked.connect(self.emitItem) self.collectionsModel = QStandardItemModel() default = QStandardItem("Default") default.setEditable(False) default.setIcon(self.folder_icon) self.collectionsModel.appendRow(default) self.collectionsTree.setModel(self.collectionsModel) self.collectionsTree.header().hide() self.collectionsTree.expandToDepth(0) self.collectionsTree.doubleClicked.connect(self.emitItem) self.collectionsTree.setContextMenuPolicy(Qt.CustomContextMenu) self.collectionsTree.customContextMenuRequested.connect( self.collectionsMenu) def emitItem(self, item): req = item.data(Qt.UserRole) self.set_item.emit(req) def getCollections(self): collections = [] for row in range(0, self.collectionsModel.rowCount()): collections.append(self.collectionsModel.item(row).text()) return collections def collectionsMenu(self, position): menu = QMenu() deleteAction = menu.addAction("Delete") action = menu.exec_(self.historyList.mapToGlobal(position)) if action == deleteAction: index = self.collectionsTree.indexAt(position) item = self.collectionsModel.itemFromIndex(index) if item: parent = item.parent() parent.removeRow(index.row()) def historyMenu(self, position): menu = QMenu() saveAction = menu.addAction("Save") deleteAction = menu.addAction("Delete") action = menu.exec_(self.historyList.mapToGlobal(position)) if action == saveAction: index = self.historyList.indexAt(position) item = self.historyModel.itemFromIndex(index) self._saveRequest(item.data(Qt.UserRole)) elif action == deleteAction: index = self.historyList.indexAt(position) item = self.historyModel.itemFromIndex(index) if item: parent = item.parent() parent.removeRow(index.row()) def _saveRequest(self, item): maybeSelected = self.collectionsTree.selectedIndexes() if maybeSelected: collection = self.collectionsModel.item( maybeSelected[0].row()).text() else: self.saveDialog = SaveToCollectionDialog(self.getCollections()) self.saveDialog.exec_() collection = self.saveDialog.collections.currentText() if collection: self.addCollectionItem(collection, item) def insertToHistory(self, response, reqObject): parents = self.historyModel.findItems(str(reqObject.date)) if not parents: parent = QStandardItem(str(reqObject.date)) parent.setEditable(False) self.historyModel.appendRow(parent) else: parent = parents.pop() historyItem = QStandardItem() historyItem.setText(reqObject.buildTextRepresentation()) historyItem.setEditable(False) historyItem.setData(reqObject, Qt.UserRole) parent.insertRow( 0, [historyItem ]) # seems like a hack inserting list here, possilby report def addCollectionItem(self, collection, item): items = self.collectionsModel.findItems(collection) if not items: parent = QStandardItem(collection) parent.setEditable(False) parent.setIcon(self.folder_icon) self.collectionsModel.appendRow(parent) else: parent = items.pop(0) newItem = QStandardItem() newItem.setText(item.method + " " + item.url) newItem.setEditable(False) newItem.setData(item, Qt.UserRole) parent.appendRow(newItem)