class AddDataConnectionWidget(QWidget): """A widget that queries user's preferences for a new item. Attributes: toolbox (ToolboxUI): toolbox widget x (int): X coordinate of new item y (int): Y coordinate of new item """ def __init__(self, toolbox, x, y): """Initialize class.""" super().__init__( parent=toolbox, f=Qt.Window) # Setting the parent inherits the stylesheet self._toolbox = toolbox self._x = x self._y = y self._project = self._toolbox.project() # Set up the user interface from Designer. self.ui = ui.add_data_connection.Ui_Form() self.ui.setupUi(self) # 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) # Class attributes self.name = '' self.description = '' self.connect_signals() self.ui.lineEdit_name.setFocus() # Ensure this window gets garbage-collected when closed self.setAttribute(Qt.WA_DeleteOnClose) def connect_signals(self): """Connect signals to slots.""" self.ui.lineEdit_name.textChanged.connect( self.name_changed) # Name -> folder name connection self.ui.pushButton_ok.clicked.connect(self.ok_clicked) self.ui.pushButton_cancel.clicked.connect(self.close) @Slot(name='name_changed') def name_changed(self): """Update label to show upcoming folder name.""" name = self.ui.lineEdit_name.text() default = "Folder:" if name == '': self.ui.label_folder.setText(default) else: folder_name = name.lower().replace(' ', '_') msg = default + " " + folder_name self.ui.label_folder.setText(msg) @Slot(name='ok_clicked') def ok_clicked(self): """Check that given item name is valid and add it to project.""" self.name = self.ui.lineEdit_name.text() self.description = self.ui.lineEdit_description.text() if not self.name: # No name given self.statusbar.showMessage("Name missing", 3000) return # Check for invalid characters for a folder name if any((True for x in self.name if x in INVALID_CHARS)): self.statusbar.showMessage("Name not valid for a folder name", 3000) return # Check that name is not reserved if self._toolbox.project_item_model.find_item(self.name): msg = "Item '{0}' already exists".format(self.name) self.statusbar.showMessage(msg, 3000) return # Check that short name (folder) is not reserved short_name = self.name.lower().replace(' ', '_') if self._toolbox.project_item_model.short_name_reserved(short_name): msg = "Item using folder '{0}' already exists".format(short_name) self.statusbar.showMessage(msg, 3000) return # Create new Item self.call_add_item() self.close() def call_add_item(self): """Creates new Item according to user's selections.""" self._project.add_data_connection(self.name, self.description, list(), self._x, self._y, set_selected=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() elif e.key() == Qt.Key_Enter or e.key() == Qt.Key_Return: self.ok_clicked() def closeEvent(self, event=None): """Handle close window. Args: event (QEvent): Closing event if 'X' is clicked. """ if event: event.accept() scene = self._toolbox.ui.graphicsView.scene() item_shadow = scene.item_shadow if item_shadow: scene.removeItem(item_shadow) scene.item_shadow = None
class NewProjectForm(QWidget): """Class for a new project widget. Attributes: toolbox (ToolboxUI): Parent widget. configs (ConfigurationParser): Configurations object """ def __init__(self, toolbox, configs): """Initialize class.""" super().__init__(parent=toolbox, f=Qt.Window) # Inherits stylesheet from parent self._toolbox = toolbox self._configs = configs # Set up the user interface from Designer. self.ui = ui.project_form.Ui_Form() self.ui.setupUi(self) # 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) # Class attributes self.name = '' # Project name self.description = '' # Project description self.connect_signals() self.ui.pushButton_ok.setDefault(True) self.ui.lineEdit_project_name.setFocus() # Ensure this window gets garbage-collected when closed self.setAttribute(Qt.WA_DeleteOnClose) def connect_signals(self): """Connect signals to slots.""" self.ui.lineEdit_project_name.textChanged.connect(self.name_changed) self.ui.pushButton_ok.clicked.connect(self.ok_clicked) self.ui.pushButton_cancel.clicked.connect(self.close) @Slot(name='name_changed') def name_changed(self): """Update label to show a preview of the project directory name.""" project_name = self.ui.lineEdit_project_name.text() default = "Project folder:" if project_name == '': self.ui.label_folder.setText(default) else: folder_name = project_name.lower().replace(' ', '_') msg = default + " " + folder_name self.ui.label_folder.setText(msg) @Slot(name='ok_clicked') def ok_clicked(self): """Check that project name is valid and create project.""" self.name = self.ui.lineEdit_project_name.text() self.description = self.ui.textEdit_description.toPlainText() if self.name == '': self.statusbar.showMessage("No project name given", 5000) return # Check for invalid characters for a folder name invalid_chars = ["<", ">", ":", "\"", "/", "\\", "|", "?", "*", "."] # "." is actually valid in a folder name but # this is to prevent creating folders like "...." if any((True for x in self.name if x in invalid_chars)): self.statusbar.showMessage( "Project name contains invalid character(s) for a folder name", 5000) return # Check if project with same name already exists short_name = self.name.lower().replace(' ', '_') project_folder = os.path.join(project_dir(self._configs), short_name) if os.path.isdir(project_folder): self.statusbar.showMessage("Project already exists", 5000) return # Create new project self.call_create_project() self.close() def call_create_project(self): """Call ToolboxUI method create_project().""" self._toolbox.create_project(self.name, self.description) def keyPressEvent(self, e): """Close project form when escape key is pressed. Args: e (QKeyEvent): Received key press event. """ if e.key() == Qt.Key_Escape: self.close() elif e.key() == Qt.Key_Enter or e.key() == Qt.Key_Return: self.ok_clicked() def closeEvent(self, event=None): """Handle close window. Args: event (QEvent): Closing event if 'X' is clicked. """ if event: event.accept()
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 AddProjectItemWidget(QWidget): """A widget to query user's preferences for a new item. Attributes: toolbox (ToolboxUI): Parent widget x (int): X coordinate of new item y (int): Y coordinate of new item """ def __init__(self, toolbox, x, y, class_, spec=""): """Initialize class.""" from ..ui.add_project_item import Ui_Form # pylint: disable=import-outside-toplevel super().__init__(parent=toolbox, f=Qt.Window) # Setting parent inherits stylesheet self._toolbox = toolbox self._x = x self._y = y self._project = self._toolbox.project() # Set up the user interface from Designer. self.ui = Ui_Form() self.ui.setupUi(self) # 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 if toolbox.supports_specifications(class_.item_type()): self.ui.comboBox_specification.setModel( toolbox.filtered_spec_factory_models[class_.item_type()]) if spec: self.ui.comboBox_specification.setCurrentText(spec) prefix = spec else: prefix = class_.item_type() self.ui.comboBox_specification.setCurrentIndex(-1) else: prefix = class_.item_type() self.ui.comboBox_specification.setEnabled(False) self.name = toolbox.propose_item_name(prefix) self.ui.lineEdit_name.setText(self.name) self.ui.lineEdit_name.selectAll() self.description = "" self.connect_signals() self.ui.lineEdit_name.setFocus() # Ensure this window gets garbage-collected when closed self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowTitle(f"Add {class_.item_type()}") def connect_signals(self): """Connect signals to slots.""" self.ui.lineEdit_name.textChanged.connect( self.handle_name_changed) # Name -> folder name connection self.ui.pushButton_ok.clicked.connect(self.handle_ok_clicked) self.ui.pushButton_cancel.clicked.connect(self.close) @Slot() def handle_name_changed(self): """Update label to show upcoming folder name.""" name = self.ui.lineEdit_name.text() default = "Folder:" if name == "": self.ui.label_folder.setText(default) else: folder_name = name.lower().replace(" ", "_") msg = default + " " + folder_name self.ui.label_folder.setText(msg) @Slot() def handle_ok_clicked(self): """Check that given item name is valid and add it to project.""" self.name = self.ui.lineEdit_name.text() self.description = self.ui.lineEdit_description.text() if not self.name: # No name given self.statusbar.showMessage("Name missing", 3000) return # Check for invalid characters for a folder name if any((True for x in self.name if x in INVALID_CHARS)): self.statusbar.showMessage("Name not valid for a folder name", 3000) return # Check that name is not reserved if self._toolbox.project_item_model.find_item(self.name): msg = "Item '{0}' already exists".format(self.name) self.statusbar.showMessage(msg, 3000) return # Check that short name (folder) is not reserved short_name = self.name.lower().replace(" ", "_") if self._toolbox.project_item_model.short_name_reserved(short_name): msg = "Item using folder '{0}' already exists".format(short_name) self.statusbar.showMessage(msg, 3000) return # Create new Item self.call_add_item() self.close() def call_add_item(self): """Creates new Item according to user's selections. Must be reimplemented by subclasses. """ raise NotImplementedError() 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() elif e.key() == Qt.Key_Enter or e.key() == Qt.Key_Return: self.handle_ok_clicked() def closeEvent(self, event=None): """Handle close window. Args: event (QEvent): Closing event if 'X' is clicked. """ if event: event.accept() scene = self._toolbox.ui.graphicsView.scene() item_shadow = scene.item_shadow if item_shadow: scene.removeItem(item_shadow) scene.item_shadow = None
class TViewerWindow(QMainWindow): def __init__(self, parent=None): super(TViewerWindow, self).__init__(parent) self.__set_title("") self.resize(600, 600) self._filename = "" # file info self._info = QLabel(self) # status bar self.status = QStatusBar(self) self.status.setSizeGripEnabled(True) self.status.insertPermanentWidget(0, self._info) self.setStatusBar(self.status) # central widget window = QWidget(self) self.setCentralWidget(window) # main layout layout = QVBoxLayout(window) layout.setContentsMargins(0, 0, 0, 0) window.setLayout(layout) layout_2 = QHBoxLayout(self) layout_2.addStretch() layout_2.setSpacing(0) layout_2.addWidget(QLabel(" Channels: ")) button = TRightClickButton("R") button.setStatusTip("Show Red (Right click to toggle solo)") button.setCheckable(True) button.setChecked(True) button.clicked.connect(self.__slot_channels) button.rightClicked.connect(self.__slot_channels_right) self._btn_r = button layout_2.addWidget(button) button = TRightClickButton("G") button.setStatusTip("Show Green (Right click to toggle solo)") button.setCheckable(True) button.setChecked(True) button.clicked.connect(self.__slot_channels) button.rightClicked.connect(self.__slot_channels_right) self._btn_g = button layout_2.addWidget(button) button = TRightClickButton("B") button.setStatusTip("Show Blue (Right click to toggle solo)") button.setCheckable(True) button.setChecked(True) button.clicked.connect(self.__slot_channels) button.rightClicked.connect(self.__slot_channels_right) self._btn_b = button layout_2.addWidget(button) button = TRightClickButton("A") button.setStatusTip("Show Alpha (Right click to toggle solo)") button.setCheckable(True) button.clicked.connect(self.__slot_channels) button.rightClicked.connect(self.__slot_channels_right) self._btn_a = button layout_2.addWidget(button) layout_2.addSpacing(32) layout_2.addWidget(QLabel(" Background: ")) button = QPushButton("Checkerboard") button.setStatusTip("Show checkerboard background") button.clicked.connect(self.__slot_checkerboard) layout_2.addWidget(button) button = QPushButton("Pick Color") button.setStatusTip("Pick solid background color") button.clicked.connect(self.__slot_solid_color) layout_2.addWidget(button) layout_2.addStretch() layout.addLayout(layout_2) # image viewer view = TGLViewport(self) self._viewport = view # view.doubleClicked.connect(self.__slot_action_open) view.setContextMenuPolicy(Qt.DefaultContextMenu) view.setStatusTip("Use context menu or double click to open") layout.addWidget(view) layout.setStretch(1, 1) layout.setSpacing(0) # @override def contextMenuEvent(self, event): if self.childAt(event.pos()) == self._viewport: self._menu.popup_for_file(self._filename, event.globalPos()) def view(self, filename): self._filename = filename self.__set_title(filename) self._viewport.set_texture(filename) self._info.setText(self._viewport.info) def __set_title(self, title): if title != "": title = " - " + title self.setWindowTitle("Texture Viewer" + title) def __slot_action_open(self): run(["explorer", self._filename]) def __slot_channels(self): self._viewport.set_channels(self._btn_r.isChecked(), self._btn_g.isChecked(), self._btn_b.isChecked(), self._btn_a.isChecked()) def __slot_channels_right(self, event): self._btn_r.setChecked(self.sender() == self._btn_r) self._btn_g.setChecked(self.sender() == self._btn_g) self._btn_b.setChecked(self.sender() == self._btn_b) self._btn_a.setChecked(self.sender() == self._btn_a) self.__slot_channels() def __slot_checkerboard(self): self._viewport.set_colors(True, None, None) def __slot_solid_color(self): color = QColorDialog.getColor(Qt.black, self, "Choose background color") if color.isValid(): self._viewport.set_colors(False, color, color)
class AddRelationshipsDialog(AddItemsDialog): """A dialog to query user's preferences for new relationships. Attributes: parent (TreeViewForm): data store widget relationship_class_id (int): default relationship class id object_id (int): default object id object_class_id (int): default object class id """ def __init__(self, parent, relationship_class_id=None, object_id=None, object_class_id=None, force_default=False): super().__init__(parent, force_default=force_default) self.remove_row_icon = QIcon(":/icons/minus_relationship_icon.png") self.relationship_class_list = \ [x for x in self._parent.db_map.wide_relationship_class_list(object_class_id=object_class_id)] self.relationship_class = None self.relationship_class_id = relationship_class_id self.object_id = object_id self.object_class_id = object_class_id self.default_object_column = None self.default_object_name = None self.set_default_object_name() self.setup_ui(ui.add_relationships.Ui_Dialog()) self.ui.tableView.setItemDelegate(AddRelationshipsDelegate(parent)) self.init_relationship_class(force_default) # 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) self.connect_signals() self.reset_model() def init_relationship_class(self, force_default): """Populate combobox and initialize relationship class if any.""" relationship_class_name_list = [ x.name for x in self.relationship_class_list ] if not force_default: self.ui.comboBox_relationship_class.addItems( relationship_class_name_list) self.ui.comboBox_relationship_class.setCurrentIndex(-1) self.relationship_class = self._parent.db_map.\ single_wide_relationship_class(id=self.relationship_class_id).one_or_none() if not self.relationship_class: # Default not found return try: if not force_default: combo_index = relationship_class_name_list.index( self.relationship_class.name) self.ui.comboBox_relationship_class.setCurrentIndex( combo_index) return self.ui.comboBox_relationship_class.addItem( self.relationship_class.name) except ValueError: pass def connect_signals(self): """Connect signals to slots.""" self.ui.comboBox_relationship_class.currentIndexChanged.connect( self.call_reset_model) super().connect_signals() @Slot("int", name='call_reset_model') def call_reset_model(self, index): """Called when relationship class's combobox's index changes. Update relationship_class attribute accordingly and reset model.""" self.relationship_class = self.relationship_class_list[index] self.reset_model() def reset_model(self): """Setup model according to current relationship class selected in combobox (or given as input). """ if not self.relationship_class: return object_class_name_list = self.relationship_class.object_class_name_list.split( ',') header = [ *[x + " name" for x in object_class_name_list], 'relationship name' ] self.model.set_horizontal_header_labels(header) self.reset_default_object_column() if self.default_object_name and self.default_object_column is not None: defaults = { header[self.default_object_column]: self.default_object_name } self.model.set_default_row(**defaults) self.model.clear() self.ui.tableView.resizeColumnsToContents() def set_default_object_name(self): if not self.object_id: return object_ = self._parent.db_map.single_object( id=self.object_id).one_or_none() if not object_: return self.default_object_name = object_.name def reset_default_object_column(self): if not self.default_object_name: return if not self.relationship_class or not self.object_class_id: return try: object_class_id_list = self.relationship_class.object_class_id_list self.default_object_column = [ int(x) for x in object_class_id_list.split(',') ].index(self.object_class_id) except ValueError: pass @busy_effect def accept(self): """Collect info from dialog and try to add items.""" wide_kwargs_list = list() name_column = self.model.horizontal_header_labels().index( "relationship name") for i in range(self.model.rowCount() - 1): # last row will always be empty row_data = self.model.row_data(i)[:-1] relationship_name = row_data[name_column] if not relationship_name: self._parent.msg_error.emit( "Relationship name missing at row {}".format(i + 1)) return object_id_list = list() for column in range(name_column): # Leave 'name' column outside object_name = row_data[column] if not object_name: self._parent.msg_error.emit( "Object name missing at row {}".format(i + 1)) return object_ = self._parent.db_map.single_object( name=object_name).one_or_none() if not object_: self._parent.msg_error.emit( "Couldn't find object '{}' at row {}".format( object_name, i + 1)) return object_id_list.append(object_.id) if len(object_id_list) < 2: self._parent.msg_error.emit( "Not enough dimensions at row {} (at least two are needed)" .format(i + 1)) return wide_kwargs = { 'name': relationship_name, 'object_id_list': object_id_list, 'class_id': self.relationship_class.id } wide_kwargs_list.append(wide_kwargs) if not wide_kwargs_list: self._parent.msg_error.emit("Nothing to add") return try: wide_relationships = self._parent.db_map.add_wide_relationships( *wide_kwargs_list) self._parent.add_relationships(wide_relationships) super().accept() except SpineIntegrityError as e: self._parent.msg_error.emit(e.msg) except SpineDBAPIError as e: self._parent.msg_error.emit(e.msg)
class AddToolWidget(QWidget): """A widget that queries user's preferences for a new item. Attributes: toolbox (ToolboxUI): Parent widget x (int): X coordinate of new item y (int): Y coordinate of new item """ def __init__(self, toolbox, x, y): """Initialize class.""" from ..ui.add_tool import Ui_Form super().__init__(parent=toolbox, f=Qt.Window) # Setting parent inherits stylesheet self._toolbox = toolbox self._x = x self._y = y self._project = self._toolbox.project() # Set up the user interface from Designer. self.ui = Ui_Form() self.ui.setupUi(self) # 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) # Class attributes self.name = toolbox.propose_item_name(Tool.default_name_prefix()) self.ui.lineEdit_name.setText(self.name) self.ui.lineEdit_name.selectAll() self.description = '' # Init self.ui.comboBox_tool.setModel(self._toolbox.tool_specification_model) self.ui.lineEdit_name.setFocus() self.connect_signals() # Ensure this window gets garbage-collected when closed self.setAttribute(Qt.WA_DeleteOnClose) def connect_signals(self): """Connect signals to slots.""" self.ui.lineEdit_name.textChanged.connect( self.name_changed) # Name -> folder name connection self.ui.pushButton_ok.clicked.connect(self.ok_clicked) self.ui.pushButton_cancel.clicked.connect(self.close) self.ui.comboBox_tool.currentIndexChanged.connect(self.update_args) @Slot(int, name='update_args') def update_args(self, row): """Show Tool specification command line arguments in text input. Args: row (int): Selected row number """ if row == 0: # No Tool selected self.ui.lineEdit_tool_args.setText("") return selected_tool = self._toolbox.tool_specification_model.tool_specification( row) args = selected_tool.cmdline_args if not args: # Tool cmdline_args is None if the line does not exist in Tool definition file args = '' self.ui.lineEdit_tool_args.setText("{0}".format(args)) return @Slot(name='name_changed') def name_changed(self): """Update label to show upcoming folder name.""" name = self.ui.lineEdit_name.text() default = "Folder:" if name == '': self.ui.label_folder.setText(default) else: folder_name = name.lower().replace(' ', '_') msg = default + " " + folder_name self.ui.label_folder.setText(msg) @Slot(name='ok_clicked') def ok_clicked(self): """Check that given item name is valid and add it to project.""" self.name = self.ui.lineEdit_name.text() self.description = self.ui.lineEdit_description.text() if not self.name: # No name given self.statusbar.showMessage("Name missing", 3000) return # Check for invalid characters for a folder name if any((True for x in self.name if x in INVALID_CHARS)): self.statusbar.showMessage("Name not valid for a folder name", 3000) return # Check that name is not reserved if self._toolbox.project_item_model.find_item(self.name): msg = "Item '{0}' already exists".format(self.name) self.statusbar.showMessage(msg, 3000) return # Check that short name (folder) is not reserved short_name = self.name.lower().replace(' ', '_') if self._toolbox.project_item_model.short_name_reserved(short_name): msg = "Item using folder '{0}' already exists".format(short_name) self.statusbar.showMessage(msg, 3000) return # Create new Item self.call_add_item() self.close() def call_add_item(self): """Creates new Item according to user's selections.""" tool = self.ui.comboBox_tool.currentText() item = dict(name=self.name, description=self.description, x=self._x, y=self._y, tool=tool, execute_in_work=True) self._project.add_project_items("Tools", item, set_selected=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() elif e.key() == Qt.Key_Enter or e.key() == Qt.Key_Return: self.ok_clicked() def closeEvent(self, event=None): """Handle close window. Args: event (QEvent): Closing event if 'X' is clicked. """ if event: event.accept() scene = self._toolbox.ui.graphicsView.scene() item_shadow = scene.item_shadow if item_shadow: scene.removeItem(item_shadow) scene.item_shadow = None
class SonetPCPManagerQt(QDialog, sonet_pcp_manager_ui.Ui_sonet_pcp_manager): """ Window in charge of managing the available PCP trajectories within the app, and also generates new ones, if desired. The matlab data is stored in .mat files, in matrix format. The app needs pcp data in table format. The tables are stored in .pkl files. """ def __init__(self, *args, p_main_window=None, p_mat_eng=None): super(SonetPCPManagerQt, self).__init__(*args) self.setupUi(self) self.setModal(True) self.show() # Reference to the main window. self._p_main_window = p_main_window # Reference to matlab engine. self._p_matlab_engine = p_mat_eng # Some widgets settings. self.sonet_read_pcp_qtw.setHeaderLabels([ 'Selected .mat files', 'Total trajectories', 'Departure Dates', 'TOFs' ]) self.matrix_tw_outgoing_root_item = QTreeWidgetItem( self.sonet_read_pcp_qtw, ['Earth-Mars', '', '', '']) self.matrix_tw_incoming_root_item = QTreeWidgetItem( self.sonet_read_pcp_qtw, ['Mars-Earth', '', '', '']) self.resize_matrix_tw_columns() self.sonet_working_pcp_qtw.setHeaderLabels( ['Selected .pkl files', 'Rows', 'Columns']) self.table_tw_outgoing_root_item = QTreeWidgetItem( self.sonet_working_pcp_qtw, ['Earth-Mars', '', '']) self.table_tw_incoming_root_item = QTreeWidgetItem( self.sonet_working_pcp_qtw, ['Mars-Earth', '', '']) self.resize_table_tw_columns() # Status bar,for messages to the user. self.status_bar = QStatusBar() self.status_bar.setBaseSize(580, 22) self.status_bar.setSizeGripEnabled(False) self.status_bar_HLayout.addWidget(self.status_bar) # Other members. self._pcp_mat_file_incoming = None self._pcp_mat_file_outgoing = None self._pcp_table_file_incoming = None self._pcp_table_file_outgoing = None # Signals and slots connect. self.sonet_read_pcp_outgoing_trajectories_matrix_qpb.clicked.connect( self.clicked_read_pcp_matrix_file_outgoing) self.sonet_read_pcp_incoming_trajectories_matrix_qpb.clicked.connect( self.clicked_read_pcp_matrix_file_incoming) self.sonet_dvt_limit_qcb.stateChanged.connect( self.clicked_dvt_limit_checkbox) self.sonet_convert_pcp_2_table_format_qpb.clicked.connect( self.clicked_convert_pcp_2_table_format) self.sonet_read_pcp_outgoing_trajectories_table_qpb.clicked.connect( self.clicked_read_pcp_table_file_outgoing) self.sonet_read_pcp_incoming_trajectories_table_qpb.clicked.connect( self.clicked_read_pcp_table_file_incoming) self.matlab_pcp_generator_pb.clicked.connect( self.clicked_matlab_pcp_generator) self.btn_OK = self.sonet_ok_cancel_qpb_group.button( QDialogButtonBox.Ok) self.btn_OK.clicked.connect(self.clicked_ok) self.btn_OK.clicked.connect(self.accept) self.btn_cancel = self.sonet_ok_cancel_qpb_group.button( QDialogButtonBox.Cancel) self.btn_cancel.clicked.connect(self.clicked_cancel) self.btn_cancel.clicked.connect(self.reject) # If there's a currently working pcp in the database, display it to the user. self.read_database_pcp() # sonet_log(SonetLogType.INFO, 'class_tal.method_tal') # self.status_bar.showMessage('tal.', SONET_MSG_TIMEOUT) def read_database_pcp(self): """ If there's a currently working pcp in the database, display it to the user. """ working_pcp_paths = database.get_working_pcp_paths() pkl_file_outgoing_path = working_pcp_paths[0] pkl_file_incoming_path = working_pcp_paths[1] # Fill the pkl files path line edit widgets. self.sonet__outgoing_trajectories_table_line_edit.setText( pkl_file_outgoing_path) self.sonet__incoming_trajectories_table_line_edit.setText( pkl_file_incoming_path) # Set the members pkl files & update the table tree view. if pkl_file_outgoing_path: self._pcp_table_file_outgoing = database.get_pcp_table( TripType.OUTGOING) self.update_table_tree_view(pkl_file_outgoing_path, p_trip='Earth-Mars') if pkl_file_incoming_path: self._pcp_table_file_incoming = database.get_pcp_table( TripType.INCOMING) self.update_table_tree_view(pkl_file_incoming_path, p_trip='Mars-Earth') def clicked_ok(self): """ Sets the current working pcp files (if any) & close the window. If we have changed the outgoing or incoming pkl files, all the currently set trajectories are going to be reset. If the s/c had any filter, it remains unchanged. """ pkl_file_path_outgoing = self.sonet__outgoing_trajectories_table_line_edit.text( ) pkl_file_path_incoming = self.sonet__incoming_trajectories_table_line_edit.text( ) has_changed_outgoing = database.set_working_pcp( TripType.OUTGOING, pkl_file_path_outgoing) has_changed_incoming = database.set_working_pcp( TripType.INCOMING, pkl_file_path_incoming) # Reset the trajectories for ALL the s/c if necessary, AND inform to the user. if has_changed_outgoing or has_changed_incoming: reset_sc_filters_and_trajectories( p_filters_and_trajectories='Trajectories') self._p_main_window.statusbar.showMessage( 'Database has changed. Selected trajectories reset for ALL s/c', SONET_MSG_TIMEOUT * 3) self._exit_status = 'clicked_ok_and_changed' else: self._exit_status = 'clicked_ok' def clicked_cancel(self): """ Abort all the operations & close the window. """ self._exit_status = 'clicked_cancel' def clicked_convert_pcp_2_table_format(self): """ Converts matrix to tabular pcp data. .mat -> .pkl. - Reads all the current available .mat files. - Converts them to tabular format (pandas dataframes). - Also does some filtering (i.e. max dvt). - Saves the tables to pickle .pkl files. """ # self.status_bar.showMessage('SonetPCPManagerQt.clicked_convert_pcp_2_table_format."Not implemented."', # SONET_MSG_TIMEOUT) # Check if user has limited the dvt value. dvt_widget = self.sonet_dvt_limit_qdoublespinbox if dvt_widget.isEnabled(): dvt_limit_value = dvt_widget.value() else: dvt_limit_value = None # Convert mat obj to dataframe obj. result = [] for mat_file in [ self._pcp_mat_file_outgoing, self._pcp_mat_file_incoming ]: if mat_file is None: result.append(None) else: result.append( self.convert_mat_2_dataframe(mat_file, dvt_limit_value)) # Save the dataframes to pickle files. for df in result: df: pd.DataFrame if df is not None: file_path = df.attrs['file_name'] file_name = file_path.split('/')[-2] self.status_bar.showMessage('Writing' + file_name + 'pkl file', 2 * SONET_MSG_TIMEOUT) df.to_pickle(file_path) self.status_bar.showMessage('Pickle files written', 2 * SONET_MSG_TIMEOUT) # Remove mat files and reset associated widgets. self.reset_matrix_widgets() def clicked_dvt_limit_checkbox(self): """ Activate/deactivate the dvt limit line edit widget, depending of the check box state. """ self.sonet_dvt_limit_qdoublespinbox.setEnabled( self.sonet_dvt_limit_qcb.isChecked()) def clicked_matlab_pcp_generator(self): self._p_matlab_engine.PCPGenerator(nargout=0) def clicked_read_pcp_matrix_file_incoming(self): """ Opens a select file dialog, the user has to select a valid matlab .mat file containing the pcp data. """ file_path, filter_ = QFileDialog.getOpenFileName( parent=self, caption='Read PCP file (.mat)', dir=SONET_PCP_DATA_DIR, filter='*.mat') if file_path: self.sonet__incoming_trajectories_matrix_line_edit.setText( file_path) else: # The user canceled the open file window. return self._pcp_mat_file_incoming = loadmat(file_path) self.status_bar.showMessage('PCP incoming .mat file read', SONET_MSG_TIMEOUT) file_name = file_path.split('/')[-2] my_mat_file_dep_dates = self._pcp_mat_file_incoming[ 'departure_dates'].shape[1] my_mat_file_tofs = self._pcp_mat_file_incoming['tofs'].shape[1] my_mat_file_total_trajectories = my_mat_file_dep_dates * my_mat_file_tofs self.fill_matrix_QTreeWidget('Mars-Earth', file_name, str(my_mat_file_total_trajectories), str(my_mat_file_dep_dates), str(my_mat_file_dep_dates)) def clicked_read_pcp_matrix_file_outgoing(self): """ Opens a select file dialog, the user has to select a valid matlab .mat file containing the pcp data. """ # Read the .mat file. file_path, filter_ = QFileDialog.getOpenFileName( parent=self, caption='Read PCP file (.mat)', dir=SONET_PCP_DATA_DIR, filter='*.mat') if file_path: self.sonet__outgoing_trajectories_matrix_line_edit.setText( file_path) else: # The user canceled the open file window. return self._pcp_mat_file_outgoing = loadmat(file_path) self.status_bar.showMessage('PCP outgoing .mat file read', SONET_MSG_TIMEOUT) file_name = file_path.split('/')[-2] my_mat_file_dep_dates = self._pcp_mat_file_outgoing[ 'departure_dates'].shape[1] my_mat_file_tofs = self._pcp_mat_file_outgoing['tofs'].shape[1] my_mat_file_total_trajectories = my_mat_file_dep_dates * my_mat_file_tofs self.fill_matrix_QTreeWidget('Earth-Mars', file_name, str(my_mat_file_total_trajectories), str(my_mat_file_dep_dates), str(my_mat_file_dep_dates)) def clicked_read_pcp_table_file_incoming(self): """ Opens a select file dialog, the user has to select a valid pickle .pkl file containing the pcp data. """ file_path, filter_ = QFileDialog.getOpenFileName( parent=self, caption='Read PCP file (.pkl)', dir=SONET_PCP_DATA_DIR, filter='*.pkl') if file_path: self.sonet__incoming_trajectories_table_line_edit.setText( file_path) else: # The user canceled the open file window. return # Read the Python .pkl file. self._pcp_table_file_incoming = pd.read_pickle(file_path) self.status_bar.showMessage('PCP incoming .pkl file read', SONET_MSG_TIMEOUT) # Update the bottom tree view. self.update_table_tree_view(file_path, p_trip='Mars-Earth') def update_table_tree_view(self, a_file_path, p_trip=''): a_file_name = a_file_path.split('/')[-2] if p_trip == 'Earth-Mars': my_pkl_file_rows = self._pcp_table_file_outgoing.shape[0] my_pkl_file_cols = self._pcp_table_file_outgoing.shape[1] self.fill_table_QTreeWidget('Earth-Mars', a_file_name, str(my_pkl_file_rows), str(my_pkl_file_cols)) else: my_pkl_file_rows = self._pcp_table_file_incoming.shape[0] my_pkl_file_cols = self._pcp_table_file_incoming.shape[1] self.fill_table_QTreeWidget('Mars-Earth', a_file_name, str(my_pkl_file_rows), str(my_pkl_file_cols)) def clicked_read_pcp_table_file_outgoing(self): """ Opens a select file dialog, the user has to select a valid pickle .pkl file containing the pcp data. """ # Read the file name. file_path, filter_ = QFileDialog.getOpenFileName( parent=self, caption='Read PCP file (.pkl)', dir=SONET_PCP_DATA_DIR, filter='*.pkl') if file_path: self.sonet__outgoing_trajectories_table_line_edit.setText( file_path) else: # The user canceled the open file window. return # Read the Python .pkl file. self._pcp_table_file_outgoing = pd.read_pickle(file_path) self.status_bar.showMessage('PCP outgoing .pkl file read', SONET_MSG_TIMEOUT) # Update the bottom tree view. self.update_table_tree_view(file_path, p_trip='Earth-Mars') def reset_matrix_widgets(self): self._pcp_mat_file_outgoing = None self._pcp_mat_file_incoming = None self.sonet_dvt_limit_qcb.setChecked(False) self.sonet__outgoing_trajectories_matrix_line_edit.clear() self.sonet__incoming_trajectories_matrix_line_edit.clear() self.matrix_tw_outgoing_root_item.takeChildren() self.matrix_tw_incoming_root_item.takeChildren() @staticmethod def convert_mat_2_dataframe(a_my_mat_file, a_dvt_limit=None) -> pd.DataFrame: """ Converts the input mat file to tabular data, in the form of pandas dataframe obj. :param a_my_mat_file: input mat file. :param a_dvt_limit: the max dvt allowed for the output dataframe. """ # Initialize the data structures. table_rows = a_my_mat_file['departure_dates'].shape[ 1] # Table rows is N. table_size = table_rows**2 # Table size is N^2. rows = [i for i in range(table_size)] cols = ['DepDates', 'tof', 'c3d', 'c3a', 'dvd', 'dva', 'dvt', 'theta'] data = np.zeros((table_size, len(cols)), dtype=np.double) # Do the conversion. get_value = SonetPCPManagerQt._fill_the_table_data row = 0 for i in range(0, table_rows): # For each departure date. # print(i) for j in range(0, table_rows): # For each tof. # First, check max dvt, if greater than the cut-off value, discard this table row. dvt = get_value(a_my_mat_file, 'dvt', i, j) if dvt > a_dvt_limit: continue # Independent vars are (1,table_rows) ndarrays. data[row, 0] = get_value(a_my_mat_file, 'departure_dates', 0, i) data[row, 1] = get_value(a_my_mat_file, 'tofs', 0, j) # Dependent vars are (table_rows,table_rows) ndarrays. data[row, 2] = get_value(a_my_mat_file, 'c3d', i, j) data[row, 3] = get_value(a_my_mat_file, 'c3a', i, j) data[row, 4] = get_value(a_my_mat_file, 'dvd', i, j) data[row, 5] = get_value(a_my_mat_file, 'dva', i, j) data[row, 6] = dvt data[row, 7] = get_value(a_my_mat_file, 'theta', i, j) row = row + 1 # If the user has set a dvt limit, the returned dataframe my have less rows than the original expected. if a_dvt_limit: unfilled_rows = len(rows) - row df = pd.DataFrame(data[:-unfilled_rows, :], columns=cols, index=[i for i in range(row)]) else: df = pd.DataFrame(data, columns=cols, index=rows) # Add 'ArrivDates' column and perform some conversions and columns reordering. df = SonetPCPManagerQt.post_process(df) # Add attributes to the dataframe. df.attrs['file_name'] = str(a_my_mat_file['fname'][0]) + '.pkl' df.attrs['limit_dvt'] = a_dvt_limit df.attrs['memory_usage'] = int(df.memory_usage().sum() / 1e6) df.attrs['m_departure_dates'] = a_my_mat_file[ 'departure_dates'].tolist()[0] df.attrs['m_tofs'] = a_my_mat_file['tofs'].tolist()[0] return df @staticmethod def _fill_the_table_data(a_my_mat_file, var='', row=0, col=0): return a_my_mat_file[var][row, col] def fill_matrix_QTreeWidget(self, a_file_type='', a_file_name='', a_total_trajectories='0', a_total_dep_dates='0', a_total_tofs='0', p_clean_qtw_before_cleaning=False): if p_clean_qtw_before_cleaning: self.sonet_read_pcp_qtw.clear() # root_node = self.sonet_read_pcp_qtw.invisibleRootItem() if a_file_type == 'Earth-Mars': self.matrix_tw_outgoing_root_item.takeChildren() the_new_item = QTreeWidgetItem(self.matrix_tw_outgoing_root_item, [ a_file_name, a_total_trajectories, a_total_dep_dates, a_total_tofs ]) else: self.matrix_tw_incoming_root_item.takeChildren() the_new_item = QTreeWidgetItem(self.matrix_tw_incoming_root_item, [ a_file_name, a_total_trajectories, a_total_dep_dates, a_total_tofs ]) self.matrix_tw_outgoing_root_item.setExpanded(True) self.matrix_tw_incoming_root_item.setExpanded(True) self.resize_matrix_tw_columns() def fill_table_QTreeWidget(self, a_file_type='', a_file_name='', a_total_rows='0', a_total_cols='0', p_clean_qtw_before_cleaning=False): if p_clean_qtw_before_cleaning: self.sonet_working_pcp_qtw.clear() if a_file_type == 'Earth-Mars': self.table_tw_outgoing_root_item.takeChildren() the_new_item = QTreeWidgetItem( self.table_tw_outgoing_root_item, [a_file_name, a_total_rows, a_total_cols]) else: self.table_tw_incoming_root_item.takeChildren() the_new_item = QTreeWidgetItem( self.table_tw_incoming_root_item, [a_file_name, a_total_rows, a_total_cols]) self.table_tw_outgoing_root_item.setExpanded(True) self.table_tw_incoming_root_item.setExpanded(True) self.resize_table_tw_columns() def resize_matrix_tw_columns(self): self.sonet_read_pcp_qtw.resizeColumnToContents(0) self.sonet_read_pcp_qtw.resizeColumnToContents(1) self.sonet_read_pcp_qtw.resizeColumnToContents(2) self.sonet_read_pcp_qtw.resizeColumnToContents(3) def resize_table_tw_columns(self): self.sonet_working_pcp_qtw.resizeColumnToContents(0) self.sonet_working_pcp_qtw.resizeColumnToContents(1) self.sonet_working_pcp_qtw.resizeColumnToContents(2) @staticmethod def post_process(a_df: pd.DataFrame): """ Given a input dataframe: - Creates a new column called 'ArrivDates', which is a lineal combination of 'DepDates' and 'tof'. - Converts 'DepDates' and 'ArrivDates' from JD2000 to JD, as QDate objects work with absolute dates. - Converts 'theta' from radians to sexagesimal degrees. - Reorder columns in a more convenient way. :param a_df: a Pandas DataFrame. """ # Create 'ArrivDates' column. a_df['ArrivDates'] = a_df.DepDates + a_df.tof # Convert dates JD2000 to JD. JD2000 = 2451545.0 # Julian Day 2000, extracted from AstroLib matlab code. a_df['DepDates'] = (a_df.DepDates + JD2000) a_df['ArrivDates'] = (a_df.ArrivDates + JD2000) # Convert theta rad to º. a_df.theta = a_df.theta * 180 / np.pi # Reorder columns. reordered_cols = [ 'DepDates', 'ArrivDates', 'tof', 'theta', 'dvt', 'dvd', 'dva', 'c3d', 'c3a' ] return a_df.reindex(columns=reordered_cols)
class WTreeEdit(QWidget): """TreeEdit widget is to show and edit all of the pyleecan objects data.""" # Signals dataChanged = Signal() def __init__(self, obj, *args, **kwargs): QWidget.__init__(self, *args, **kwargs) self.class_dict = ClassInfo().get_dict() self.treeDict = None # helper to track changes self.obj = obj # the object self.is_save_needed = False self.model = TreeEditModel(obj) self.setupUi() # === Signals === self.selectionModel.selectionChanged.connect(self.onSelectionChanged) self.treeView.collapsed.connect(self.onItemCollapse) self.treeView.expanded.connect(self.onItemExpand) self.treeView.customContextMenuRequested.connect(self.openContextMenu) self.model.dataChanged.connect(self.onDataChanged) self.dataChanged.connect(self.setSaveNeeded) # === Finalize === # set 'root' the selected item and resize columns self.treeView.setCurrentIndex(self.treeView.model().index(0, 0)) self.treeView.resizeColumnToContents(0) def setupUi(self): """Setup the UI""" # === Widgets === # TreeView self.treeView = QTreeView() # self.treeView.rootNode = model.invisibleRootItem() self.treeView.setModel(self.model) self.treeView.setAlternatingRowColors(False) # self.treeView.setColumnWidth(0, 150) self.treeView.setMinimumWidth(100) self.treeView.setContextMenuPolicy(Qt.CustomContextMenu) self.selectionModel = self.treeView.selectionModel() self.statusBar = QStatusBar() self.statusBar.setSizeGripEnabled(False) self.statusBar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) self.statusBar.setStyleSheet( "QStatusBar {border: 1px solid rgb(200, 200, 200)}") self.saveLabel = QLabel("unsaved") self.saveLabel.setVisible(False) self.statusBar.addPermanentWidget(self.saveLabel) # Splitters self.leftSplitter = QSplitter() self.leftSplitter.setStretchFactor(0, 0) self.leftSplitter.setStretchFactor(1, 1) # === Layout === # Horizontal Div. self.hLayout = QVBoxLayout() self.hLayout.setContentsMargins(0, 0, 0, 0) self.hLayout.setSpacing(0) # add widgets to layout self.hLayout.addWidget(self.leftSplitter) self.hLayout.addWidget(self.statusBar) # add widgets self.leftSplitter.addWidget(self.treeView) self.setLayout(self.hLayout) def update(self, obj): """Check if object has changed and update tree in case.""" if not obj is self.obj: self.obj = obj self.model = TreeEditModel(obj) self.treeView.setModel(self.model) self.model.dataChanged.connect(self.onDataChanged) self.selectionModel = self.treeView.selectionModel() self.selectionModel.selectionChanged.connect( self.onSelectionChanged) self.treeView.setCurrentIndex(self.treeView.model().index(0, 0)) self.setSaveNeeded(True) def setSaveNeeded(self, state=True): self.is_save_needed = state self.saveLabel.setVisible(state) def openContextMenu(self, point): """Generate and open context the menu at the given point position.""" index = self.treeView.indexAt(point) pos = QtGui.QCursor.pos() if not index.isValid(): return # get the data item = self.model.item(index) obj_info = self.model.get_obj_info(item) # init the menu menu = TreeEditContextMenu(obj_dict=obj_info, parent=self) menu.exec_(pos) self.onSelectionChanged(self.selectionModel.selection()) def onItemCollapse(self, index): """Slot for item collapsed""" # dynamic resize for ii in range(3): self.treeView.resizeColumnToContents(ii) def onItemExpand(self, index): """Slot for item expand""" # dynamic resize for ii in range(3): self.treeView.resizeColumnToContents(ii) def onDataChanged(self, first=None, last=None): """Slot for changed data""" self.dataChanged.emit() self.onSelectionChanged(self.selectionModel.selection()) def onSelectionChanged(self, itemSelection): """Slot for changed item selection""" # get the index if itemSelection.indexes(): index = itemSelection.indexes()[0] else: index = self.treeView.model().index(0, 0) self.treeView.setCurrentIndex(index) return # get the data item = self.model.item(index) obj = item.object() typ = type(obj).__name__ obj_info = self.model.get_obj_info(item) ref_typ = obj_info["ref_typ"] if obj_info else None # set statusbar information on class typ msg = f"{typ} (Ref: {ref_typ})" if ref_typ else f"{typ}" self.statusBar.showMessage(msg) # --- choose the respective widget by class type --- # numpy array -> table editor if typ == "ndarray": widget = WTableData(obj, editable=True) widget.dataChanged.connect(self.dataChanged.emit) elif typ == "MeshSolution": widget = WMeshSolution(obj) # only a view (not editable) # list (no pyleecan type, non empty) -> table editor # TODO add another widget for lists of non 'primitive' types (e.g. DataND) elif isinstance(obj, list) and not self.isListType(ref_typ) and obj: widget = WTableData(obj, editable=True) widget.dataChanged.connect(self.dataChanged.emit) # generic editor else: # widget = SimpleInputWidget().generate(obj) widget = WTableParameterEdit(obj) widget.dataChanged.connect(self.dataChanged.emit) # show the widget if self.leftSplitter.widget(1) is None: self.leftSplitter.addWidget(widget) else: self.leftSplitter.replaceWidget(1, widget) widget.setParent( self.leftSplitter) # workaround for PySide2 replace bug widget.show() pass def isListType(self, typ): if not typ: return False return typ[0] == "[" and typ[-1] == "]" and typ[1:-1] in self.class_dict def isDictType(self, typ): if not typ: return False return typ[0] == "{" and typ[-1] == "}" and typ[1:-1] in self.class_dict
class SettingsWidget(QWidget): """ A widget to change user's preferred settings. Attributes: toolbox (ToolboxUI): Parent widget. configs (ConfigurationParser): Configuration object """ def __init__(self, toolbox, configs): """ Initialize class. """ # FIXME: setting the parent to toolbox causes the checkboxes in the # groupBox_general to not layout correctly, this might be caused elsewhere? super().__init__( parent=None) # Do not set parent. Uses own stylesheet. self._toolbox = toolbox # QWidget parent self._configs = configs self._project = self._toolbox.project() self.orig_work_dir = "" # Work dir when this widget was opened # Set up the ui from Qt Designer files self.ui = ui.settings.Ui_SettingsForm() self.ui.setupUi(self) self.ui.toolButton_browse_gams.setIcon(self.style().standardIcon( QStyle.SP_DialogOpenButton)) self.ui.toolButton_browse_julia.setIcon(self.style().standardIcon( QStyle.SP_DialogOpenButton)) self.ui.toolButton_browse_work.setIcon(self.style().standardIcon( QStyle.SP_DialogOpenButton)) self.setWindowFlags(Qt.Window | Qt.CustomizeWindowHint) # Ensure this window gets garbage-collected when closed self.setAttribute(Qt.WA_DeleteOnClose) 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) self.setStyleSheet(SETTINGS_SS) self.ui.pushButton_ok.setDefault(True) self._mousePressPos = None self._mouseReleasePos = None self._mouseMovePos = None self.connect_signals() self.read_settings() self.read_project_settings() def connect_signals(self): """ Connect PyQt signals. """ self.ui.pushButton_ok.clicked.connect(self.ok_clicked) self.ui.pushButton_cancel.clicked.connect(self.close) self.ui.toolButton_browse_gams.clicked.connect(self.browse_gams_path) self.ui.toolButton_browse_julia.clicked.connect(self.browse_julia_path) self.ui.toolButton_browse_work.clicked.connect(self.browse_work_path) @Slot(bool, name="browse_gams_path") def browse_gams_path(self, checked=False): """Open file browser where user can select the directory of GAMS that the user wants to use.""" # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QFileDialog.getExistingDirectory(self, 'Select GAMS Directory', os.path.abspath('C:\\')) if answer == '': # Cancel button clicked return selected_path = os.path.abspath(answer) gams_path = os.path.join(selected_path, GAMS_EXECUTABLE) gamside_path = os.path.join(selected_path, GAMSIDE_EXECUTABLE) if not os.path.isfile(gams_path) and not os.path.isfile(gamside_path): self.statusbar.showMessage( "gams.exe and gamside.exe not found in selected directory", 10000) self.ui.lineEdit_gams_path.setText("") return else: self.statusbar.showMessage( "Selected directory is valid GAMS directory", 10000) self.ui.lineEdit_gams_path.setText(selected_path) return @Slot(bool, name="browse_julia_path") def browse_julia_path(self, checked=False): """Open file browser where user can select the path to wanted Julia version.""" # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QFileDialog.getExistingDirectory(self, 'Select Julia Directory', os.path.abspath('C:\\')) if answer == '': # Cancel button clicked return selected_path = os.path.abspath(answer) julia_path = os.path.join(selected_path, JULIA_EXECUTABLE) if not os.path.isfile(julia_path): self.statusbar.showMessage( "julia.exe not found in selected directory", 10000) self.ui.lineEdit_julia_path.setText("") return else: self.statusbar.showMessage( "Selected directory is valid Julia directory", 10000) self.ui.lineEdit_julia_path.setText(selected_path) return @Slot(bool, name="browse_work_path") def browse_work_path(self, checked=False): """Open file browser where user can select the path to wanted work directory.""" # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QFileDialog.getExistingDirectory(self, 'Select work directory', os.path.abspath('C:\\')) if answer == '': # Cancel button clicked return selected_path = os.path.abspath(answer) self.ui.lineEdit_work_dir.setText(selected_path) def read_settings(self): """Read current settings from config object and update UI to show them.""" open_previous_project = self._configs.getboolean( "settings", "open_previous_project") show_exit_prompt = self._configs.getboolean("settings", "show_exit_prompt") save_at_exit = self._configs.get("settings", "save_at_exit") # Tri-state checkBox commit_at_exit = self._configs.get( "settings", "commit_at_exit") # Tri-state checkBox use_smooth_zoom = self._configs.getboolean("settings", "use_smooth_zoom") proj_dir = self._configs.get("settings", "project_directory") datetime = self._configs.getboolean("settings", "datetime") gams_path = self._configs.get("settings", "gams_path") use_repl = self._configs.getboolean("settings", "use_repl") julia_path = self._configs.get("settings", "julia_path") delete_data = self._configs.getboolean("settings", "delete_data") if open_previous_project: self.ui.checkBox_open_previous_project.setCheckState(Qt.Checked) if show_exit_prompt: self.ui.checkBox_exit_prompt.setCheckState(Qt.Checked) if save_at_exit == "0": # Not needed but makes the code more readable. self.ui.checkBox_save_at_exit.setCheckState(Qt.Unchecked) elif save_at_exit == "1": self.ui.checkBox_save_at_exit.setCheckState(Qt.PartiallyChecked) elif save_at_exit == "2": self.ui.checkBox_save_at_exit.setCheckState(Qt.Checked) else: # default self.ui.checkBox_save_at_exit.setCheckState(Qt.PartiallyChecked) if commit_at_exit == "0": # Not needed but makes the code more readable. self.ui.checkBox_commit_at_exit.setCheckState(Qt.Unchecked) elif commit_at_exit == "1": self.ui.checkBox_commit_at_exit.setCheckState(Qt.PartiallyChecked) elif commit_at_exit == "2": self.ui.checkBox_commit_at_exit.setCheckState(Qt.Checked) else: # default self.ui.checkBox_commit_at_exit.setCheckState(Qt.PartiallyChecked) if use_smooth_zoom: self.ui.checkBox_use_smooth_zoom.setCheckState(Qt.Checked) if datetime: self.ui.checkBox_datetime.setCheckState(Qt.Checked) if delete_data: self.ui.checkBox_delete_data.setCheckState(Qt.Checked) if not proj_dir: proj_dir = DEFAULT_PROJECT_DIR self.ui.lineEdit_project_dir.setText(proj_dir) self.ui.lineEdit_gams_path.setText(gams_path) if use_repl: self.ui.checkBox_use_repl.setCheckState(Qt.Checked) self.ui.lineEdit_julia_path.setText(julia_path) def read_project_settings(self): """Read project settings from config object and update settings widgets accordingly.""" work_dir = DEFAULT_WORK_DIR if self._project: self.ui.lineEdit_project_name.setText(self._project.name) self.ui.textEdit_project_description.setText( self._project.description) work_dir = self._project.work_dir self.ui.lineEdit_work_dir.setText(work_dir) self.orig_work_dir = work_dir @Slot(name='ok_clicked') def ok_clicked(self): """Get selections and save them to conf file.""" a = int(self.ui.checkBox_open_previous_project.checkState()) b = int(self.ui.checkBox_exit_prompt.checkState()) f = str(int(self.ui.checkBox_save_at_exit.checkState())) g = str(int(self.ui.checkBox_commit_at_exit.checkState())) h = int(self.ui.checkBox_use_smooth_zoom.checkState()) d = int(self.ui.checkBox_datetime.checkState()) delete_data = int(self.ui.checkBox_delete_data.checkState()) # Check that GAMS directory is valid. Set it empty if not. gams_path = self.ui.lineEdit_gams_path.text() if not gams_path == "": # Skip this if using GAMS in system path gams_exe_path = os.path.join(gams_path, GAMS_EXECUTABLE) gamside_exe_path = os.path.join(gams_path, GAMSIDE_EXECUTABLE) if not os.path.isfile(gams_exe_path) and not os.path.isfile( gamside_exe_path): self.statusbar.showMessage( "GAMS executables not found in selected directory", 10000) return e = int(self.ui.checkBox_use_repl.checkState()) # Check that Julia directory is valid. Set it empty if not. julia_path = self.ui.lineEdit_julia_path.text() if not julia_path == "": # Skip this if using Julia in system path julia_exe_path = os.path.join(julia_path, JULIA_EXECUTABLE) if not os.path.isfile(julia_exe_path): self.statusbar.showMessage( "Julia executable not found in selected directory", 10000) return # Write to config object self._configs.setboolean("settings", "open_previous_project", a) self._configs.setboolean("settings", "show_exit_prompt", b) self._configs.set("settings", "save_at_exit", f) self._configs.set("settings", "commit_at_exit", g) self._configs.setboolean("settings", "use_smooth_zoom", h) self._configs.setboolean("settings", "datetime", d) self._configs.setboolean("settings", "delete_data", delete_data) self._configs.set("settings", "gams_path", gams_path) self._configs.setboolean("settings", "use_repl", e) self._configs.set("settings", "julia_path", julia_path) # Update project settings self.update_project_settings() self._configs.save() self.close() def update_project_settings(self): """Update project settings when Ok has been clicked.""" if not self._project: return save = False if not self.ui.lineEdit_work_dir.text(): work_dir = DEFAULT_WORK_DIR else: work_dir = self.ui.lineEdit_work_dir.text() # Check if work directory was changed if not self.orig_work_dir == work_dir: if not self._project.change_work_dir(work_dir): self._toolbox.msg_error.emit( "Could not change project work directory. Creating new dir:{0} failed " .format(work_dir)) else: save = True if not self._project.description == self.ui.textEdit_project_description.toPlainText( ): # Set new project description self._project.set_description( self.ui.textEdit_project_description.toPlainText()) save = True if save: self._toolbox.msg.emit( "Project settings changed. Saving project...") self._toolbox.save_project() def keyPressEvent(self, e): """Close settings 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 mousePressEvent(self, e): """Save mouse position at the start of dragging. Args: e (QMouseEvent): Mouse event """ self._mousePressPos = e.globalPos() self._mouseMovePos = e.globalPos() super().mousePressEvent(e) def mouseReleaseEvent(self, e): """Save mouse position at the end of dragging. Args: e (QMouseEvent): Mouse event """ if self._mousePressPos is not None: self._mouseReleasePos = e.globalPos() moved = self._mouseReleasePos - self._mousePressPos if moved.manhattanLength() > 3: e.ignore() return def mouseMoveEvent(self, e): """Moves the window when mouse button is pressed and mouse cursor is moved. Args: e (QMouseEvent): Mouse event """ # logging.debug("MouseMoveEvent at pos:%s" % e.pos()) # logging.debug("MouseMoveEvent globalpos:%s" % e.globalPos()) currentpos = self.pos() globalpos = e.globalPos() if not self._mouseMovePos: e.ignore() return diff = globalpos - self._mouseMovePos newpos = currentpos + diff self.move(newpos) self._mouseMovePos = globalpos