Source code for widgets.tool_template_widget

######################################################################################################################
# Copyright (C) 2017 - 2019 Spine project consortium
# This file is part of Spine Toolbox.
# Spine Toolbox is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
######################################################################################################################

"""
QWidget that is used to create or edit Tool Templates.
In the former case it is presented empty, but in the latter it
is filled with all the information from the template being edited.

:author: M. Marin (KTH), P. Savolainen (VTT)
:date:   12.4.2018
"""

import os
import json
from PySide2.QtGui import QStandardItemModel, QStandardItem
from PySide2.QtWidgets import QWidget, QStatusBar, QInputDialog, QFileDialog, QFileIconProvider, QMessageBox
from PySide2.QtCore import Slot, Qt, QUrl, QFileInfo
from PySide2.QtGui import QDesktopServices
from ui.tool_template_form import Ui_Form
from config import STATUSBAR_SS, TREEVIEW_HEADER_SS, APPLICATION_PATH, TOOL_TYPES, REQUIRED_KEYS, INVALID_FILENAME_CHARS
from helpers import busy_effect
from widgets.custom_menus import AddIncludesPopupMenu, CreateMainProgramPopupMenu


[docs]class ToolTemplateWidget(QWidget): """A widget to query user's preferences for a new tool template. Attributes: toolbox (ToolboxUI): QMainWindow instance tool_template (ToolTemplate): If given, the form is pre-filled with this template """ def __init__(self, toolbox, tool_template=None): """ Initialize class.""" 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 # NOTE: Trying out fontawesome icons # self.ui.toolButton_add_source_files.setIcon(self.style().standardIcon(QStyle.SP_FileLinkIcon)) # self.ui.toolButton_add_source_dirs.setIcon(self.style().standardIcon(QStyle.SP_FileDialogNewFolder)) # self.ui.toolButton_minus_source_files.setIcon(self.style().standardIcon(QStyle.SP_DialogDiscardButton)) # self.ui.toolButton_plus_inputfiles.setIcon(self.style().standardIcon(QStyle.SP_FileLinkIcon)) # self.ui.toolButton_minus_inputfiles.setIcon(self.style().standardIcon(QStyle.SP_DialogDiscardButton)) # self.ui.toolButton_plus_inputfiles_opt.setIcon(self.style().standardIcon(QStyle.SP_FileLinkIcon)) # self.ui.toolButton_minus_inputfiles_opt.setIcon(self.style().standardIcon(QStyle.SP_DialogDiscardButton)) # self.ui.toolButton_plus_outputfiles.setIcon(self.style().standardIcon(QStyle.SP_FileLinkIcon)) # self.ui.toolButton_minus_outputfiles.setIcon(self.style().standardIcon(QStyle.SP_DialogDiscardButton)) 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 template is given, fill the form with data from it if tool_template: self.ui.lineEdit_name.setText(tool_template.name) check_state = Qt.Checked if tool_template.execute_in_work else Qt.Unchecked self.ui.checkBox_execute_in_work.setCheckState(check_state) self.ui.textEdit_description.setPlainText(tool_template.description) self.ui.lineEdit_args.setText(tool_template.cmdline_args) tool_types = [x.lower() for x in TOOL_TYPES] index = tool_types.index(tool_template.tooltype) + 1 self.ui.comboBox_tooltype.setCurrentIndex(index) # Init lists self.main_program_file = "" self.sourcefiles = list(tool_template.includes) if tool_template else list() self.inputfiles = list(tool_template.inputfiles) if tool_template else list() self.inputfiles_opt = list(tool_template.inputfiles_opt) if tool_template else list() self.outputfiles = list(tool_template.outputfiles) if tool_template else list() self.def_file_path = tool_template.def_file_path if tool_template else None self.program_path = tool_template.path if tool_template 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.connect_signals()
[docs] 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.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)
[docs] 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)
[docs] 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)
[docs] 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)
[docs] 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")
[docs] 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")
[docs] 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(name="new_main_program_file")
[docs] def new_main_program_file(self): """Create a new blank main program file. Let user decide the file name and location.""" msg = "Main program file name?" # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QInputDialog.getText( self, "New main program", msg, flags=Qt.WindowTitleHint | Qt.WindowCloseButtonHint ) file_name = answer[0] if not file_name: # Cancel button clicked return if file_name.strip() == "": return # Check that file name has no invalid chars if any(True for x in file_name if x in INVALID_FILENAME_CHARS): msg = "File name <b>{0}</b> contains invalid characters.".format(file_name) # noinspection PyTypeChecker, PyArgumentList, PyCallByClass QMessageBox.information(self, "Creating file failed", msg) return # Let user select the directory where the new main program is saved. dir_msg = "Please choose a directory where the main program file is saved" # noinspection PyTypeChecker, PyArgumentList, PyCallByClass main_dir = QFileDialog.getExistingDirectory(self, dir_msg, APPLICATION_PATH, QFileDialog.ShowDirsOnly) if not main_dir: # Cancel button clicked return # Make new file file_path = os.path.abspath(os.path.join(main_dir, file_name)) if os.path.exists(file_path): msg = "File <b>{0}</b> already exists.".format(file_name) # noinspection PyTypeChecker, PyArgumentList, PyCallByClass QMessageBox.information(self, "Creating file failed", msg) return try: with open(file_path, "w"): self.statusbar.showMessage("New main program file {0} now available".format(file_path), 8000) except OSError: msg = "Please check directory permissions." # noinspection PyTypeChecker, PyArgumentList, PyCallByClass QMessageBox.information(self, "Creating file failed", msg) return 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_main_program_file") 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")
[docs] def new_source_file(self): """Let user create a new source file for this tool template.""" 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")
[docs] def show_add_source_files_dialog(self, checked=False): """Let user select source files for this tool template.""" 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")
[docs] def show_add_source_dirs_dialog(self, checked=False): """Let user select a source directory for this tool template. 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")
[docs] def add_dropped_includes(self, file_paths): for path in file_paths: if not self.add_single_include(path): continue
[docs] 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")
[docs] 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")
[docs] 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")
[docs] 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")
[docs] def add_inputfiles(self, checked=False): """Let user select input files for this tool template.""" 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")
[docs] def remove_inputfiles_with_del(self): """Support for deleting items with the Delete key.""" self.remove_inputfiles()
@Slot(bool, name="remove_inputfiles")
[docs] 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")
[docs] def add_inputfiles_opt(self, checked=False): """Let user select optional input files for this tool template.""" 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")
[docs] 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")
[docs] 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")
[docs] def add_outputfiles(self, checked=False): """Let user select output files for this tool template.""" msg = ( "Add output files that will be archived into the Tool results directory after the <br/>" "Tool template 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")
[docs] def remove_outputfiles_with_del(self): """Support for deleting items with the Delete key.""" self.remove_outputfiles()
@Slot(bool, name="remove_outputfiles")
[docs] 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(name="ok_clicked")
[docs] def ok_clicked(self): """Check that everything is valid, create definition dictionary and add template 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"] = " ".join(self.ui.lineEdit_args.text().split()) for k in REQUIRED_KEYS: if not self.definition[k]: self.statusbar.showMessage("{} missing".format(k), 3000) return # Create new Template 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_template(): self.close()
[docs] def call_add_tool_template(self): """Add or update Tool Template according to user's selections. If the name is the same as an existing tool template, it is updated and auto-saved to the definition file. (User is editing an existing tool template.) If the name is not in the tool template model, create a new tool template and offer to save the definition file. (User is creating a new tool template from scratch or spawning from an existing one). """ # Load tool template path = self.program_path tool = self._project.load_tool_template_from_dict(self.definition, path) if not tool: self.statusbar.showMessage("Adding Tool template failed", 3000) return False # Check if a tool template with this name already exists row = self._toolbox.tool_template_model.tool_template_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_template_model.tool_template(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 template '{}'".format(tool.name)) self._toolbox.update_tool_template(row, tool) else: # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QFileDialog.getSaveFileName(self, "Save Tool template 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_template(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 template definition file failed. Path:{0}".format(def_file)) return False return True
[docs] 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()
[docs] def closeEvent(self, event=None): """Handle close window. Args: event (QEvent): Closing event if 'X' is clicked. """ if event: event.accept()