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))
示例#2
0
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()
示例#4
0
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 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 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