Exemple #1
0
class FileWatcher(QObject):
    pathChanged = pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent)

        self._watcher = QFileSystemWatcher(self)
        self._watcher.directoryChanged.connect(self.onChanged)
        self._watcher.fileChanged.connect(self.onChanged)
        self._events = {}

    @property
    def watched(self):
        return self._watcher.directories() + self._watcher.files()

    def clear(self):
        self._watcher.removePaths(self.watched)

    def watch(self, path):
        self._watcher.addPath(path)

    def watchWalk(self, path: Path):
        try:
            for root, dirs, files in os.walk(str(path)):
                for dir in dirs:
                    r = Path(root).joinpath(dir)
                    self._watcher.addPath(str(r))
        except Exception:
            pass

    def onChanged(self, path):
        self.pathChanged.emit(path)
Exemple #2
0
class FileWatcher(QObject):
    def __init__(self, ui, path):
        QObject.__init__(self)
        self._watcher = QFileSystemWatcher()
        self.ui = ui
        self._path = path
        self.set_watcher_path(self._path)

    def enable(self):
        self._watcher.fileChanged.connect(self._onFileChanged)

    def disable(self):
        self._watcher.fileChanged.disconnect(self._onFileChanged)

    def set_watcher_path(self, path):
        if self._watcher.files():
            self._watcher.removePaths(self._watcher.files())
        if path is not None and os.path.isfile(path):
            self._watcher.addPath(path)
        self._path = path

    @pyqtSlot()
    def _onFileChanged(self):
        if os.path.exists(self._path):
            # Get last line
            line = self.last_insert(self._path).decode('utf-8')[:-1]

            # Split datetime and event output and raise an event
            line = line.split(' ', 1)
            event = Event(str(datetime.datetime.now()), line[1])
            self.addNotification(event)

    def last_insert(self, path):
        return subprocess.check_output(['tail', '-1', path])

    def addNotification(self, event):
        def eventColor(details):
            if details.startswith('Warning'):
                return QColor(192, 192, 192)
            elif details.startswith('Error'):
                return QColor(255, 204, 204)
            elif details.startswith('Notice'):
                return QColor(255, 204, 229)
            elif details.startswith('Emergency'):
                return QColor(255, 102, 102)
            elif details.startswith('Informational'):
                return QColor(255, 229, 204)
            else:
                return QColor(255, 255, 255)

        values = [event.datetime, event.title, event.filename, event.details]
        self.ui.notificationsTableWidget.insertRow(0)
        for i in range(self.ui.notificationsTableWidget.columnCount()):
            rowItem = QTableWidgetItem(values[i])
            rowItem.setBackground(eventColor(event.details))
            self.ui.notificationsTableWidget.setItem(0, i, rowItem)
Exemple #3
0
class FileWatcher(object):
    def __init__(self, files=None):
        self._watcher = QFileSystemWatcher()
        self._files = list()  # List of file(s) to watch
        self.isWatching = False
        if files is not None:
            self.addFile(files)

    def test(self):
        file = ['/Users/bitzer/hudat.spec']

        self.addFile(file)

    def addFile(self, files):
        # Add a file(s) to the watch list
        # Files needs to be a list, even if a single file

        # Do it while actively watching?

        for _f in files:
            print(_f)
            self._files.append(_f)

    def removeFile(self):
        # Remove a file
        pass

    def replaceFile(self, file):
        # In the (usual) case of a watching a single file, replace it
        if len(self._files) != 1:
            print('Only allowed if one file is currently watched')

        self._files[0] = file

    def startWatch(self):
        # Start watching the files

        self._watcher.addPaths(self._files)

        self._watcher.fileChanged.connect(self.onChange)
        self.isWatching = True

    def stopWatch(self):
        # Stop Watching the folder

        self._watcher.removePaths(self._files)

        self._watcher.fileChanged.disconnect(self.onChange)

        self.isWatching = False

    def onChange(self, file):
        # When a file changes, do something
        # Which file changed?
        print('changed ' + file)
Exemple #4
0
class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.setupUi(self)

        self.centralwidget.hide()

        self.project = ProjectManager()
        self.modules_manager = ModuleManager(self)

        self.setTabPosition(Qt.AllDockWidgetAreas, QTabWidget.North)

        self.modules_manager.init_modules([
            CompilerModule,
            LevelWidget,
            AssetViewWidget,
            AssetBrowser,
            LogWidget,
            ProfilerWidget,
            ScriptEditorManager
        ])

        self.file_watch = QFileSystemWatcher(self)
        self.file_watch.fileChanged.connect(self.file_changed)
        self.file_watch.directoryChanged.connect(self.dir_changed)

        self.build_file_watch = QFileSystemWatcher(self)
        self.build_file_watch.fileChanged.connect(self.build_file_changed)
        self.build_file_watch.directoryChanged.connect(self.build_dir_changed)

    def open_project(self, name, dir):
        self.project.open_project(name, dir)
        self.modules_manager.open_project(self.project)
        self.watch_project_dir()

    def reload_all(self):
        self.modules_manager['compiler'].compile_all()

        for k, v in self.project.instances.items():
            v.console_api.reload_all()

    def watch_project_dir(self):
        files = self.file_watch.files()
        directories = self.file_watch.directories()

        if len(files):
            self.file_watch.removePaths(files)

        if len(directories):
            self.file_watch.removePaths(directories)

        files = self.build_file_watch.files()
        directories = self.build_file_watch.directories()

        if len(files):
            self.build_file_watch.removePaths(files)

        if len(directories):
            self.build_file_watch.removePaths(directories)

        files = []
        it = QDirIterator(self.project.source_dir, QDirIterator.Subdirectories)
        while it.hasNext():
            files.append(it.next())

        self.file_watch.addPaths(files)

        files = []
        it = QDirIterator(self.project.build_dir, QDirIterator.Subdirectories)
        while it.hasNext():
            files.append(it.next())

        self.build_file_watch.addPaths(files)

    def file_changed(self, path):
        self.modules_manager['compiler'].compile_all()

    def dir_changed(self, path):
        self.watch_project_dir()

    def build_file_changed(self, path):
        pass

    def build_dir_changed(self, path):
        pass

    def open_script_editor(self):
        self.script_editor_dock_widget.show()

    def open_recorded_events(self):
        self.modules_manager['profiler'].dock.show()

    def run_standalone(self):
        self.project.run_release("Standalone")

    def run_level(self):
        self.project.run_develop("Level", compile_=True, continue_=True, port=5566)

    def closeEvent(self, evnt):
        self.modules_manager.close_project()
        self.project.killall()
        evnt.accept()
Exemple #5
0
class ExecuteOptionsPlugin(QWidget, Plugin):
    """
    Handles setting the various arguments for running.
    Signals:
        executableChanged(str): Path of the new executable is emitted when changed
        executableInfoChanged(ExecutableInfo): Emitted when the executable path is changed
        workingDirChanged(str): Path of the current directory is changed
    """
    executableChanged = pyqtSignal(str)
    executableInfoChanged = pyqtSignal(ExecutableInfo)
    workingDirChanged = pyqtSignal(str)

    def __init__(self, **kwds):
        super(ExecuteOptionsPlugin, self).__init__(**kwds)

        self._preferences.addInt("execute/maxRecentWorkingDirs",
                "Max recent working directories",
                10,
                1,
                50,
                "Set the maximum number of recent working directories that have been used.",
                )
        self._preferences.addInt("execute/maxRecentExes",
                "Max recent executables",
                10,
                1,
                50,
                "Set the maximum number of recent executables that have been used.",
                )
        self._preferences.addInt("execute/maxRecentArgs",
                "Max recent command line arguments",
                10,
                1,
                50,
                "Set the maximum number of recent command line arguments that have been used.",
                )
        self._preferences.addBool("execute/mpiEnabled",
                "Enable MPI by default",
                False,
                "Set the MPI checkbox on by default",
                )
        self._preferences.addString("execute/mpiArgs",
                "Default mpi command",
                "mpiexec -n 2",
                "Set the default MPI command to run",
                )
        self._preferences.addBool("execute/threadsEnabled",
                "Enable threads by default",
                False,
                "Set the threads checkbox on by default",
                )
        self._preferences.addString("execute/threadsArgs",
                "Default threads arguments",
                "--n-threads=2",
                "Set the default threads arguments",
                )

        self.all_exe_layout = WidgetUtils.addLayout(grid=True)
        self.setLayout(self.all_exe_layout)

        self.working_label = WidgetUtils.addLabel(None, self, "Working Directory")
        self.all_exe_layout.addWidget(self.working_label, 0, 0)
        self.choose_working_button = WidgetUtils.addButton(None, self, "Choose", self._chooseWorkingDir)
        self.all_exe_layout.addWidget(self.choose_working_button, 0, 1)
        self.working_line = WidgetUtils.addLineEdit(None, self, None, readonly=True)
        self.working_line.setText(os.getcwd())
        self.all_exe_layout.addWidget(self.working_line, 0, 2)

        self.exe_label = WidgetUtils.addLabel(None, self, "Executable")
        self.all_exe_layout.addWidget(self.exe_label, 1, 0)
        self.choose_exe_button = WidgetUtils.addButton(None, self, "Choose", self._chooseExecutable)
        self.all_exe_layout.addWidget(self.choose_exe_button, 1, 1)
        self.exe_line = WidgetUtils.addLineEdit(None, self, None, readonly=True)
        self.all_exe_layout.addWidget(self.exe_line, 1, 2)

        self.args_label = WidgetUtils.addLabel(None, self, "Extra Arguments")
        self.all_exe_layout.addWidget(self.args_label, 2, 0)
        self.args_line = WidgetUtils.addLineEdit(None, self, None)
        self.all_exe_layout.addWidget(self.args_line, 2, 2)

        self.mpi_label = WidgetUtils.addLabel(None, self, "Use MPI")
        self.all_exe_layout.addWidget(self.mpi_label, 3, 0)
        self.mpi_checkbox = WidgetUtils.addCheckbox(None, self, "", None)
        self.mpi_checkbox.setChecked(self._preferences.value("execute/mpiEnabled"))
        self.all_exe_layout.addWidget(self.mpi_checkbox, 3, 1, alignment=Qt.AlignHCenter)
        self.mpi_line = WidgetUtils.addLineEdit(None, self, None)
        self.mpi_line.setText(self._preferences.value("execute/mpiArgs"))
        self.mpi_line.cursorPositionChanged.connect(self._mpiLineCursorChanged)
        self.all_exe_layout.addWidget(self.mpi_line, 3, 2)

        self.threads_label = WidgetUtils.addLabel(None, self, "Use Threads")
        self.all_exe_layout.addWidget(self.threads_label, 4, 0)
        self.threads_checkbox = WidgetUtils.addCheckbox(None, self, "", None)
        self.threads_checkbox.setChecked(self._preferences.value("execute/threadsEnabled"))
        self.all_exe_layout.addWidget(self.threads_checkbox, 4, 1, alignment=Qt.AlignHCenter)
        self.threads_line = WidgetUtils.addLineEdit(None, self, None)
        self.threads_line.setText(self._preferences.value("execute/threadsArgs"))
        self.threads_line.cursorPositionChanged.connect(self._threadsLineCursorChanged)
        self.all_exe_layout.addWidget(self.threads_line, 4, 2)

        self.csv_label = WidgetUtils.addLabel(None, self, "Postprocessor CSV Output")
        self.all_exe_layout.addWidget(self.csv_label, 5, 0)
        self.csv_checkbox = WidgetUtils.addCheckbox(None, self, "", None)
        self.all_exe_layout.addWidget(self.csv_checkbox, 5, 1, alignment=Qt.AlignHCenter)
        self.csv_checkbox.setCheckState(Qt.Checked)

        self.recover_label = WidgetUtils.addLabel(None, self, "Recover")
        self.all_exe_layout.addWidget(self.recover_label, 6, 0)
        self.recover_checkbox = WidgetUtils.addCheckbox(None, self, "", None)
        self.all_exe_layout.addWidget(self.recover_checkbox, 6, 1, alignment=Qt.AlignHCenter)

        self._recent_exe_menu = None
        self._recent_working_menu = None
        self._recent_args_menu = None
        self._exe_watcher = QFileSystemWatcher()
        self._exe_watcher.fileChanged.connect(self.setExecutablePath)

        self._loading_dialog = QMessageBox(parent=self)
        self._loading_dialog.setWindowTitle("Loading executable")
        self._loading_dialog.setStandardButtons(QMessageBox.NoButton) # get rid of the OK button
        self._loading_dialog.setWindowModality(Qt.ApplicationModal)
        self._loading_dialog.setIcon(QMessageBox.Information)
        self._loading_dialog.setText("Loading executable")

        self.setup()

    def setExecutablePath(self, app_path):
        """
        The user select a new executable path.
        Input:
            app_path: The path of the executable.
        """
        if not app_path:
            return

        self._loading_dialog.setInformativeText(app_path)
        self._loading_dialog.show()
        self._loading_dialog.raise_()
        QApplication.processEvents()

        app_info = ExecutableInfo()
        app_info.setPath(app_path)

        QApplication.processEvents()

        if app_info.valid():
            self.exe_line.setText(app_path)
            self.executableInfoChanged.emit(app_info)
            self.executableChanged.emit(app_path)
            files = self._exe_watcher.files()
            if files:
                self._exe_watcher.removePaths(files)
            self._exe_watcher.addPath(app_path)
        self._updateRecentExe(app_path, not app_info.valid())
        self._loading_dialog.hide()

    def _chooseExecutable(self):
        """
        Open a dialog to allow the user to choose an executable.
        """
        #FIXME: QFileDialog seems to be a bit broken. Using
        # .setFilter() to filter only executable files doesn't
        # seem to work. Setting a QSortFilterProxyModel doesn't
        # seem to work either.
        # So just use the static method.
        exe_name, other = QFileDialog.getOpenFileName(self, "Chooose executable")
        self.setExecutablePath(exe_name)

    def _workingDirChanged(self):
        """
        Slot called when working directory changed.
        """
        working = str(self.working_line.text())
        self.setWorkingDir(working)

    def _chooseWorkingDir(self):
        """
        Open dialog to choose a current working directory.
        """
        dirname = QFileDialog.getExistingDirectory(self, "Choose directory")
        self.setWorkingDir(dirname)

    def setWorkingDir(self, dir_name):
        """
        Sets the working directory.
        Input:
            dir_name: The path of the working directory.
        """
        if not dir_name:
            return
        old_dirname = str(self.working_line.text())
        try:
            os.chdir(dir_name)
            self.working_line.setText(dir_name)
            if old_dirname != dir_name:
                self.workingDirChanged.emit(dir_name)
            self._updateRecentWorkingDir(dir_name)
        except OSError:
            mooseutils.mooseError("Invalid directory %s" % dir_name, dialog=True)
            self._updateRecentWorkingDir(dir_name, True)

    def _setExecutableArgs(self, args):
        """
        Set the executable arguments.
        Input:
            args: str: A string of all the arguments.
        """
        self.args_line.setText(args)

    def buildCommand(self, input_file):
        cmd, args = self.buildCommandWithNoInputFile()
        args.append("-i")
        args.append(os.path.relpath(input_file))
        return cmd, args

    def buildCommandWithNoInputFile(self):
        """
        Builds the full command line with arguments.
        Return: <string of command to run>, <list of arguments>
        """
        cmd = ""
        args = []
        if self.mpi_checkbox.isChecked():
            mpi_args = shlex.split(str(self.mpi_line.text()))
            if mpi_args:
                cmd = mpi_args[0]
                args = mpi_args[1:]
                args.append(str(self.exe_line.text()))

        if not cmd:
            cmd = str(self.exe_line.text())

        args += shlex.split(str(self.args_line.text()))

        if self.recover_checkbox.isChecked():
            args.append("--recover")

        if self.csv_checkbox.isChecked():
            #args.append("--no-color")
            args.append("Outputs/csv=true")

        if self.threads_checkbox.isChecked():
            args += shlex.split(str(self.threads_line.text()))

        return cmd, args

    def _updateRecentExe(self, path, remove=False):
        """
        Updates the recently used menu with the current executable
        """
        if self._recent_exe_menu:
            abs_path = os.path.normcase(os.path.abspath(path))
            if remove:
                self._recent_exe_menu.removeEntry(abs_path)
            else:
                self._recent_exe_menu.update(abs_path)

    def _updateRecentWorkingDir(self, path, remove=False):
        """
        Updates the recently used menu with the current executable
        """
        full_path = os.path.abspath(path)
        if self._recent_working_menu:
            if remove:
                self._recent_working_menu.removeEntry(full_path)
            else:
                self._recent_working_menu.update(full_path)

    def onPreferencesSaved(self):
        self._recent_args_menu.updateRecentlyOpened()
        self._recent_working_menu.updateRecentlyOpened()
        self._recent_exe_menu.updateRecentlyOpened()

    def addToMenu(self, menu):
        """
        Adds menu entries specific to the Arguments to the menubar.
        """
        workingMenu = menu.addMenu("Recent &working dirs")
        self._recent_working_menu = RecentlyUsedMenu(workingMenu,
                "execute/recentWorkingDirs",
                "execute/maxRecentWorkingDirs",
                20,
                )
        self._recent_working_menu.selected.connect(self.setWorkingDir)
        self._workingDirChanged()

        exeMenu = menu.addMenu("Recent &executables")
        self._recent_exe_menu = RecentlyUsedMenu(exeMenu,
                "execute/recentExes",
                "execute/maxRecentExes",
                20,
                )
        self._recent_exe_menu.selected.connect(self.setExecutablePath)

        argsMenu = menu.addMenu("Recent &arguments")
        self._recent_args_menu = RecentlyUsedMenu(argsMenu,
                "execute/recentArgs",
                "execute/maxRecentArgs",
                20,
                )
        self._recent_args_menu.selected.connect(self._setExecutableArgs)

    def clearRecentlyUsed(self):
        if self._recent_args_menu:
            self._recent_args_menu.clearValues()
            self._recent_working_menu.clearValues()
            self._recent_exe_menu.clearValues()
            self._workingDirChanged()

    def _mpiLineCursorChanged(self, old, new):
        self.mpi_checkbox.setChecked(True)

    def _threadsLineCursorChanged(self, old, new):
        self.threads_checkbox.setChecked(True)
class comics_project_manager_docker(DockWidget):
    setupDictionary = {}
    stringName = i18n("Comics Manager")
    projecturl = None
    pagesWatcher = None

    def __init__(self):
        super().__init__()
        self.setWindowTitle(self.stringName)

        # Setup layout:
        base = QHBoxLayout()
        widget = QWidget()
        widget.setLayout(base)
        baseLayout = QSplitter()
        base.addWidget(baseLayout)
        self.setWidget(widget)
        buttonLayout = QVBoxLayout()
        buttonBox = QWidget()
        buttonBox.setLayout(buttonLayout)
        baseLayout.addWidget(buttonBox)

        # Comic page list and pages model
        self.comicPageList = QListView()
        self.comicPageList.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.comicPageList.setDragEnabled(True)
        self.comicPageList.setDragDropMode(QAbstractItemView.InternalMove)
        self.comicPageList.setDefaultDropAction(Qt.MoveAction)
        self.comicPageList.setAcceptDrops(True)
        self.comicPageList.setItemDelegate(comic_page_delegate())
        self.pagesModel = QStandardItemModel()
        self.comicPageList.doubleClicked.connect(self.slot_open_page)
        self.comicPageList.setIconSize(QSize(128, 128))
        # self.comicPageList.itemDelegate().closeEditor.connect(self.slot_write_description)
        self.pagesModel.layoutChanged.connect(self.slot_write_config)
        self.pagesModel.rowsInserted.connect(self.slot_write_config)
        self.pagesModel.rowsRemoved.connect(self.slot_write_config)
        self.pagesModel.rowsMoved.connect(self.slot_write_config)
        self.comicPageList.setModel(self.pagesModel)
        pageBox = QWidget()
        pageBox.setLayout(QVBoxLayout())
        zoomSlider = QSlider(Qt.Horizontal, None)
        zoomSlider.setRange(1, 8)
        zoomSlider.setValue(4)
        zoomSlider.setTickInterval(1)
        zoomSlider.setMinimumWidth(10)
        zoomSlider.valueChanged.connect(self.slot_scale_thumbnails)
        self.projectName = Elided_Text_Label()
        pageBox.layout().addWidget(self.projectName)
        pageBox.layout().addWidget(zoomSlider)
        pageBox.layout().addWidget(self.comicPageList)
        baseLayout.addWidget(pageBox)

        self.btn_project = QToolButton()
        self.btn_project.setPopupMode(QToolButton.MenuButtonPopup)
        self.btn_project.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
        menu_project = QMenu()
        self.action_new_project = QAction(i18n("New Project"), self)
        self.action_new_project.triggered.connect(self.slot_new_project)
        self.action_load_project = QAction(i18n("Open Project"), self)
        self.action_load_project.triggered.connect(self.slot_open_config)
        menu_project.addAction(self.action_new_project)
        menu_project.addAction(self.action_load_project)
        self.btn_project.setMenu(menu_project)
        self.btn_project.setDefaultAction(self.action_load_project)
        buttonLayout.addWidget(self.btn_project)

        # Settings dropdown with actions for the different settings menus.
        self.btn_settings = QToolButton()
        self.btn_settings.setPopupMode(QToolButton.MenuButtonPopup)
        self.btn_settings.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
        self.action_edit_project_settings = QAction(i18n("Project Settings"), self)
        self.action_edit_project_settings.triggered.connect(self.slot_edit_project_settings)
        self.action_edit_meta_data = QAction(i18n("Meta Data"), self)
        self.action_edit_meta_data.triggered.connect(self.slot_edit_meta_data)
        self.action_edit_export_settings = QAction(i18n("Export Settings"), self)
        self.action_edit_export_settings.triggered.connect(self.slot_edit_export_settings)
        menu_settings = QMenu()
        menu_settings.addAction(self.action_edit_project_settings)
        menu_settings.addAction(self.action_edit_meta_data)
        menu_settings.addAction(self.action_edit_export_settings)
        self.btn_settings.setDefaultAction(self.action_edit_project_settings)
        self.btn_settings.setMenu(menu_settings)
        buttonLayout.addWidget(self.btn_settings)
        self.btn_settings.setDisabled(True)

        # Add page drop down with different page actions.
        self.btn_add_page = QToolButton()
        self.btn_add_page.setPopupMode(QToolButton.MenuButtonPopup)
        self.btn_add_page.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)

        self.action_add_page = QAction(i18n("Add Page"), self)
        self.action_add_page.triggered.connect(self.slot_add_new_page_single)
        self.action_add_template = QAction(i18n("Add Page from Template"), self)
        self.action_add_template.triggered.connect(self.slot_add_new_page_from_template)
        self.action_add_existing = QAction(i18n("Add Existing Pages"), self)
        self.action_add_existing.triggered.connect(self.slot_add_page_from_url)
        self.action_remove_selected_page = QAction(i18n("Remove Page"), self)
        self.action_remove_selected_page.triggered.connect(self.slot_remove_selected_page)
        self.action_resize_all_pages = QAction(i18n("Batch Resize"), self)
        self.action_resize_all_pages.triggered.connect(self.slot_batch_resize)
        self.btn_add_page.setDefaultAction(self.action_add_page)
        self.action_show_page_viewer = QAction(i18n("View Page In Window"), self)
        self.action_show_page_viewer.triggered.connect(self.slot_show_page_viewer)
        self.action_scrape_authors = QAction(i18n("Scrape Author Info"), self)
        self.action_scrape_authors.setToolTip(i18n("Search for author information in documents and add it to the author list. This does not check for duplicates."))
        self.action_scrape_authors.triggered.connect(self.slot_scrape_author_list)
        self.action_scrape_translations = QAction(i18n("Scrape Text for Translation"), self)
        self.action_scrape_translations.triggered.connect(self.slot_scrape_translations)
        actionList = []
        menu_page = QMenu()
        actionList.append(self.action_add_page)
        actionList.append(self.action_add_template)
        actionList.append(self.action_add_existing)
        actionList.append(self.action_remove_selected_page)
        actionList.append(self.action_resize_all_pages)
        actionList.append(self.action_show_page_viewer)
        actionList.append(self.action_scrape_authors)
        actionList.append(self.action_scrape_translations)
        menu_page.addActions(actionList)
        self.btn_add_page.setMenu(menu_page)
        buttonLayout.addWidget(self.btn_add_page)
        self.btn_add_page.setDisabled(True)

        self.comicPageList.setContextMenuPolicy(Qt.ActionsContextMenu)
        self.comicPageList.addActions(actionList)

        # Export button that... exports.
        self.btn_export = QPushButton(i18n("Export Comic"))
        self.btn_export.clicked.connect(self.slot_export)
        buttonLayout.addWidget(self.btn_export)
        self.btn_export.setDisabled(True)

        self.btn_project_url = QPushButton(i18n("Copy Location"))
        self.btn_project_url.setToolTip(i18n("Copies the path of the project to the clipboard. Useful for quickly copying to a file manager or the like."))
        self.btn_project_url.clicked.connect(self.slot_copy_project_url)
        self.btn_project_url.setDisabled(True)
        buttonLayout.addWidget(self.btn_project_url)

        self.page_viewer_dialog = comics_project_page_viewer.comics_project_page_viewer()
        
        self.pagesWatcher = QFileSystemWatcher()
        self.pagesWatcher.fileChanged.connect(self.slot_check_for_page_update)

        buttonLayout.addItem(QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.MinimumExpanding))

    """
    Open the config file and load the json file into a dictionary.
    """

    def slot_open_config(self):
        self.path_to_config = QFileDialog.getOpenFileName(caption=i18n("Please select the JSON comic config file."), filter=str(i18n("JSON files") + "(*.json)"))[0]
        if os.path.exists(self.path_to_config) is True:
            if os.access(self.path_to_config, os.W_OK) is False:
                QMessageBox.warning(None, i18n("Config cannot be used"), i18n("Krita doesn't have write access to this folder, so new files cannot be made. Please configure the folder access or move the project to a folder that can be written to."), QMessageBox.Ok)
                return
            configFile = open(self.path_to_config, "r", newline="", encoding="utf-16")
            self.setupDictionary = json.load(configFile)
            self.projecturl = os.path.dirname(str(self.path_to_config))
            configFile.close()
            self.load_config()
    """
    Further config loading.
    """

    def load_config(self):
        self.projectName.setMainText(text=str(self.setupDictionary["projectName"]))
        self.fill_pages()
        self.btn_settings.setEnabled(True)
        self.btn_add_page.setEnabled(True)
        self.btn_export.setEnabled(True)
        self.btn_project_url.setEnabled(True)

    """
    Fill the pages model with the pages from the pages list.
    """

    def fill_pages(self):
        self.loadingPages = True
        self.pagesModel.clear()
        if len(self.pagesWatcher.files())>0:
            self.pagesWatcher.removePaths(self.pagesWatcher.files())
        pagesList = []
        if "pages" in self.setupDictionary.keys():
            pagesList = self.setupDictionary["pages"]
        progress = QProgressDialog()
        progress.setMinimum(0)
        progress.setMaximum(len(pagesList))
        progress.setWindowTitle(i18n("Loading Pages..."))
        for url in pagesList:
            absurl = os.path.join(self.projecturl, url)
            relative = os.path.relpath(absurl, self.projecturl)
            if (os.path.exists(absurl)):
                #page = Application.openDocument(absurl)
                page = zipfile.ZipFile(absurl, "r")
                thumbnail = QImage.fromData(page.read("preview.png"))
                pageItem = QStandardItem()
                dataList = self.get_description_and_title(page.read("documentinfo.xml"))
                if (dataList[0].isspace() or len(dataList[0]) < 1):
                    dataList[0] = os.path.basename(url)
                pageItem.setText(dataList[0].replace("_", " "))
                pageItem.setDragEnabled(True)
                pageItem.setDropEnabled(False)
                pageItem.setEditable(False)
                pageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail)))
                pageItem.setData(dataList[1], role = CPE.DESCRIPTION)
                pageItem.setData(relative, role = CPE.URL)
                self.pagesWatcher.addPath(absurl)
                pageItem.setData(dataList[2], role = CPE.KEYWORDS)
                pageItem.setData(dataList[3], role = CPE.LASTEDIT)
                pageItem.setData(dataList[4], role = CPE.EDITOR)
                pageItem.setToolTip(relative)
                page.close()
                self.pagesModel.appendRow(pageItem)
                progress.setValue(progress.value() + 1)
        progress.setValue(len(pagesList))
        self.loadingPages = False
    """
    Function that is triggered by the zoomSlider
    Resizes the thumbnails.
    """

    def slot_scale_thumbnails(self, multiplier=4):
        self.comicPageList.setIconSize(QSize(multiplier * 32, multiplier * 32))

    """
    Function that takes the documentinfo.xml and parses it for the title, subject and abstract tags,
    to get the title and description.
    
    @returns a stringlist with the name on 0 and the description on 1.
    """

    def get_description_and_title(self, string):
        xmlDoc = ET.fromstring(string)
        calligra = str("{http://www.calligra.org/DTD/document-info}")
        name = ""
        if ET.iselement(xmlDoc[0].find(calligra + 'title')):
            name = xmlDoc[0].find(calligra + 'title').text
            if name is None:
                name = " "
        desc = ""
        if ET.iselement(xmlDoc[0].find(calligra + 'subject')):
            desc = xmlDoc[0].find(calligra + 'subject').text
        if desc is None or desc.isspace() or len(desc) < 1:
            if ET.iselement(xmlDoc[0].find(calligra + 'abstract')):
                desc = xmlDoc[0].find(calligra + 'abstract').text
                if desc is not None:
                    if desc.startswith("<![CDATA["):
                        desc = desc[len("<![CDATA["):]
                    if desc.startswith("]]>"):
                        desc = desc[:-len("]]>")]
        keywords = ""
        if ET.iselement(xmlDoc[0].find(calligra + 'keyword')):
            keywords = xmlDoc[0].find(calligra + 'keyword').text
        date = ""
        if ET.iselement(xmlDoc[0].find(calligra + 'date')):
            date = xmlDoc[0].find(calligra + 'date').text
        author = []
        if ET.iselement(xmlDoc[1].find(calligra + 'creator-first-name')):
            string = xmlDoc[1].find(calligra + 'creator-first-name').text
            if string is not None:
                author.append(string)
        if ET.iselement(xmlDoc[1].find(calligra + 'creator-last-name')):
            string = xmlDoc[1].find(calligra + 'creator-last-name').text
            if string is not None:
                author.append(string)
        if ET.iselement(xmlDoc[1].find(calligra + 'full-name')):
            string = xmlDoc[1].find(calligra + 'full-name').text
            if string is not None:
                author.append(string)
            
        return [name, desc, keywords, date, " ".join(author)]

    """
    Scrapes authors from the author data in the document info and puts them into the author list.
    Doesn't check for duplicates.
    """

    def slot_scrape_author_list(self):
        listOfAuthors = []
        if "authorList" in self.setupDictionary.keys():
            listOfAuthors = self.setupDictionary["authorList"]
        if "pages" in self.setupDictionary.keys():
            for relurl in self.setupDictionary["pages"]:
                absurl = os.path.join(self.projecturl, relurl)
                page = zipfile.ZipFile(absurl, "r")
                xmlDoc = ET.fromstring(page.read("documentinfo.xml"))
                calligra = str("{http://www.calligra.org/DTD/document-info}")
                authorelem = xmlDoc.find(calligra + 'author')
                author = {}
                if ET.iselement(authorelem.find(calligra + 'full-name')):
                    author["nickname"] = str(authorelem.find(calligra + 'full-name').text)

                if ET.iselement(authorelem.find(calligra + 'creator-first-name')):
                    author["first-name"] = str(authorelem.find(calligra + 'creator-first-name').text)

                if ET.iselement(authorelem.find(calligra + 'initial')):
                    author["initials"] = str(authorelem.find(calligra + 'initial').text)

                if ET.iselement(authorelem.find(calligra + 'creator-last-name')):
                    author["last-name"] = str(authorelem.find(calligra + 'creator-last-name').text)

                if ET.iselement(authorelem.find(calligra + 'email')):
                    author["email"] = str(authorelem.find(calligra + 'email').text)

                if ET.iselement(authorelem.find(calligra + 'contact')):
                    contact = authorelem.find(calligra + 'contact')
                    contactMode = contact.get("type")
                    if contactMode == "email":
                        author["email"] = str(contact.text)
                    if contactMode == "homepage":
                        author["homepage"] = str(contact.text)

                if ET.iselement(authorelem.find(calligra + 'position')):
                    author["role"] = str(authorelem.find(calligra + 'position').text)
                listOfAuthors.append(author)
                page.close()
        self.setupDictionary["authorList"] = listOfAuthors

    """
    Edit the general project settings like the project name, concept, pages location, export location, template location, metadata
    """

    def slot_edit_project_settings(self):
        dialog = comics_project_settings_dialog.comics_project_details_editor(self.projecturl)
        dialog.setConfig(self.setupDictionary, self.projecturl)

        if dialog.exec_() == QDialog.Accepted:
            self.setupDictionary = dialog.getConfig(self.setupDictionary)
            self.slot_write_config()
            self.projectName.setMainText(str(self.setupDictionary["projectName"]))

    """
    This allows users to select existing pages and add them to the pages list. The pages are currently not copied to the pages folder. Useful for existing projects.
    """

    def slot_add_page_from_url(self):
        # get the pages.
        urlList = QFileDialog.getOpenFileNames(caption=i18n("Which existing pages to add?"), directory=self.projecturl, filter=str(i18n("Krita files") + "(*.kra)"))[0]

        # get the existing pages list.
        pagesList = []
        if "pages" in self.setupDictionary.keys():
            pagesList = self.setupDictionary["pages"]

        # And add each url in the url list to the pages list and the model.
        for url in urlList:
            if self.projecturl not in urlList:
                newUrl = os.path.join(self.projecturl, self.setupDictionary["pagesLocation"], os.path.basename(url))
                shutil.move(url, newUrl)
                url = newUrl
            relative = os.path.relpath(url, self.projecturl)
            if url not in pagesList:
                page = zipfile.ZipFile(url, "r")
                thumbnail = QImage.fromData(page.read("preview.png"))
                dataList = self.get_description_and_title(page.read("documentinfo.xml"))
                if (dataList[0].isspace() or len(dataList[0]) < 1):
                    dataList[0] = os.path.basename(url)
                newPageItem = QStandardItem()
                newPageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail)))
                newPageItem.setDragEnabled(True)
                newPageItem.setDropEnabled(False)
                newPageItem.setEditable(False)
                newPageItem.setText(dataList[0].replace("_", " "))
                newPageItem.setData(dataList[1], role = CPE.DESCRIPTION)
                newPageItem.setData(relative, role = CPE.URL)
                self.pagesWatcher.addPath(url)
                newPageItem.setData(dataList[2], role = CPE.KEYWORDS)
                newPageItem.setData(dataList[3], role = CPE.LASTEDIT)
                newPageItem.setData(dataList[4], role = CPE.EDITOR)
                newPageItem.setToolTip(relative)
                page.close()
                self.pagesModel.appendRow(newPageItem)

    """
    Remove the selected page from the list of pages. This does not remove it from disk(far too dangerous).
    """

    def slot_remove_selected_page(self):
        index = self.comicPageList.currentIndex()
        self.pagesModel.removeRow(index.row())

    """
    This function adds a new page from the default template. If there's no default template, or the file does not exist, it will 
    show the create/import template dialog. It will remember the selected item as the default template.
    """

    def slot_add_new_page_single(self):
        templateUrl = "templatepage"
        templateExists = False

        if "singlePageTemplate" in self.setupDictionary.keys():
            templateUrl = self.setupDictionary["singlePageTemplate"]
        if os.path.exists(os.path.join(self.projecturl, templateUrl)):
            templateExists = True

        if templateExists is False:
            if "templateLocation" not in self.setupDictionary.keys():
                self.setupDictionary["templateLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where are the templates located?"), options=QFileDialog.ShowDirsOnly), self.projecturl)

            templateDir = os.path.join(self.projecturl, self.setupDictionary["templateLocation"])
            template = comics_template_dialog.comics_template_dialog(templateDir)

            if template.exec_() == QDialog.Accepted:
                templateUrl = os.path.relpath(template.url(), self.projecturl)
                self.setupDictionary["singlePageTemplate"] = templateUrl
        if os.path.exists(os.path.join(self.projecturl, templateUrl)):
            self.add_new_page(templateUrl)

    """
    This function always asks for a template showing the new template window. This allows users to have multiple different
    templates created for back covers, spreads, other and have them accessible, while still having the convenience of a singular
    "add page" that adds a default.
    """

    def slot_add_new_page_from_template(self):
        if "templateLocation" not in self.setupDictionary.keys():
            self.setupDictionary["templateLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where are the templates located?"), options=QFileDialog.ShowDirsOnly), self.projecturl)

        templateDir = os.path.join(self.projecturl, self.setupDictionary["templateLocation"])
        template = comics_template_dialog.comics_template_dialog(templateDir)

        if template.exec_() == QDialog.Accepted:
            templateUrl = os.path.relpath(template.url(), self.projecturl)
            self.add_new_page(templateUrl)

    """
    This is the actual function that adds the template using the template url.
    It will attempt to name the new page projectName+number.
    """

    def add_new_page(self, templateUrl):

        # check for page list and or location.
        pagesList = []
        if "pages" in self.setupDictionary.keys():
            pagesList = self.setupDictionary["pages"]
        if not "pageNumber" in self.setupDictionary.keys():
            self.setupDictionary['pageNumber'] = 0

        if (str(self.setupDictionary["pagesLocation"]).isspace()):
            self.setupDictionary["pagesLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where should the pages go?"), options=QFileDialog.ShowDirsOnly), self.projecturl)

        # Search for the possible name.
        extraUnderscore = str()
        if str(self.setupDictionary["projectName"])[-1].isdigit():
            extraUnderscore = "_"
        self.setupDictionary['pageNumber'] += 1
        pageName = str(self.setupDictionary["projectName"]).replace(" ", "_") + extraUnderscore + str(format(self.setupDictionary['pageNumber'], "03d"))
        url = os.path.join(str(self.setupDictionary["pagesLocation"]), pageName + ".kra")

        # open the page by opening the template and resaving it, or just opening it.
        absoluteUrl = os.path.join(self.projecturl, url)
        if (os.path.exists(absoluteUrl)):
            newPage = Application.openDocument(absoluteUrl)
        else:
            booltemplateExists = os.path.exists(os.path.join(self.projecturl, templateUrl))
            if booltemplateExists is False:
                templateUrl = os.path.relpath(QFileDialog.getOpenFileName(caption=i18n("Which image should be the basis the new page?"), directory=self.projecturl, filter=str(i18n("Krita files") + "(*.kra)"))[0], self.projecturl)
            newPage = Application.openDocument(os.path.join(self.projecturl, templateUrl))
            newPage.waitForDone()
            newPage.setFileName(absoluteUrl)
            newPage.setName(pageName.replace("_", " "))
            newPage.save()
            newPage.waitForDone()

        # Get out the extra data for the standard item.
        newPageItem = QStandardItem()
        newPageItem.setIcon(QIcon(QPixmap.fromImage(newPage.thumbnail(256, 256))))
        newPageItem.setDragEnabled(True)
        newPageItem.setDropEnabled(False)
        newPageItem.setEditable(False)
        newPageItem.setText(pageName.replace("_", " "))
        newPageItem.setData("", role = CPE.DESCRIPTION)
        newPageItem.setData(url, role = CPE.URL)
        newPageItem.setData("", role = CPE.KEYWORDS)
        newPageItem.setData("", role = CPE.LASTEDIT)
        newPageItem.setData("", role = CPE.EDITOR)
        newPageItem.setToolTip(url)

        # close page document.
        while os.path.exists(absoluteUrl) is False:
            qApp.processEvents()

        self.pagesWatcher.addPath(absoluteUrl)
        newPage.close()

        # add item to page.
        self.pagesModel.appendRow(newPageItem)

    """
    Write to the json configuration file.
    This also checks the current state of the pages list.
    """

    def slot_write_config(self):

        # Don't load when the pages are still being loaded, otherwise we'll be overwriting our own pages list.
        if (self.loadingPages is False):
            print("CPMT: writing comic configuration...")

            # Generate a pages list from the pagesmodel.
            pagesList = []
            for i in range(self.pagesModel.rowCount()):
                index = self.pagesModel.index(i, 0)
                url = str(self.pagesModel.data(index, role=CPE.URL))
                if url not in pagesList:
                    pagesList.append(url)
            self.setupDictionary["pages"] = pagesList

            # Save to our json file.
            configFile = open(self.path_to_config, "w", newline="", encoding="utf-16")
            json.dump(self.setupDictionary, configFile, indent=4, sort_keys=True, ensure_ascii=False)
            configFile.close()
            print("CPMT: done")

    """
    Open a page in the pagesmodel in Krita.
    """

    def slot_open_page(self, index):
        if index.column() is 0:
            # Get the absolute url from the relative one in the pages model.
            absoluteUrl = os.path.join(self.projecturl, str(self.pagesModel.data(index, role=CPE.URL)))

            # Make sure the page exists.
            if os.path.exists(absoluteUrl):
                page = Application.openDocument(absoluteUrl)

                # Set the title to the filename if it was empty. It looks a bit neater.
                if page.name().isspace or len(page.name()) < 1:
                    page.setName(str(self.pagesModel.data(index, role=Qt.DisplayRole)).replace("_", " "))

                # Add views for the document so the user can use it.
                Application.activeWindow().addView(page)
                Application.setActiveDocument(page)
            else:
                print("CPMT: The page cannot be opened because the file doesn't exist:", absoluteUrl)

    """
    Call up the metadata editor dialog. Only when the dialog is "Accepted" will the metadata be saved.
    """

    def slot_edit_meta_data(self):
        dialog = comics_metadata_dialog.comic_meta_data_editor()

        dialog.setConfig(self.setupDictionary)
        if (dialog.exec_() == QDialog.Accepted):
            self.setupDictionary = dialog.getConfig(self.setupDictionary)
            self.slot_write_config()

    """
    An attempt at making the description editable from the comic pages list.
    It is currently not working because ZipFile has no overwrite mechanism,
    and I don't have the energy to write one yet.
    """

    def slot_write_description(self, index):

        for row in range(self.pagesModel.rowCount()):
            index = self.pagesModel.index(row, 1)
            indexUrl = self.pagesModel.index(row, 0)
            absoluteUrl = os.path.join(self.projecturl, str(self.pagesModel.data(indexUrl, role=CPE.URL)))
            page = zipfile.ZipFile(absoluteUrl, "a")
            xmlDoc = ET.ElementTree()
            ET.register_namespace("", "http://www.calligra.org/DTD/document-info")
            location = os.path.join(self.projecturl, "documentinfo.xml")
            xmlDoc.parse(location)
            xmlroot = ET.fromstring(page.read("documentinfo.xml"))
            calligra = "{http://www.calligra.org/DTD/document-info}"
            aboutelem = xmlroot.find(calligra + 'about')
            if ET.iselement(aboutelem.find(calligra + 'subject')):
                desc = aboutelem.find(calligra + 'subject')
                desc.text = self.pagesModel.data(index, role=Qt.EditRole)
                xmlstring = ET.tostring(xmlroot, encoding='unicode', method='xml', short_empty_elements=False)
                page.writestr(zinfo_or_arcname="documentinfo.xml", data=xmlstring)
                for document in Application.documents():
                    if str(document.fileName()) == str(absoluteUrl):
                        document.setDocumentInfo(xmlstring)
            page.close()

    """
    Calls up the export settings dialog. Only when accepted will the configuration be written.
    """

    def slot_edit_export_settings(self):
        dialog = comics_export_dialog.comic_export_setting_dialog()
        dialog.setConfig(self.setupDictionary)

        if (dialog.exec_() == QDialog.Accepted):
            self.setupDictionary = dialog.getConfig(self.setupDictionary)
            self.slot_write_config()

    """
    Export the comic. Won't work without export settings set.
    """

    def slot_export(self):
        
        #ensure there is a unique identifier
        if "uuid" not in self.setupDictionary.keys():
            uuid = str()
            if "acbfID" in self.setupDictionary.keys():
                uuid = str(self.setupDictionary["acbfID"])
            else:
                uuid = QUuid.createUuid().toString()
            self.setupDictionary["uuid"] = uuid
        
        exporter = comics_exporter.comicsExporter()
        exporter.set_config(self.setupDictionary, self.projecturl)
        exportSuccess = exporter.export()
        if exportSuccess:
            print("CPMT: Export success! The files have been written to the export folder!")
            QMessageBox.information(self, i18n("Export success"), i18n("The files have been written to the export folder."), QMessageBox.Ok)

    """
    Calls up the comics project setup wizard so users can create a new json file with the basic information.
    """

    def slot_new_project(self):
        setup = comics_project_setup_wizard.ComicsProjectSetupWizard()
        setup.showDialog()
        self.path_to_config = os.path.join(setup.projectDirectory, "comicConfig.json")
        if os.path.exists(self.path_to_config) is True:
            configFile = open(self.path_to_config, "r", newline="", encoding="utf-16")
            self.setupDictionary = json.load(configFile)
            self.projecturl = os.path.dirname(str(self.path_to_config))
            configFile.close()
            self.load_config()
    """
    This is triggered by any document save.
    It checks if the given url in in the pages list, and if so,
    updates the appropriate page thumbnail.
    This helps with the management of the pages, because the user
    will be able to see the thumbnails as a todo for the whole comic,
    giving a good overview over whether they still need to ink, color or
    the like for a given page, and it thus also rewards the user whenever
    they save.
    """

    def slot_check_for_page_update(self, url):
        if "pages" in self.setupDictionary.keys():
            relUrl = os.path.relpath(url, self.projecturl)
            if relUrl in self.setupDictionary["pages"]:
                index = self.pagesModel.index(self.setupDictionary["pages"].index(relUrl), 0)
                if index.isValid():
                    if os.path.exists(url) is False:
                        # we cannot check from here whether the file in question has been renamed or deleted.
                        self.pagesModel.removeRow(index.row())
                        return
                    else:
                        # Krita will trigger the filesystemwatcher when doing backupfiles,
                        # so ensure the file is still watched if it exists.
                        self.pagesWatcher.addPath(url)
                    pageItem = self.pagesModel.itemFromIndex(index)
                    page = zipfile.ZipFile(url, "r")
                    dataList = self.get_description_and_title(page.read("documentinfo.xml"))
                    if (dataList[0].isspace() or len(dataList[0]) < 1):
                        dataList[0] = os.path.basename(url)
                    thumbnail = QImage.fromData(page.read("preview.png"))
                    pageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail)))
                    pageItem.setText(dataList[0])
                    pageItem.setData(dataList[1], role = CPE.DESCRIPTION)
                    pageItem.setData(relUrl, role = CPE.URL)
                    pageItem.setData(dataList[2], role = CPE.KEYWORDS)
                    pageItem.setData(dataList[3], role = CPE.LASTEDIT)
                    pageItem.setData(dataList[4], role = CPE.EDITOR)
                    self.pagesModel.setItem(index.row(), index.column(), pageItem)

    """
    Resize all the pages in the pages list.
    It will show a dialog with the options for resizing.
    Then, it will try to pop up a progress dialog while resizing.
    The progress dialog shows the remaining time and pages.
    """

    def slot_batch_resize(self):
        dialog = QDialog()
        dialog.setWindowTitle(i18n("Resize all Pages"))
        buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        buttons.accepted.connect(dialog.accept)
        buttons.rejected.connect(dialog.reject)
        sizesBox = comics_export_dialog.comic_export_resize_widget("Scale", batch=True, fileType=False)
        exporterSizes = comics_exporter.sizesCalculator()
        dialog.setLayout(QVBoxLayout())
        dialog.layout().addWidget(sizesBox)
        dialog.layout().addWidget(buttons)

        if dialog.exec_() == QDialog.Accepted:
            progress = QProgressDialog(i18n("Resizing pages..."), str(), 0, len(self.setupDictionary["pages"]))
            progress.setWindowTitle(i18n("Resizing Pages"))
            progress.setCancelButton(None)
            timer = QElapsedTimer()
            timer.start()
            config = {}
            config = sizesBox.get_config(config)
            for p in range(len(self.setupDictionary["pages"])):
                absoluteUrl = os.path.join(self.projecturl, self.setupDictionary["pages"][p])
                progress.setValue(p)
                timePassed = timer.elapsed()
                if (p > 0):
                    timeEstimated = (len(self.setupDictionary["pages"]) - p) * (timePassed / p)
                    passedString = str(int(timePassed / 60000)) + ":" + format(int(timePassed / 1000), "02d") + ":" + format(timePassed % 1000, "03d")
                    estimatedString = str(int(timeEstimated / 60000)) + ":" + format(int(timeEstimated / 1000), "02d") + ":" + format(int(timeEstimated % 1000), "03d")
                    progress.setLabelText(str(i18n("{pages} of {pagesTotal} done. \nTime passed: {passedString}:\n Estimated:{estimated}")).format(pages=p, pagesTotal=len(self.setupDictionary["pages"]), passedString=passedString, estimated=estimatedString))
                    qApp.processEvents()
                if os.path.exists(absoluteUrl):
                    doc = Application.openDocument(absoluteUrl)
                    listScales = exporterSizes.get_scale_from_resize_config(config["Scale"], [doc.width(), doc.height(), doc.resolution(), doc.resolution()])
                    doc.scaleImage(listScales[0], listScales[1], listScales[2], listScales[3], "bicubic")
                    doc.waitForDone()
                    doc.save()
                    doc.waitForDone()
                    doc.close()

    def slot_show_page_viewer(self):
        index = int(self.comicPageList.currentIndex().row())
        self.page_viewer_dialog.load_comic(self.path_to_config)
        self.page_viewer_dialog.go_to_page_index(index)
        self.page_viewer_dialog.show()

    """
    Function to copy the current project location into the clipboard.
    This is useful for users because they'll be able to use that url to quickly
    move to the project location in outside applications.
    """

    def slot_copy_project_url(self):
        if self.projecturl is not None:
            clipboard = qApp.clipboard()
            clipboard.setText(str(self.projecturl))

    """
    Scrape text files with the textlayer keys for text, and put those in a POT
    file. This makes it possible to handle translations.
    """

    def slot_scrape_translations(self):
        translationFolder = self.setupDictionary.get("translationLocation", "translations")
        fullTranslationPath = os.path.join(self.projecturl, translationFolder)
        os.makedirs(fullTranslationPath, exist_ok=True)
        textLayersToSearch = self.setupDictionary.get("textLayerNames", ["text"])

        scraper = comics_project_translation_scraper.translation_scraper(self.projecturl, translationFolder, textLayersToSearch, self.setupDictionary["projectName"])
        # Run text scraper.
        language = self.setupDictionary.get("language", "en")
        metadata = {}
        metadata["title"] = self.setupDictionary.get("title", "")
        metadata["summary"] = self.setupDictionary.get("summary", "")
        metadata["keywords"] = ", ".join(self.setupDictionary.get("otherKeywords", [""]))
        metadata["transnotes"] = self.setupDictionary.get("translatorHeader", "Translator's Notes")
        scraper.start(self.setupDictionary["pages"], language, metadata)
        QMessageBox.information(self, i18n("Scraping success"), str(i18n("POT file has been written to: {file}")).format(file=fullTranslationPath), QMessageBox.Ok)
    """
    This is required by the dockwidget class, otherwise unused.
    """

    def canvasChanged(self, canvas):
        pass
Exemple #7
0
class Data_Dialog(QDialog):
    def __init__(self,
                 fname=None,
                 data=None,
                 comment='#',
                 skiprows=0,
                 delimiter=' ',
                 expressions={},
                 autoupdate=False,
                 parent=None,
                 matplotlib=False,
                 plotIndex=None,
                 colors=None):
        QDialog.__init__(self, parent=parent)
        loadUi('UI_Forms/Data_Dialog.ui', self)
        self.colcycler = cycle(['r', 'g', 'b', 'c', 'm', 'y', 'w'])
        self.plotWidget = PlotWidget(parent=self, matplotlib=matplotlib)
        self.plotTab = self.tabWidget.addTab(self.plotWidget, 'Plots')
        self.tabWidget.setCurrentIndex(0)
        self.show()
        self.fileWatcher = QFileSystemWatcher()
        self.fileWatcher.fileChanged.connect(self.fileUpdated)
        self.cwd = None
        self.plotNum = 0
        self.xlabel = []
        self.ylabel = []
        self.oldPlotIndex = {}
        self.oldColors = {}
        self.dataAltered = False
        self.expressions = expressions
        if data is not None:
            self.data = data
            self.autoUpdateCheckBox.setEnabled(False)
        elif fname is not None:
            self.data = self.readData(fname,
                                      comment=comment,
                                      skiprows=skiprows,
                                      delimiter=delimiter)
        else:
            self.data = None
            self.autoUpdateCheckBox.setEnabled(False)
            self.saveDataPushButton.setEnabled(False)
            self.addRowPushButton.setEnabled(False)
            self.removeRowsPushButton.setEnabled(False)
            self.removeColumnPushButton.setEnabled(False)
        if self.data is not None:
            self.setMeta2Table()
            self.setData2Table()
            if plotIndex is None:
                self.addPlots(color=None)
            else:
                self.addMultiPlots(plotIndex=plotIndex, colors=colors)
        self.init_signals()
        self.okPushButton.setAutoDefault(False)
        self.make_default()
        self.setWindowTitle('Data Dialog')
        self.acceptData = True

        #self.setWindowSize((600,400))
        # if self.parentWidget() is not None:
        #     self.addPlotPushButton.setEnabled(False)
        #     self.removePlotPushButton.setEnabled(False)

    def make_default(self):
        self.okPushButton.setAutoDefault(False)
        self.closePushButton.setAutoDefault(False)
        self.openDataFilePushButton.setAutoDefault(False)
        self.saveDataPushButton.setAutoDefault(False)
        self.okPushButton.setDefault(False)
        self.closePushButton.setDefault(False)
        self.openDataFilePushButton.setDefault(False)
        self.saveDataPushButton.setDefault(False)

    def init_signals(self):
        self.closePushButton.clicked.connect(self.closeWidget)
        self.okPushButton.clicked.connect(self.acceptWidget)
        self.openDataFilePushButton.clicked.connect(self.openFile)
        self.autoUpdateCheckBox.stateChanged.connect(self.autoUpdate_ON_OFF)
        self.saveDataPushButton.clicked.connect(self.saveData)
        self.addPlotPushButton.clicked.connect(
            lambda x: self.addPlots(plotIndex=None))
        self.plotSetupTableWidget.cellChanged.connect(self.updatePlotData)
        self.removePlotPushButton.clicked.connect(self.removePlots)

        self.addMetaDataPushButton.clicked.connect(self.addMetaData)
        self.metaDataTableWidget.itemChanged.connect(self.metaDataChanged)
        self.metaDataTableWidget.itemClicked.connect(self.metaDataClicked)
        self.metaDataTableWidget.itemSelectionChanged.connect(
            self.metaDataSelectionChanged)
        self.removeMetaDataPushButton.clicked.connect(self.removeMetaData)

        self.dataTableWidget.itemChanged.connect(self.dataChanged)
        self.editColumnPushButton.clicked.connect(self.editDataColumn)
        self.addColumnPushButton.clicked.connect(
            lambda x: self.addDataColumn(colName='Col_X'))
        self.removeColumnPushButton.clicked.connect(self.removeDataColumn)
        self.removeRowsPushButton.clicked.connect(self.removeDataRows)
        self.dataTableWidget.setSelection
        self.dataTableWidget.horizontalHeader().sortIndicatorChanged.connect(
            self.dataSorted)
        self.addRowPushButton.clicked.connect(self.addDataRow)

    def closeWidget(self):
        self.acceptData = False
        self.reject()

    def acceptWidget(self):
        self.acceptData = True
        self.accept()

    def addMetaData(self):
        """
        Opens a MetaData Dialog and by accepting the dialog inputs the data to the MetaDataTable
        """

        self.metaDialog = MetaData_Dialog()
        if self.metaDialog.exec_():
            name, value = self.metaDialog.parNameLineEdit.text(
            ), self.metaDialog.parValueLineEdit.text()
            if name not in self.data['meta'].keys():
                row = self.metaDataTableWidget.rowCount()
                self.metaDataTableWidget.insertRow(row)
                self.metaDataTableWidget.setItem(row, 0,
                                                 QTableWidgetItem(name))
                self.metaDataTableWidget.setItem(row, 1,
                                                 QTableWidgetItem(value))
                try:
                    self.data['meta'][name] = eval(value)
                except:
                    self.data['meta'][name] = value
            else:
                QMessageBox.warning(
                    self, "Parameter Exists",
                    "The parameter %s already exists in meta data. Please provide a different parameter name"
                    % name, QMessageBox.Ok)
                self.addMetaData()

    def removeMetaData(self):
        """
        Removes the selected Metadata from the table
        """
        self.metaDataTableWidget.itemSelectionChanged.disconnect()
        rows = list(
            set([
                item.row()
                for item in self.metaDataTableWidget.selectedItems()
            ]))
        for row in rows:
            key = self.metaDataTableWidget.item(row, 0).text()
            if key != 'col_names':
                del self.data['meta'][key]
                self.metaDataTableWidget.removeRow(row)
            else:
                QMessageBox.warning(self, 'Restricted Parameter',
                                    'You cannot delete the parameter %s' % key,
                                    QMessageBox.Ok)
        self.metaDataTableWidget.itemSelectionChanged.connect(
            self.metaDataSelectionChanged)

    def metaDataChanged(self, item):
        """
        Updates the value metadata as per the changes in the metaDataTableWidget
        """
        row = item.row()
        col = item.column()
        key = self.metaDataTableWidget.item(row, 0).text()
        if col != 0:
            try:
                self.data['meta'][key] = eval(item.text())
            except:
                self.data['meta'][key] = item.text()
            if self.metaDataTableWidget.item(
                    row, 0).text() == 'col_names' and len(
                        self.data['meta'][key]) != len(
                            self.data['data'].columns):
                QMessageBox.warning(
                    self, 'Restricted Parameter',
                    'Please provide same length of col_names as the number of the column of the data'
                )
                self.data['meta'][key] = eval(self.oldMetaText)
                item.setText(self.oldMetaText)
            elif self.metaDataTableWidget.item(
                    row, 0).text() == 'col_names' and len(
                        self.data['meta'][key]) == len(
                            self.data['data'].columns):
                self.data['data'].columns = self.data['meta'][key]
                self.dataTableWidget.setHorizontalHeaderLabels(
                    self.data['meta'][key])
                self.dataAltered = True
                self.resetPlotSetup()
                self.dataAltered = False
        else:
            if self.oldMetaText == 'col_names':
                QMessageBox.warning(
                    self, 'Restricted Parameter',
                    'col_names is a restricted parameter the name of which cannot be changed',
                    QMessageBox.Ok)
                item.setText(self.oldMetaText)
            elif item.text() not in self.data['meta'].keys():
                self.data['meta'][key] = self.data['meta'][self.oldMetaText]
                del self.data['meta'][self.oldMetaText]
            else:
                self.metaDataTableWidget.itemChanged.disconnect()
                QMessageBox.warning(
                    self, "Parameter Exists",
                    "The parameter %s already exists in meta data. Please provide a different parameter name"
                    % item.text(), QMessageBox.Ok)
                item.setText(self.oldMetaText)
                self.metaDataTableWidget.itemChanged.connect(
                    self.metaDataChanged)
        self.oldMetaText = item.text()

    def metaDataClicked(self, item):
        self.oldMetaText = item.text()

    def metaDataSelectionChanged(self):
        self.oldMetaText = self.metaDataTableWidget.selectedItems()[0].text()

    def dataChanged(self, item):
        row, col = item.row(), item.column()
        key = self.dataTableWidget.horizontalHeaderItem(col).text()
        self.data['data'][key][row] = eval(item.text())
        self.dataAltered = True
        self.resetPlotSetup()
        self.dataAltered = False

    def dataSorted(self):
        """
        Updates the data after sorting the DataTableWidget
        """
        self.getDataFromTable()
        self.dataAltered = True
        self.resetPlotSetup()
        self.dataAltered = False

    def addDataRow(self):
        try:
            self.dataTableWidget.itemChanged.disconnect()
        except:
            pass
        row = self.dataTableWidget.currentRow()
        self.dataTableWidget.insertRow(row + 1)
        for col in range(self.dataTableWidget.columnCount()):
            self.dataTableWidget.setItem(
                row + 1, col,
                QCustomTableWidgetItem(
                    float(self.dataTableWidget.item(row, col).text())))
        self.getDataFromTable()
        self.dataAltered = True
        self.resetPlotSetup()
        self.dataAltered = False
        self.dataTableWidget.itemChanged.connect(self.dataChanged)

    def editDataColumn(self):
        if self.data is not None:
            items = self.dataTableWidget.selectedItems()
            selCols = list([item.column() for item in items])
            if len(selCols) == 1:
                colName = self.dataTableWidget.horizontalHeaderItem(
                    selCols[0]).text()
                self.addDataColumn(colName=colName,
                                   expr=self.expressions[colName],
                                   new=False)
            else:
                QMessageBox.warning(
                    self, 'Column Selection Error',
                    'Please select only elements of a single column.',
                    QMessageBox.Ok)
        else:
            QMessageBox.warning(self, 'Data error', 'There is no data',
                                QMessageBox.Ok)

    def addDataColumn(self, colName='Col_X', expr=None, new=True):
        if self.data is not None:
            row, col = self.data['data'].shape
            self.insertColDialog = InsertCol_Dialog(colName=colName,
                                                    minCounter=1,
                                                    maxCounter=row,
                                                    expr=expr)
            if self.insertColDialog.exec_():
                imin = eval(self.insertColDialog.minCounterLineEdit.text())
                imax = eval(self.insertColDialog.maxCounterLineEdit.text())
                i = arange(imin, imax + 1)
                colname = self.insertColDialog.colNameLineEdit.text()
                data = copy.copy(self.data)
                if new:
                    if colname not in self.data['data'].columns:
                        try:
                            self.data['data'][colname] = eval(expr)
                        except:
                            try:
                                expr = self.insertColDialog.colExprTextEdit.toPlainText(
                                )
                                cexpr = expr.replace('col',
                                                     "self.data['data']")
                                self.data['data'][colname] = eval(cexpr)
                                self.data['meta']['col_names'].append(colname)
                            except:
                                QMessageBox.warning(
                                    self, 'Column Error',
                                    'Please check the expression.\n The expression should be in this format:\n col[column_name]*5',
                                    QMessageBox.Ok)
                                self.addDataColumn(colName='Col_X', expr=expr)
                        self.expressions[colname] = expr
                        self.setData2Table()
                        self.setMeta2Table()
                        self.dataAltered = True
                        self.resetPlotSetup()
                        self.dataAltered = False
                    else:
                        QMessageBox.warning(
                            self, 'Column Name Error',
                            'Please choose different column name than the exisiting ones',
                            QMessageBox.Ok)
                        self.addDataColumn(colName='Col_X', expr=expr)
                else:
                    try:
                        self.data['data'][colname] = eval(expr)
                    except:
                        try:
                            expr = self.insertColDialog.colExprTextEdit.toPlainText(
                            )
                            cexpr = expr.replace('col', "self.data['data\']")
                            self.data['data'][colname] = eval(cexpr)
                        except:
                            QMessageBox.warning(
                                self, 'Column Error',
                                'Please check the expression.\n The expression should be in this format:\n col[column_name]*5',
                                QMessageBox.Ok)
                            self.addDataColumn(colName='Col_X', expr=expr)
                        self.expressions[colname] = expr
                        self.setData2Table()
                        self.setMeta2Table()
                        self.dataAltered = True
                        self.resetPlotSetup()
                        self.dataAltered = False
        else:
            self.data = {}
            self.insertColDialog = InsertCol_Dialog(colName=colName,
                                                    minCounter=1,
                                                    maxCounter=100,
                                                    expr=expr)
            if self.insertColDialog.exec_():
                imin = eval(self.insertColDialog.minCounterLineEdit.text())
                imax = eval(self.insertColDialog.maxCounterLineEdit.text())
                i = arange(imin, imax + 1)
                colname = self.insertColDialog.colNameLineEdit.text()
                expr = self.insertColDialog.colExprTextEdit.toPlainText()
                expr = expr.replace('col.', "self.data['data']")
                try:
                    self.data['data'] = pd.DataFrame(eval(expr),
                                                     columns=[colname])
                    self.data['meta'] = {}
                    self.data['meta']['col_names'] = [colname]
                    self.setData2Table()
                    self.setMeta2Table()
                    self.dataAltered = True
                    self.resetPlotSetup()
                    self.dataAltered = False
                    self.saveDataPushButton.setEnabled(True)
                    self.addRowPushButton.setEnabled(True)
                    self.removeRowsPushButton.setEnabled(True)
                    self.removeColumnPushButton.setEnabled(True)
                    self.expressions[colname] = expr
                except:
                    QMessageBox.warning(
                        self, 'Column Error',
                        'Please check the expression.\n The expression should be in this format:\n col.column_name*5',
                        QMessageBox.Ok)
                    self.data = None
                    self.addDataColumn(colName='Col_X', expr=expr)

    def removeDataColumn(self):
        """
        Removes selected columns from dataTableWidget
        """
        colIndexes = [
            index.column() for index in
            self.dataTableWidget.selectionModel().selectedColumns()
        ]
        colIndexes.sort(reverse=True)
        if self.dataTableWidget.columnCount() - len(
                colIndexes) >= 2 or self.plotSetupTableWidget.rowCount() == 0:
            for index in colIndexes:
                colname = self.data['meta']['col_names'][index]
                self.data['meta']['col_names'].pop(index)
                del self.expressions[colname]
                self.dataTableWidget.removeColumn(index)
            if self.dataTableWidget.columnCount() != 0:
                self.getDataFromTable()
                self.setMeta2Table()
                self.dataAltered = True
                self.resetPlotSetup()
                self.dataAltered = False
            else:
                self.data['data'] = None
                self.dataTableWidget.clear()
                #self.metaDataTableWidget.clear()
                self.autoUpdateCheckBox.setEnabled(False)
                self.saveDataPushButton.setEnabled(False)
                self.addRowPushButton.setEnabled(False)
                self.removeRowsPushButton.setEnabled(False)
                self.removeColumnPushButton.setEnabled(False)
        else:
            QMessageBox.warning(
                self, 'Remove Error',
                'Cannot remove these many columns because Data Dialog needs to have atleast two columns',
                QMessageBox.Ok)

    def removeDataRows(self):
        rowIndexes = [
            index.row()
            for index in self.dataTableWidget.selectionModel().selectedRows()
        ]
        rowIndexes.sort(reverse=True)
        if len(rowIndexes) > 0:
            ans = QMessageBox.question(
                self, 'Confirmation',
                'Are you sure of removing the selected rows?', QMessageBox.Yes,
                QMessageBox.No)
            if ans == QMessageBox.Yes:
                for i in rowIndexes:
                    self.dataTableWidget.removeRow(i)
                self.getDataFromTable()
                self.dataAltered = True
                self.resetPlotSetup()
                self.dataAltered = False

    def setMeta2Table(self):
        """
        Populates the metaDataTable widget with metadata available from the data
        """
        try:
            self.metaDataTableWidget.itemChanged.disconnect()
            self.metaDataTableWidget.itemSelectionChanged.disconnect()
        except:
            pass
        self.metaDataTableWidget.clear()
        self.metaDataTableWidget.setColumnCount(2)
        self.metaDataTableWidget.setRowCount(len(self.data['meta'].keys()))
        for num, key in enumerate(self.data['meta'].keys()):
            self.metaDataTableWidget.setItem(num, 0, QTableWidgetItem(key))
            self.metaDataTableWidget.setItem(
                num, 1, QTableWidgetItem(str(self.data['meta'][key])))
        if 'col_names' not in self.data['meta'].keys():
            self.data['meta']['col_names'] = self.data['data'].columns.tolist()
            self.metaDataTableWidget.insertRow(
                self.metaDataTableWidget.rowCount())
            self.metaDataTableWidget.setItem(num + 1, 0,
                                             QTableWidgetItem('col_names'))
            self.metaDataTableWidget.setItem(
                num + 1, 1,
                QTableWidgetItem(str(self.data['meta']['col_names'])))
        self.metaDataTableWidget.setHorizontalHeaderLabels(
            ['Parameter', 'Value'])
        self.metaDataTableWidget.itemChanged.connect(self.metaDataChanged)
        self.metaDataTableWidget.itemSelectionChanged.connect(
            self.metaDataSelectionChanged)

    def getMetaFromTable(self):
        self.data['meta'] = {}
        for i in range(self.metaDataTableWidget.rowCount()):
            try:
                self.data['meta'][self.metaDataTableWidget.item(
                    i, 0).text()] = eval(
                        self.metaDataTableWidget.item(i, 1).text())
            except:
                self.data['meta'][self.metaDataTableWidget.item(
                    i, 0).text()] = self.metaDataTableWidget.item(i, 1).text()

    def setData2Table(self):
        """
        Populates the dataTableWidget with data available from data
        """
        try:
            self.dataTableWidget.itemChanged.disconnect()
        except:
            pass
        self.dataTableWidget.clear()
        self.dataTableWidget.setColumnCount(len(self.data['data'].columns))
        self.dataTableWidget.setRowCount(len(self.data['data'].index))
        for j, colname in enumerate(self.data['data'].columns):
            if colname not in self.expressions.keys():
                self.expressions[colname] = "col['%s']" % colname
            for i in range(len(self.data['data'].index)):
                #self.dataTableWidget.setItem(i,j,QTableWidgetItem(str(self.data['data'][colname][i])))
                self.dataTableWidget.setItem(
                    i, j,
                    QCustomTableWidgetItem(self.data['data'][colname][i]))
        self.dataTableWidget.setHorizontalHeaderLabels(
            self.data['data'].columns.values.tolist())
        self.dataTableWidget.itemChanged.connect(self.dataChanged)

    def getDataFromTable(self):
        self.data['data'] = pd.DataFrame()
        for col in range(self.dataTableWidget.columnCount()):
            label = self.dataTableWidget.horizontalHeaderItem(col).text()
            self.data['data'][label] = array([
                float(self.dataTableWidget.item(i, col).text())
                for i in range(self.dataTableWidget.rowCount())
            ])

    def readData(self, fname, skiprows=0, comment='#', delimiter=' '):
        """
        Read data from a file and put it in dictionary structure with keys 'meta' and 'data' and the data would look like the following
        data={'meta':meta_dictionary,'data'=pandas_dataframe}
        """
        if os.path.exists(os.path.abspath(fname)):
            self.data = {}
            self.fname = fname
            self.dataFileLineEdit.setText(self.fname)
            self.cwd = os.path.dirname(self.fname)
            fh = open(os.path.abspath(self.fname), 'r')
            lines = fh.readlines()
            fh.close()
            self.data['meta'] = {}
            for line in lines[skiprows:]:
                if line[0] == comment:
                    try:
                        key, value = line[1:].strip().split('=')
                        try:
                            self.data['meta'][key] = eval(
                                value
                            )  # When the value is either valid number, lists, arrays, dictionaries
                        except:
                            self.data['meta'][
                                key] = value  # When the value is just a string
                    except:
                        pass
                else:
                    if '\t' in line:
                        delimiter = '\t'
                    elif ',' in line:
                        delimiter = ','
                    elif ' ' in line:
                        delimiter = ' '
                    break
            if 'col_names' in self.data['meta'].keys():
                self.data['data'] = pd.read_csv(
                    self.fname,
                    comment=comment,
                    names=self.data['meta']['col_names'],
                    header=None,
                    sep=delimiter)
                if not all(self.data['data'].isnull().values):
                    self.data['data'] = pd.DataFrame(
                        loadtxt(self.fname, skiprows=skiprows),
                        columns=self.data['meta']['col_names'])
            else:
                self.data['data'] = pd.read_csv(self.fname,
                                                comment=comment,
                                                header=None,
                                                sep=delimiter)
                if not all(self.data['data'].isnull()):
                    self.data['data'] = pd.DataFrame(
                        loadtxt(self.fname, skiprows=skiprows))
                self.data['data'].columns = [
                    'Col_%d' % i
                    for i in self.data['data'].columns.values.tolist()
                ]
                self.data['meta']['col_names'] = self.data[
                    'data'].columns.values.tolist()
            self.autoUpdate_ON_OFF()
            self.autoUpdateCheckBox.setEnabled(True)
            self.saveDataPushButton.setEnabled(True)
            self.addRowPushButton.setEnabled(True)
            self.removeRowsPushButton.setEnabled(True)
            self.removeColumnPushButton.setEnabled(True)
            return self.data
        else:
            QMessageBox.warning(self, 'File Error', 'The file doesnot exists!')
            return None

    def fileUpdated(self, fname):
        QTest.qWait(1000)
        self.readData(fname=fname)
        if self.data is not None:
            self.setMeta2Table()
            self.setData2Table()
            self.dataAltered = True
            self.resetPlotSetup()
            self.dataAltered = False

    def autoUpdate_ON_OFF(self):
        files = self.fileWatcher.files()
        if len(files) != 0:
            self.fileWatcher.removePaths(files)
        if self.autoUpdateCheckBox.isChecked():
            self.fileWatcher.addPath(self.fname)

    def saveData(self):
        """
        Save data to a file
        """
        fname = QFileDialog.getSaveFileName(self,
                                            'Save file as',
                                            self.cwd,
                                            filter='*.*')[0]
        if fname != '':
            ext = os.path.splitext(fname)[1]
            if ext == '':
                ext = '.txt'
                fname = fname + ext
            header = 'File saved on %s\n' % time.asctime()
            for key in self.data['meta'].keys():
                header = header + '%s=%s\n' % (key, str(
                    self.data['meta'][key]))
            if 'col_names' not in self.data['meta'].keys():
                header = header + 'col_names=%s\n' % str(
                    self.data['data'].columns.tolist())
            savetxt(fname,
                    self.data['data'].values,
                    header=header,
                    comments='#')

    def openFile(self):
        """
        Opens a openFileDialog to open a data file
        """
        if self.cwd is not None:
            fname = QFileDialog.getOpenFileName(self,
                                                'Select a data file to open',
                                                directory=self.cwd,
                                                filter='*.*')[0]
        else:
            fname = QFileDialog.getOpenFileName(self,
                                                'Select a data file to open',
                                                directory='',
                                                filter='*.*')[0]
        if fname != '':
            self.data = self.readData(fname=fname)
            if self.data is not None:
                self.setMeta2Table()
                self.setData2Table()
                self.dataAltered = True
                self.resetPlotSetup()
                self.dataAltered = False

    def resetPlotSetup(self):
        try:
            self.plotSetupTableWidget.cellChanged.disconnect()
        except:
            pass
        columns = self.data['data'].columns.tolist()
        self.xlabel = []
        self.ylabel = []
        for row in range(self.plotSetupTableWidget.rowCount()):
            for i in range(1, 3):
                self.plotSetupTableWidget.cellWidget(
                    row, i).currentIndexChanged.disconnect()
                self.plotSetupTableWidget.cellWidget(row, i).clear()
                self.plotSetupTableWidget.cellWidget(row, i).addItems(columns)
                self.plotSetupTableWidget.cellWidget(row,
                                                     i).setCurrentIndex(i - 1)
                self.plotSetupTableWidget.cellWidget(
                    row, i).currentIndexChanged.connect(self.updateCellData)
            self.xlabel.append(
                '[%s]' %
                self.plotSetupTableWidget.cellWidget(row, 1).currentText())
            self.ylabel.append(
                '[%s]' %
                self.plotSetupTableWidget.cellWidget(row, 2).currentText())
            self.plotSetupTableWidget.cellWidget(
                row, 3).currentIndexChanged.disconnect()
            self.plotSetupTableWidget.cellWidget(row, 3).clear()
            self.plotSetupTableWidget.cellWidget(row, 3).addItems(['None'] +
                                                                  columns)
            self.plotSetupTableWidget.cellWidget(row, 3).setCurrentIndex(0)
            self.plotSetupTableWidget.cellWidget(
                row, 3).currentIndexChanged.connect(self.updateCellData)
            self.plotSetupTableWidget.setCurrentCell(row, 3)
            color = self.plotSetupTableWidget.cellWidget(row, 4).color()
            self.plotSetupTableWidget.setCellWidget(
                row, 4, pg.ColorButton(color=color))
            self.plotSetupTableWidget.cellWidget(
                row, 4).sigColorChanging.connect(self.updateCellData)
            self.plotSetupTableWidget.cellWidget(
                row, 4).sigColorChanged.connect(self.updateCellData)
            self.updatePlotData(row, i)
        self.plotSetupTableWidget.cellChanged.connect(self.updatePlotData)

    def addMultiPlots(self, plotIndex=None, colors=None):
        for key in plotIndex.keys():
            pi = plotIndex[key]
            if colors is None:
                color = next(self.colcycler
                             )  #array([random.randint(200, high=255),0,0])
                print(color)
            else:
                color = colors[key]
            self.addPlots(plotIndex=pi, color=color)

    def addPlots(self, plotIndex=None, color=None):
        #self.plotSetupTableWidget.clear()
        # if self.parentWidget() is None or self.plotSetupTableWidget.rowCount()==0:
        try:
            self.plotSetupTableWidget.cellChanged.disconnect()
        except:
            pass
        columns = self.data['data'].columns.tolist()
        if len(columns) >= 2:
            self.plotSetupTableWidget.insertRow(
                self.plotSetupTableWidget.rowCount())
            row = self.plotSetupTableWidget.rowCount() - 1
            self.plotSetupTableWidget.setItem(
                row, 0, QTableWidgetItem('Data_%d' % self.plotNum))
            for i in range(1, 3):
                self.plotSetupTableWidget.setCellWidget(row, i, QComboBox())
                self.plotSetupTableWidget.cellWidget(row, i).addItems(columns)
                if plotIndex is not None:
                    self.plotSetupTableWidget.cellWidget(
                        row, i).setCurrentIndex(plotIndex[i - 1])
                else:
                    self.plotSetupTableWidget.cellWidget(
                        row, i).setCurrentIndex(i - 1)
                self.plotSetupTableWidget.cellWidget(
                    row, i).currentIndexChanged.connect(self.updateCellData)
            self.xlabel.append(
                '[%s]' %
                self.plotSetupTableWidget.cellWidget(row, 1).currentText())
            self.ylabel.append(
                '[%s]' %
                self.plotSetupTableWidget.cellWidget(row, 2).currentText())
            self.plotSetupTableWidget.setCellWidget(row, 3, QComboBox())
            self.plotSetupTableWidget.cellWidget(row, 3).addItems(['None'] +
                                                                  columns)
            if color is None:
                color = next(self.colcycler
                             )  #array([random.randint(200, high=255),0,0])
            self.plotSetupTableWidget.setCellWidget(
                row, 4, pg.ColorButton(color=color))
            self.plotSetupTableWidget.cellWidget(
                row, 4).sigColorChanging.connect(self.updateCellData)
            self.plotSetupTableWidget.cellWidget(
                row, 4).sigColorChanged.connect(self.updateCellData)
            if plotIndex is not None:
                self.plotSetupTableWidget.cellWidget(row, 3).setCurrentIndex(
                    plotIndex[-1])
            else:
                # try:
                #     self.plotSetupTableWidget.cellWidget(row,3).setCurrentIndex(2)
                # except:
                #
                self.plotSetupTableWidget.cellWidget(row, 3).setCurrentIndex(0)
            self.plotSetupTableWidget.cellWidget(
                row, 3).currentIndexChanged.connect(self.updateCellData)
            self.plotSetupTableWidget.setCurrentCell(row, 3)
            self.updatePlotData(row, 3)
            self.plotNum += 1
        else:
            QMessageBox.warning(
                self, 'Data file error',
                'The data file do not have two or more columns to be plotted.',
                QMessageBox.Ok)
        self.plotSetupTableWidget.cellChanged.connect(self.updatePlotData)
        # else:
        #     QMessageBox.warning(self,'Warning','As the Data Dialog is used within another widget you cannot add more plots',QMessageBox.Ok)

    def removePlots(self):
        """
        Removes data for PlotSetup
        """
        try:
            self.plotSetupTableWidget.cellChanged.disconnect()
        except:
            pass
        rowIndexes = self.plotSetupTableWidget.selectionModel().selectedRows()
        selectedRows = [index.row() for index in rowIndexes]
        selectedRows.sort(reverse=True)
        if self.parentWidget() is None:
            for row in selectedRows:
                name = self.plotSetupTableWidget.item(row, 0).text()
                self.plotWidget.remove_data([name])
                self.plotSetupTableWidget.removeRow(row)
        else:
            if self.plotSetupTableWidget.rowCount() - len(rowIndexes) >= 1:
                for row in selectedRows:
                    name = self.plotSetupTableWidget.item(row, 0).text()
                    self.plotWidget.remove_data([name])
                    self.plotSetupTableWidget.removeRow(row)
            else:
                QMessageBox.warning(
                    self, 'Warning',
                    'Cannot remove single plots from Data Dialog because the Data Dialog is used within another widget',
                    QMessageBox.Ok)
        self.updatePlot()
        self.plotSetupTableWidget.cellChanged.connect(self.updatePlotData)

    def updatePlotData(self, row, col):
        #row=self.plotSetupTableWidget.currentRow()
        name = self.plotSetupTableWidget.item(row, 0).text()
        if self.dataAltered:
            for i in range(1, 4):
                try:
                    self.plotSetupTableWidget.cellWidget(
                        row, i).setCurrentIndex(self.oldPlotIndex[name][i - 1])
                except:
                    pass
        xcol, ycol, yerrcol = [
            self.plotSetupTableWidget.cellWidget(row, i).currentText()
            for i in range(1, 4)
        ]
        #ycol=self.plotSetupTableWidget.cellWidget(row,2).currentText()
        #yerrcol=self.plotSetupTableWidget.cellWidget(row,3).currentText()
        if yerrcol != 'None':
            if ycol == 'fit':
                self.plotWidget.add_data(
                    self.data['data'][xcol].values,
                    self.data['data'][ycol].values,
                    yerr=self.data['data'][yerrcol].values,
                    name=name,
                    fit=True,
                    color=self.plotSetupTableWidget.cellWidget(row, 4).color())
            else:
                self.plotWidget.add_data(
                    self.data['data'][xcol].values,
                    self.data['data'][ycol].values,
                    yerr=self.data['data'][yerrcol].values,
                    name=name,
                    fit=False,
                    color=self.plotSetupTableWidget.cellWidget(row, 4).color())
        else:
            if ycol == 'fit':
                self.plotWidget.add_data(
                    self.data['data'][xcol].values,
                    self.data['data'][ycol].values,
                    name=name,
                    fit=True,
                    color=self.plotSetupTableWidget.cellWidget(row, 4).color())
            else:
                self.plotWidget.add_data(
                    self.data['data'][xcol].values,
                    self.data['data'][ycol].values,
                    name=name,
                    fit=False,
                    color=self.plotSetupTableWidget.cellWidget(row, 4).color())
        self.xlabel[row] = '[%s]' % self.plotSetupTableWidget.cellWidget(
            row, 1).currentText()
        self.ylabel[row] = '[%s]' % self.plotSetupTableWidget.cellWidget(
            row, 2).currentText()
        self.updatePlot()
        self.oldPlotIndex[name] = [
            self.plotSetupTableWidget.cellWidget(row, i).currentIndex()
            for i in range(1, 4)
        ]

    def updateCellData(self, index):
        row = self.plotSetupTableWidget.indexAt(self.sender().pos()).row()
        self.updatePlotData(row, index)

    def updatePlot(self):
        self.make_default()
        names = [
            self.plotSetupTableWidget.item(i, 0).text()
            for i in range(self.plotSetupTableWidget.rowCount())
        ]
        #self.plotColIndex=[self.plotSetupTableWidget.cellWidget(0,i).currentIndex() for i in range(1,4)]
        self.plotColIndex = {}
        self.externalData = {}
        self.plotColors = {}
        for i in range(self.plotSetupTableWidget.rowCount()):
            key = self.plotSetupTableWidget.cellWidget(i, 2).currentText()
            self.plotColIndex[key] = [
                self.plotSetupTableWidget.cellWidget(i, j).currentIndex()
                for j in range(1, 4)
            ]
            self.plotColors[key] = self.plotSetupTableWidget.cellWidget(
                i, 4).color()
            self.externalData[key] = copy.copy(self.data['meta'])
            self.externalData[key]['x'] = copy.copy(
                self.data['data'][self.plotSetupTableWidget.cellWidget(
                    i, 1).currentText()].values)
            self.externalData[key]['y'] = copy.copy(
                self.data['data'][self.plotSetupTableWidget.cellWidget(
                    i, 2).currentText()].values)
            if self.plotSetupTableWidget.cellWidget(i,
                                                    3).currentText() == 'None':
                self.externalData[key]['yerr'] = ones_like(
                    self.externalData[key]['x'])
            else:
                self.externalData[key]['yerr'] = copy.copy(
                    self.data['data'][self.plotSetupTableWidget.cellWidget(
                        i, 3).currentText()].values)
            self.externalData[key][
                'color'] = self.plotSetupTableWidget.cellWidget(i, 4).color()
        self.plotWidget.Plot(names)
        self.plotWidget.setXLabel(' '.join(self.xlabel))
        self.plotWidget.setYLabel(' '.join(self.ylabel))
class ProjectBrowserModel(BrowserModel):
    """
    Class implementing the project browser model.
    
    @signal vcsStateChanged(str) emitted after the VCS state has changed
    """
    vcsStateChanged = pyqtSignal(str)

    def __init__(self, parent):
        """
        Constructor
        
        @param parent reference to parent object (Project.Project)
        """
        super(ProjectBrowserModel, self).__init__(parent, nopopulate=True)

        rootData = self.tr("Name")
        self.rootItem = BrowserItem(None, rootData)
        self.rootItem.itemData.append(self.tr("VCS Status"))

        self.progDir = None
        self.project = parent

        self.watchedItems = {}
        self.watcher = QFileSystemWatcher(self)
        self.watcher.directoryChanged.connect(self.directoryChanged)

        self.inRefresh = False

        self.projectBrowserTypes = {
            "SOURCES": ProjectBrowserSourceType,
            "FORMS": ProjectBrowserFormType,
            "RESOURCES": ProjectBrowserResourceType,
            "INTERFACES": ProjectBrowserInterfaceType,
            "TRANSLATIONS": ProjectBrowserTranslationType,
            "OTHERS": ProjectBrowserOthersType,
        }

        self.colorNames = {
            "A": "VcsAdded",
            "M": "VcsModified",
            "O": "VcsRemoved",
            "R": "VcsReplaced",
            "U": "VcsUpdate",
            "Z": "VcsConflict",
        }
        self.itemBackgroundColors = {
            " ": QColor(),
            "A": Preferences.getProjectBrowserColour(self.colorNames["A"]),
            "M": Preferences.getProjectBrowserColour(self.colorNames["M"]),
            "O": Preferences.getProjectBrowserColour(self.colorNames["O"]),
            "R": Preferences.getProjectBrowserColour(self.colorNames["R"]),
            "U": Preferences.getProjectBrowserColour(self.colorNames["U"]),
            "Z": Preferences.getProjectBrowserColour(self.colorNames["Z"]),
        }

        self.highLightColor = \
            Preferences.getProjectBrowserColour("Highlighted")
        # needed by preferencesChanged()

        self.vcsStatusReport = {}

    def data(self, index, role):
        """
        Public method to get data of an item.
        
        @param index index of the data to retrieve (QModelIndex)
        @param role role of data (Qt.ItemDataRole)
        @return requested data
        """
        if not index.isValid():
            return None

        if role == Qt.TextColorRole:
            if index.column() == 0:
                try:
                    return index.internalPointer().getTextColor()
                except AttributeError:
                    return None
        elif role == Qt.BackgroundColorRole:
            try:
                col = self.itemBackgroundColors[
                    index.internalPointer().vcsState]
                if col.isValid():
                    return col
                else:
                    return None
            except AttributeError:
                return None
            except KeyError:
                return None

        return BrowserModel.data(self, index, role)

    def populateItem(self, parentItem, repopulate=False):
        """
        Public method to populate an item's subtree.
        
        @param parentItem reference to the item to be populated
        @param repopulate flag indicating a repopulation (boolean)
        """
        if parentItem.type() == ProjectBrowserItemSimpleDirectory:
            return  # nothing to do
        elif parentItem.type() == ProjectBrowserItemDirectory:
            self.populateProjectDirectoryItem(parentItem, repopulate)
        elif parentItem.type() == ProjectBrowserItemFile:
            self.populateFileItem(parentItem, repopulate)
        else:
            BrowserModel.populateItem(self, parentItem, repopulate)

    def populateProjectDirectoryItem(self, parentItem, repopulate=False):
        """
        Public method to populate a directory item's subtree.
        
        @param parentItem reference to the directory item to be populated
        @param repopulate flag indicating a repopulation (boolean)
        """
        self._addWatchedItem(parentItem)

        qdir = QDir(parentItem.dirName())

        if Preferences.getUI("BrowsersListHiddenFiles"):
            filter = QDir.Filters(QDir.AllEntries | QDir.Hidden
                                  | QDir.NoDotAndDotDot)
        else:
            filter = QDir.Filters(QDir.AllEntries | QDir.NoDot | QDir.NoDotDot)
        entryInfoList = qdir.entryInfoList(filter)

        if len(entryInfoList) > 0:
            if repopulate:
                self.beginInsertRows(
                    self.createIndex(parentItem.row(), 0, parentItem), 0,
                    len(entryInfoList) - 1)
            states = {}
            if self.project.vcs is not None:
                for f in entryInfoList:
                    fname = f.absoluteFilePath()
                    states[os.path.normcase(fname)] = 0
                dname = parentItem.dirName()
                self.project.vcs.clearStatusCache()
                states = self.project.vcs.vcsAllRegisteredStates(states, dname)

            for f in entryInfoList:
                if f.isDir():
                    node = ProjectBrowserDirectoryItem(
                        parentItem,
                        Utilities.toNativeSeparators(f.absoluteFilePath()),
                        parentItem.getProjectTypes()[0], False)
                else:
                    node = ProjectBrowserFileItem(
                        parentItem,
                        Utilities.toNativeSeparators(f.absoluteFilePath()),
                        parentItem.getProjectTypes()[0])
                if self.project.vcs is not None:
                    fname = f.absoluteFilePath()
                    if states[os.path.normcase(fname)] == \
                            self.project.vcs.canBeCommitted:
                        node.addVcsStatus(self.project.vcs.vcsName())
                        self.project.clearStatusMonitorCachedState(
                            f.absoluteFilePath())
                    else:
                        node.addVcsStatus(self.tr("local"))
                self._addItem(node, parentItem)
            if repopulate:
                self.endInsertRows()

    def projectClosed(self):
        """
        Public method called after a project has been closed.
        """
        self.__vcsStatus = {}

        self.watchedItems = {}
        watchedDirs = self.watcher.directories()
        if watchedDirs:
            self.watcher.removePaths(watchedDirs)

        self.rootItem.removeChildren()
        self.beginResetModel()
        self.endResetModel()

        # reset the module parser cache
        Utilities.ModuleParser.resetParsedModules()

    def projectOpened(self):
        """
        Public method used to populate the model after a project has been
        opened.
        """
        self.__vcsStatus = {}
        states = {}
        keys = list(self.projectBrowserTypes.keys())[:]

        if self.project.vcs is not None:
            for key in keys:
                for fn in self.project.pdata[key]:
                    states[os.path.normcase(
                        os.path.join(self.project.ppath, fn))] = 0

            self.project.vcs.clearStatusCache()
            states = self.project.vcs.vcsAllRegisteredStates(
                states, self.project.ppath)

        self.inRefresh = True
        for key in keys:
            # Show the entry in bold in the others browser to make it more
            # distinguishable
            if key == "OTHERS":
                bold = True
            else:
                bold = False

            if key == "SOURCES":
                sourceLanguage = self.project.pdata["PROGLANGUAGE"][0]
            else:
                sourceLanguage = ""

            for fn in self.project.pdata[key]:
                fname = os.path.join(self.project.ppath, fn)
                parentItem, dt = self.findParentItemByName(
                    self.projectBrowserTypes[key], fn)
                if os.path.isdir(fname):
                    itm = ProjectBrowserDirectoryItem(
                        parentItem, fname, self.projectBrowserTypes[key],
                        False, bold)
                else:
                    itm = ProjectBrowserFileItem(parentItem,
                                                 fname,
                                                 self.projectBrowserTypes[key],
                                                 False,
                                                 bold,
                                                 sourceLanguage=sourceLanguage)
                self._addItem(itm, parentItem)
                if self.project.vcs is not None:
                    if states[os.path.normcase(fname)] == \
                            self.project.vcs.canBeCommitted:
                        itm.addVcsStatus(self.project.vcs.vcsName())
                    else:
                        itm.addVcsStatus(self.tr("local"))
                else:
                    itm.addVcsStatus("")
        self.inRefresh = False
        self.beginResetModel()
        self.endResetModel()

    def findParentItemByName(self, type_, name, dontSplit=False):
        """
        Public method to find an item given its name.
        
        <b>Note</b>: This method creates all necessary parent items, if they
        don't exist.
        
        @param type_ type of the item
        @param name name of the item (string)
        @param dontSplit flag indicating the name should not be split (boolean)
        @return reference to the item found and the new display name (string)
        """
        if dontSplit:
            pathlist = []
            pathlist.append(name)
            pathlist.append("ignore_me")
        else:
            pathlist = re.split(r'/|\\', name)

        if len(pathlist) > 1:
            olditem = self.rootItem
            path = self.project.ppath
            for p in pathlist[:-1]:
                itm = self.findChildItem(p, 0, olditem)
                path = os.path.join(path, p)
                if itm is None:
                    itm = ProjectBrowserSimpleDirectoryItem(
                        olditem, type_, p, path)
                    self.__addVCSStatus(itm, path)
                    if self.inRefresh:
                        self._addItem(itm, olditem)
                    else:
                        if olditem == self.rootItem:
                            oldindex = QModelIndex()
                        else:
                            oldindex = self.createIndex(
                                olditem.row(), 0, olditem)
                        self.addItem(itm, oldindex)
                else:
                    if type_ and type_ not in itm.getProjectTypes():
                        itm.addProjectType(type_)
                        index = self.createIndex(itm.row(), 0, itm)
                        self.dataChanged.emit(index, index)
                olditem = itm
            return (itm, pathlist[-1])
        else:
            return (self.rootItem, name)

    def findChildItem(self, text, column, parentItem=None):
        """
        Public method to find a child item given some text.
        
        @param text text to search for (string)
        @param column column to search in (integer)
        @param parentItem reference to parent item
        @return reference to the item found
        """
        if parentItem is None:
            parentItem = self.rootItem

        for itm in parentItem.children():
            if itm.data(column) == text:
                return itm

        return None

    def addNewItem(self, typeString, name, additionalTypeStrings=[]):
        """
        Public method to add a new item to the model.
        
        @param typeString string denoting the type of the new item (string)
        @param name name of the new item (string)
        @param additionalTypeStrings names of additional types (list of string)
        """
        # Show the entry in bold in the others browser to make it more
        # distinguishable
        if typeString == "OTHERS":
            bold = True
        else:
            bold = False

        fname = os.path.join(self.project.ppath, name)
        parentItem, dt = self.findParentItemByName(
            self.projectBrowserTypes[typeString], name)
        if parentItem == self.rootItem:
            parentIndex = QModelIndex()
        else:
            parentIndex = self.createIndex(parentItem.row(), 0, parentItem)
        if os.path.isdir(fname):
            itm = ProjectBrowserDirectoryItem(
                parentItem, fname, self.projectBrowserTypes[typeString], False,
                bold)
        else:
            if typeString == "SOURCES":
                sourceLanguage = self.project.pdata["PROGLANGUAGE"][0]
            else:
                sourceLanguage = ""
            itm = ProjectBrowserFileItem(parentItem,
                                         fname,
                                         self.projectBrowserTypes[typeString],
                                         False,
                                         bold,
                                         sourceLanguage=sourceLanguage)
        self.__addVCSStatus(itm, fname)
        if additionalTypeStrings:
            for additionalTypeString in additionalTypeStrings:
                type_ = self.projectBrowserTypes[additionalTypeString]
                itm.addProjectType(type_)
        self.addItem(itm, parentIndex)

    def renameItem(self, name, newFilename):
        """
        Public method to rename an item.
        
        @param name the old display name (string)
        @param newFilename new filename of the item (string)
        """
        itm = self.findItem(name)
        if itm is None:
            return

        index = self.createIndex(itm.row(), 0, itm)
        itm.setName(newFilename)
        self.dataChanged.emit(index, index)
        self.repopulateItem(newFilename)

    def findItem(self, name):
        """
        Public method to find an item given its name.
        
        @param name name of the item (string)
        @return reference to the item found
        """
        if QDir.isAbsolutePath(name):
            name = self.project.getRelativePath(name)
        pathlist = re.split(r'/|\\', name)
        if len(pathlist) > 0:
            olditem = self.rootItem
            for p in pathlist:
                itm = self.findChildItem(p, 0, olditem)
                if itm is None:
                    return None
                olditem = itm
            return itm
        else:
            return None

    def itemIndexByName(self, name):
        """
        Public method to find an item's index given its name.
        
        @param name name of the item (string)
        @return index of the item found (QModelIndex)
        """
        itm = self.findItem(name)
        if itm is None:
            index = QModelIndex()
        else:
            index = self.createIndex(itm.row(), 0, itm)
        return index

    def itemIndexByNameAndLine(self, name, lineno):
        """
        Public method to find an item's index given its name.
        
        @param name name of the item (string)
        @param lineno one based line number of the item (integer)
        @return index of the item found (QModelIndex)
        """
        index = QModelIndex()
        itm = self.findItem(name)
        if itm is not None and \
           isinstance(itm, ProjectBrowserFileItem):
            olditem = itm
            autoPopulate = Preferences.getProject("AutoPopulateItems")
            while itm is not None:
                if not itm.isPopulated():
                    if itm.isLazyPopulated() and autoPopulate:
                        self.populateItem(itm)
                    else:
                        break
                for child in itm.children():
                    try:
                        start, end = child.boundaries()
                        if end == -1:
                            end = 1000000  # assume end of file
                        if start <= lineno <= end:
                            itm = child
                            break
                    except AttributeError:
                        pass
                else:
                    itm = None
                if itm:
                    olditem = itm
            index = self.createIndex(olditem.row(), 0, olditem)

        return index

    def directoryChanged(self, path):
        """
        Public slot to handle the directoryChanged signal of the watcher.
        
        @param path path of the directory (string)
        """
        if path not in self.watchedItems:
            # just ignore the situation we don't have a reference to the item
            return

        if Preferences.getUI("BrowsersListHiddenFiles"):
            filter = QDir.Filters(QDir.AllEntries | QDir.Hidden
                                  | QDir.NoDotAndDotDot)
        else:
            filter = QDir.Filters(QDir.AllEntries | QDir.NoDot | QDir.NoDotDot)

        for itm in self.watchedItems[path]:
            oldCnt = itm.childCount()

            qdir = QDir(itm.dirName())

            entryInfoList = qdir.entryInfoList(filter)

            # step 1: check for new entries
            children = itm.children()
            for f in entryInfoList:
                fpath = Utilities.toNativeSeparators(f.absoluteFilePath())
                childFound = False
                for child in children:
                    if child.name() == fpath:
                        childFound = True
                        children.remove(child)
                        break
                if childFound:
                    continue

                cnt = itm.childCount()
                self.beginInsertRows(self.createIndex(itm.row(), 0, itm), cnt,
                                     cnt)
                if f.isDir():
                    node = ProjectBrowserDirectoryItem(
                        itm,
                        Utilities.toNativeSeparators(f.absoluteFilePath()),
                        itm.getProjectTypes()[0], False)
                else:
                    node = ProjectBrowserFileItem(
                        itm,
                        Utilities.toNativeSeparators(f.absoluteFilePath()),
                        itm.getProjectTypes()[0])
                self._addItem(node, itm)
                if self.project.vcs is not None:
                    self.project.vcs.clearStatusCache()
                    state = self.project.vcs.vcsRegisteredState(node.name())
                    if state == self.project.vcs.canBeCommitted:
                        node.addVcsStatus(self.project.vcs.vcsName())
                    else:
                        node.addVcsStatus(self.tr("local"))
                self.endInsertRows()

            # step 2: check for removed entries
            if len(entryInfoList) != itm.childCount():
                for row in range(oldCnt - 1, -1, -1):
                    child = itm.child(row)
                    childname = Utilities.fromNativeSeparators(child.name())
                    entryFound = False
                    for f in entryInfoList:
                        if f.absoluteFilePath() == childname:
                            entryFound = True
                            entryInfoList.remove(f)
                            break
                    if entryFound:
                        continue

                    self._removeWatchedItem(child)
                    self.beginRemoveRows(self.createIndex(itm.row(), 0, itm),
                                         row, row)
                    itm.removeChild(child)
                    self.endRemoveRows()

    def __addVCSStatus(self, item, name):
        """
        Private method used to set the vcs status of a node.
        
        @param item item to work on
        @param name filename belonging to this item (string)
        """
        if self.project.vcs is not None:
            state = self.project.vcs.vcsRegisteredState(name)
            if state == self.project.vcs.canBeCommitted:
                item.addVcsStatus(self.project.vcs.vcsName())
            else:
                item.addVcsStatus(self.tr("local"))
        else:
            item.addVcsStatus("")

    def __updateVCSStatus(self, item, name, recursive=True):
        """
        Private method used to update the vcs status of a node.
        
        @param item item to work on
        @param name filename belonging to this item (string)
        @keyparam recursive flag indicating a recursive update (boolean)
        """
        if self.project.vcs is not None:
            self.project.vcs.clearStatusCache()
            state = self.project.vcs.vcsRegisteredState(name)
            if state == self.project.vcs.canBeCommitted:
                item.setVcsStatus(self.project.vcs.vcsName())
            else:
                item.setVcsStatus(self.tr("local"))
            if recursive:
                name = os.path.dirname(name)
                parentItem = item.parent()
                if name and parentItem is not self.rootItem:
                    self.__updateVCSStatus(parentItem, name, recursive)
        else:
            item.setVcsStatus("")

        index = self.createIndex(item.row(), 0, item)
        self.dataChanged.emit(index, index)

    def updateVCSStatus(self, name, recursive=True):
        """
        Public method used to update the vcs status of a node.
        
        @param name filename belonging to this item (string)
        @param recursive flag indicating a recursive update (boolean)
        """
        item = self.findItem(name)
        if item:
            self.__updateVCSStatus(item, name, recursive)

    def removeItem(self, name):
        """
        Public method to remove a named item.
        
        @param name file or directory name of the item (string).
        """
        fname = os.path.basename(name)
        parentItem = self.findParentItemByName(0, name)[0]
        if parentItem == self.rootItem:
            parentIndex = QModelIndex()
        else:
            parentIndex = self.createIndex(parentItem.row(), 0, parentItem)
        childItem = self.findChildItem(fname, 0, parentItem)
        if childItem is not None:
            self.beginRemoveRows(parentIndex, childItem.row(), childItem.row())
            parentItem.removeChild(childItem)
            self.endRemoveRows()

    def repopulateItem(self, name):
        """
        Public method to repopulate an item.
        
        @param name name of the file relative to the project root (string)
        """
        itm = self.findItem(name)
        if itm is None:
            return

        if itm.isLazyPopulated():
            if not itm.isPopulated():
                # item is not populated yet, nothing to do
                return

            if itm.childCount():
                index = self.createIndex(itm.row(), 0, itm)
                self.beginRemoveRows(index, 0, itm.childCount() - 1)
                itm.removeChildren()
                self.endRemoveRows()
            Utilities.ModuleParser.resetParsedModule(
                os.path.join(self.project.ppath, name))

            self.populateItem(itm, True)

    def projectPropertiesChanged(self):
        """
        Public method to react on a change of the project properties.
        """
        # nothing to do for now
        return

    def changeVCSStates(self, statesList):
        """
        Public slot to record the (non normal) VCS states.
        
        @param statesList list of VCS state entries (list of strings) giving
            the states in the first column and the path relative to the project
            directory starting with the third column. The allowed status flags
            are:
            <ul>
                <li>"A" path was added but not yet comitted</li>
                <li>"M" path has local changes</li>
                <li>"O" path was removed</li>
                <li>"R" path was deleted and then re-added</li>
                <li>"U" path needs an update</li>
                <li>"Z" path contains a conflict</li>
                <li>" " path is back at normal</li>
            </ul>
        """
        statesList.sort()
        lastHead = ""
        itemCache = {}
        if len(statesList) == 1 and statesList[0] == '--RESET--':
            statesList = []
            for name in list(self.__vcsStatus.keys()):
                statesList.append(" {0}".format(name))

        for name in statesList:
            state = name[0]
            name = name[1:].strip()
            if state == ' ':
                if name in self.__vcsStatus:
                    del self.__vcsStatus[name]
            else:
                self.__vcsStatus[name] = state

            try:
                itm = itemCache[name]
            except KeyError:
                itm = self.findItem(name)
                if itm:
                    itemCache[name] = itm
            if itm:
                itm.setVcsState(state)
                itm.setVcsStatus(self.project.vcs.vcsName())
                index1 = self.createIndex(itm.row(), 0, itm)
                index2 = self.createIndex(itm.row(),
                                          self.rootItem.columnCount(), itm)
                self.dataChanged.emit(index1, index2)

            head, tail = os.path.split(name)
            if head != lastHead:
                if lastHead:
                    self.__changeParentsVCSState(lastHead, itemCache)
                lastHead = head
        if lastHead:
            self.__changeParentsVCSState(lastHead, itemCache)
        try:
            globalVcsStatus = sorted(self.__vcsStatus.values())[-1]
        except IndexError:
            globalVcsStatus = ' '
        self.vcsStateChanged.emit(globalVcsStatus)

    def __changeParentsVCSState(self, path, itemCache):
        """
        Private method to recursively change the parents VCS state.
        
        @param path pathname of parent item (string)
        @param itemCache reference to the item cache used to store
            references to named items
        """
        while path:
            try:
                itm = itemCache[path]
            except KeyError:
                itm = self.findItem(path)
                if itm:
                    itemCache[path] = itm
            if itm:
                state = " "
                for id_ in itm.children():
                    if state < id_.vcsState:
                        state = id_.vcsState
                if state != itm.vcsState:
                    itm.setVcsState(state)
                    index1 = self.createIndex(itm.row(), 0, itm)
                    index2 = self.createIndex(itm.row(),
                                              self.rootItem.columnCount(), itm)
                    self.dataChanged.emit(index1, index2)
            path, tail = os.path.split(path)

    def preferencesChanged(self):
        """
        Public method used to handle a change in preferences.
        """
        for code in list(self.colorNames.keys()):
            color = Preferences.getProjectBrowserColour(self.colorNames[code])
            if color.name() == self.itemBackgroundColors[code].name():
                continue

            self.itemBackgroundColors[code] = color

        color = Preferences.getProjectBrowserColour("Highlighted")
        if self.highLightColor.name() != color.name():
            self.highLightColor = color
Exemple #9
0
class _FileWatcher(QObject):
    """File watcher.

    QFileSystemWatcher notifies client about any change (file access mode, modification date, etc.)
    But, we need signal, only after file contents had been changed
    """
    modified = pyqtSignal(bool)
    removed = pyqtSignal(bool)

    def __init__(self, path):
        QObject.__init__(self)
        self._contents = None
        self._watcher = QFileSystemWatcher()
        self._timer = None
        self._path = path

        self._lastEmittedModifiedStatus = None
        self._lastEmittedRemovedStatus = None

        self.setPath(path)
        self.enable()

    def term(self):
        self.disable()

    def enable(self):
        """Enable signals from the watcher
        """
        self._watcher.fileChanged.connect(self._onFileChanged)

    def disable(self):
        """Disable signals from the watcher
        """
        self._watcher.fileChanged.disconnect(self._onFileChanged)
        self._stopTimer()

    def setContents(self, contents):
        """Set file contents. Watcher uses it to compare old and new contents of the file.
        """
        self._contents = contents
        # Qt File watcher may work incorrectly, if file was not existing, when it started
        if not self._watcher.files():
            self.setPath(self._path)
        self._lastEmittedModifiedStatus = None
        self._lastEmittedRemovedStatus = None

    def setPath(self, path):
        """Path had been changed or file had been created. Set new path
        """
        if self._watcher.files():
            self._watcher.removePaths(self._watcher.files())
        if path is not None and os.path.isfile(path):
            self._watcher.addPath(path)
        self._path = path
        self._lastEmittedModifiedStatus = None
        self._lastEmittedRemovedStatus = None

    def _emitModifiedStatus(self):
        """Emit self.modified signal with right status
        """
        isModified = self._contents != self._safeRead(self._path)
        if isModified != self._lastEmittedModifiedStatus:
            self.modified.emit(isModified)
            self._lastEmittedModifiedStatus = isModified

    def _emitRemovedStatus(self, isRemoved):
        """Emit 'removed', if status changed"""
        if isRemoved != self._lastEmittedRemovedStatus:
            self._lastEmittedRemovedStatus = isRemoved
            self.removed.emit(isRemoved)

    @pyqtSlot()
    def _onFileChanged(self):
        """File changed. Emit own signal, if contents changed
        """
        if os.path.exists(self._path):
            self._emitModifiedStatus()
        else:
            self._emitRemovedStatus(True)

        # Sometimes QFileSystemWatcher emits only 1 signal for 2 modifications
        # Check once more later
        self._startTimer()

    def _startTimer(self):
        """Init a timer.
        It is used for monitoring file after deletion.
        Git removes file, than restores it.
        """
        if self._timer is None:
            self._timer = QTimer()
            self._timer.setInterval(500)
            self._timer.timeout.connect(self._onCheckIfDeletedTimer)
        self._timer.start()

    def _stopTimer(self):
        """Stop timer, if exists
        """
        if self._timer is not None:
            self._timer.stop()

    @pyqtSlot()
    def _onCheckIfDeletedTimer(self):
        """Check, if file has been restored
        """
        if os.path.exists(self._path):
            self.setPath(self._path)  # restart Qt file watcher after file has been restored
            self._stopTimer()
            self._emitRemovedStatus(False)
            self._emitModifiedStatus()

    def _safeRead(self, path):
        """Read file. Ignore exceptions
        """
        try:
            with open(path, 'rb') as file:
                return file.read()
        except (OSError, IOError):
            return None
Exemple #10
0
class _FileWatcher(QObject):
    """File watcher.

    QFileSystemWatcher notifies client about any change (file access mode, modification date, etc.)
    But, we need signal, only after file contents had been changed
    """
    modified = pyqtSignal(bool)
    removed = pyqtSignal(bool)

    def __init__(self, path):
        QObject.__init__(self)
        self._contents = None
        self._watcher = QFileSystemWatcher(self)
        self._timer = None
        self._path = path

        self._lastEmittedModifiedStatus = None
        self._lastEmittedRemovedStatus = None

        self.setPath(path)
        self.enable()

    def term(self):
        self.disable()
        sip.delete(self)

    def enable(self):
        """Enable signals from the watcher
        """
        self._watcher.fileChanged.connect(self._onFileChanged)

    def disable(self):
        """Disable signals from the watcher
        """
        self._watcher.fileChanged.disconnect(self._onFileChanged)
        self._stopTimer()

    def setContents(self, contents):
        """Set file contents. Watcher uses it to compare old and new contents of the file.
        """
        self._contents = contents
        # Qt File watcher may work incorrectly, if file was not existing, when it started
        if not self._watcher.files():
            self.setPath(self._path)
        self._lastEmittedModifiedStatus = None
        self._lastEmittedRemovedStatus = None

    def setPath(self, path):
        """Path had been changed or file had been created. Set new path
        """
        if self._watcher.files():
            self._watcher.removePaths(self._watcher.files())
        if path is not None and os.path.isfile(path):
            self._watcher.addPath(path)
        self._path = path
        self._lastEmittedModifiedStatus = None
        self._lastEmittedRemovedStatus = None

    def _emitModifiedStatus(self):
        """Emit self.modified signal with right status
        """
        isModified = self._contents != self._safeRead(self._path)
        if isModified != self._lastEmittedModifiedStatus:
            self.modified.emit(isModified)
            self._lastEmittedModifiedStatus = isModified

    def _emitRemovedStatus(self, isRemoved):
        """Emit 'removed', if status changed"""
        if isRemoved != self._lastEmittedRemovedStatus:
            self._lastEmittedRemovedStatus = isRemoved
            self.removed.emit(isRemoved)

    @pyqtSlot()
    def _onFileChanged(self):
        """File changed. Emit own signal, if contents changed
        """
        if os.path.exists(self._path):
            self._emitModifiedStatus()
        else:
            self._emitRemovedStatus(True)

        # Sometimes QFileSystemWatcher emits only 1 signal for 2 modifications
        # Check once more later
        self._startTimer()

    def _startTimer(self):
        """Init a timer.
        It is used for monitoring file after deletion.
        Git removes file, than restores it.
        """
        if self._timer is None:
            self._timer = QTimer()
            self._timer.setInterval(500)
            self._timer.timeout.connect(self._onCheckIfDeletedTimer)
        self._timer.start()

    def _stopTimer(self):
        """Stop timer, if exists
        """
        if self._timer is not None:
            self._timer.stop()

    @pyqtSlot()
    def _onCheckIfDeletedTimer(self):
        """Check, if file has been restored
        """
        if os.path.exists(self._path):
            self.setPath(
                self._path
            )  # restart Qt file watcher after file has been restored
            self._stopTimer()
            self._emitRemovedStatus(False)
            self._emitModifiedStatus()

    def _safeRead(self, path):
        """Read file. Ignore exceptions
        """
        try:
            with open(path, 'rb') as file:
                return file.read()
        except (OSError, IOError):
            return None
Exemple #11
0
class ExternalEditor(QObject):

    """Class to simplify editing a text in an external editor.

    Attributes:
        _text: The current text before the editor is opened.
        _filename: The name of the file to be edited.
        _remove_file: Whether the file should be removed when the editor is
                      closed.
        _proc: The GUIProcess of the editor.
        _watcher: A QFileSystemWatcher to watch the edited file for changes.
                  Only set if watch=True.
        _content: The last-saved text of the editor.

    Signals:
        file_updated: The text in the edited file was updated.
                      arg: The new text.
        editing_finished: The editor process was closed.
    """

    file_updated = pyqtSignal(str)
    editing_finished = pyqtSignal()

    def __init__(self, parent=None, watch=False):
        super().__init__(parent)
        self._filename = None
        self._proc = None
        self._remove_file = None
        self._watcher = QFileSystemWatcher(parent=self) if watch else None
        self._content = None

    def _cleanup(self):
        """Clean up temporary files after the editor closed."""
        assert self._remove_file is not None

        watched_files = self._watcher.files() if self._watcher else []
        if watched_files:
            failed = self._watcher.removePaths(watched_files)
            if failed:
                log.procs.error("Failed to unwatch paths: {}".format(failed))

        if self._filename is None or not self._remove_file:
            # Could not create initial file.
            return

        try:
            if self._proc.exit_status() != QProcess.CrashExit:
                os.remove(self._filename)
        except OSError as e:
            # NOTE: Do not replace this with "raise CommandError" as it's
            # executed async.
            message.error("Failed to delete tempfile... ({})".format(e))

    @pyqtSlot(int, QProcess.ExitStatus)
    def _on_proc_closed(self, _exitcode, exitstatus):
        """Write the editor text into the form field and clean up tempfile.

        Callback for QProcess when the editor was closed.
        """
        log.procs.debug("Editor closed")
        if exitstatus != QProcess.NormalExit:
            # No error/cleanup here, since we already handle this in
            # on_proc_error.
            return
        # do a final read to make sure we don't miss the last signal
        self._on_file_changed(self._filename)
        self.editing_finished.emit()
        self._cleanup()

    @pyqtSlot(QProcess.ProcessError)
    def _on_proc_error(self, _err):
        self._cleanup()

    def edit(self, text, caret_position=None):
        """Edit a given text.

        Args:
            text: The initial text to edit.
            caret_position: The position of the caret in the text.
        """
        if self._filename is not None:
            raise ValueError("Already editing a file!")
        try:
            self._filename = self._create_tempfile(text, 'qutebrowser-editor-')
        except OSError as e:
            message.error("Failed to create initial file: {}".format(e))
            return

        self._remove_file = True

        line, column = self._calc_line_and_column(text, caret_position)
        self._start_editor(line=line, column=column)

    def backup(self):
        """Create a backup if the content has changed from the original."""
        if not self._content:
            return
        try:
            fname = self._create_tempfile(self._content,
                                          'qutebrowser-editor-backup-')
            message.info('Editor backup at {}'.format(fname))
        except OSError as e:
            message.error('Failed to create editor backup: {}'.format(e))

    def _create_tempfile(self, text, prefix):
        # Close while the external process is running, as otherwise systems
        # with exclusive write access (e.g. Windows) may fail to update
        # the file from the external editor, see
        # https://github.com/qutebrowser/qutebrowser/issues/1767
        with tempfile.NamedTemporaryFile(
                mode='w', prefix=prefix,
                encoding=config.val.editor.encoding,
                delete=False) as fobj:
            if text:
                fobj.write(text)
            return fobj.name

    @pyqtSlot(str)
    def _on_file_changed(self, path):
        try:
            with open(path, 'r', encoding=config.val.editor.encoding) as f:
                text = f.read()
        except OSError as e:
            # NOTE: Do not replace this with "raise CommandError" as it's
            # executed async.
            message.error("Failed to read back edited file: {}".format(e))
            return
        log.procs.debug("Read back: {}".format(text))
        if self._content != text:
            self._content = text
            self.file_updated.emit(text)

    def edit_file(self, filename):
        """Edit the file with the given filename."""
        self._filename = filename
        self._remove_file = False
        self._start_editor()

    def _start_editor(self, line=1, column=1):
        """Start the editor with the file opened as self._filename.

        Args:
            line: the line number to pass to the editor
            column: the column number to pass to the editor
        """
        self._proc = guiprocess.GUIProcess(what='editor', parent=self)
        self._proc.finished.connect(self._on_proc_closed)
        self._proc.error.connect(self._on_proc_error)
        editor = config.val.editor.command
        executable = editor[0]

        if self._watcher:
            ok = self._watcher.addPath(self._filename)
            if not ok:
                log.procs.error("Failed to watch path: {}"
                                .format(self._filename))
            self._watcher.fileChanged.connect(self._on_file_changed)

        args = [self._sub_placeholder(arg, line, column) for arg in editor[1:]]
        log.procs.debug("Calling \"{}\" with args {}".format(executable, args))
        self._proc.start(executable, args)

    def _calc_line_and_column(self, text, caret_position):
        r"""Calculate line and column numbers given a text and caret position.

        Both line and column are 1-based indexes, because that's what most
        editors use as line and column starting index.  By "most" we mean at
        least vim, nvim, gvim, emacs, atom, sublimetext, notepad++, brackets,
        visual studio, QtCreator and so on.

        To find the line we just count how many newlines there are before the
        caret and add 1.

        To find the column we calculate the difference between the caret and
        the last newline before the caret.

        For example in the text `aaa\nbb|bbb` (| represents the caret):
        caret_position = 6
        text[:caret_position] = `aaa\nbb`
        text[:caret_position].count('\n') = 1
        caret_position - text[:caret_position].rfind('\n') = 3

        Thus line, column = 2, 3, and the caret is indeed in the second
        line, third column

        Args:
            text: the text for which the numbers must be calculated
            caret_position: the position of the caret in the text, or None

        Return:
            A (line, column) tuple of (int, int)
        """
        if caret_position is None:
            return 1, 1
        line = text[:caret_position].count('\n') + 1
        column = caret_position - text[:caret_position].rfind('\n')
        return line, column

    def _sub_placeholder(self, arg, line, column):
        """Substitute a single placeholder.

        If the `arg` input to this function is a valid placeholder it will
        be substituted with the appropriate value, otherwise it will be left
        unchanged.

        Args:
            arg: an argument of editor.command.
            line: the previously-calculated line number for the text caret.
            column: the previously-calculated column number for the text caret.

        Return:
            The substituted placeholder or the original argument.
        """
        replacements = {
            '{}': self._filename,
            '{file}': self._filename,
            '{line}': str(line),
            '{line0}': str(line-1),
            '{column}': str(column),
            '{column0}': str(column-1)
        }

        for old, new in replacements.items():
            arg = arg.replace(old, new)

        return arg
Exemple #12
0
class ProjectBrowserModel(BrowserModel):
    """
    Class implementing the project browser model.
    
    @signal vcsStateChanged(str) emitted after the VCS state has changed
    """
    vcsStateChanged = pyqtSignal(str)
    
    def __init__(self, parent):
        """
        Constructor
        
        @param parent reference to parent object (Project.Project)
        """
        super(ProjectBrowserModel, self).__init__(parent, nopopulate=True)
        
        rootData = self.tr("Name")
        self.rootItem = BrowserItem(None, rootData)
        self.rootItem.itemData.append(self.tr("VCS Status"))
        
        self.progDir = None
        self.project = parent
        
        self.watchedItems = {}
        self.watcher = QFileSystemWatcher(self)
        self.watcher.directoryChanged.connect(self.directoryChanged)
        
        self.inRefresh = False
        
        self.projectBrowserTypes = {
            "SOURCES": ProjectBrowserSourceType,
            "FORMS": ProjectBrowserFormType,
            "RESOURCES": ProjectBrowserResourceType,
            "INTERFACES": ProjectBrowserInterfaceType,
            "TRANSLATIONS": ProjectBrowserTranslationType,
            "OTHERS": ProjectBrowserOthersType,
        }
        
        self.colorNames = {
            "A": "VcsAdded",
            "M": "VcsModified",
            "O": "VcsRemoved",
            "R": "VcsReplaced",
            "U": "VcsUpdate",
            "Z": "VcsConflict",
        }
        self.itemBackgroundColors = {
            " ": QColor(),
            "A": Preferences.getProjectBrowserColour(self.colorNames["A"]),
            "M": Preferences.getProjectBrowserColour(self.colorNames["M"]),
            "O": Preferences.getProjectBrowserColour(self.colorNames["O"]),
            "R": Preferences.getProjectBrowserColour(self.colorNames["R"]),
            "U": Preferences.getProjectBrowserColour(self.colorNames["U"]),
            "Z": Preferences.getProjectBrowserColour(self.colorNames["Z"]),
        }
        
        self.highLightColor = \
            Preferences.getProjectBrowserColour("Highlighted")
        # needed by preferencesChanged()
        
        self.vcsStatusReport = {}
    
    def data(self, index, role):
        """
        Public method to get data of an item.
        
        @param index index of the data to retrieve (QModelIndex)
        @param role role of data (Qt.ItemDataRole)
        @return requested data
        """
        if not index.isValid():
            return None
        
        if role == Qt.TextColorRole:
            if index.column() == 0:
                try:
                    return index.internalPointer().getTextColor()
                except AttributeError:
                    return None
        elif role == Qt.BackgroundColorRole:
            try:
                col = self.itemBackgroundColors[
                    index.internalPointer().vcsState]
                if col.isValid():
                    return col
                else:
                    return None
            except AttributeError:
                return None
            except KeyError:
                return None
        
        return BrowserModel.data(self, index, role)
    
    def populateItem(self, parentItem, repopulate=False):
        """
        Public method to populate an item's subtree.
        
        @param parentItem reference to the item to be populated
        @param repopulate flag indicating a repopulation (boolean)
        """
        if parentItem.type() == ProjectBrowserItemSimpleDirectory:
            return  # nothing to do
        elif parentItem.type() == ProjectBrowserItemDirectory:
            self.populateProjectDirectoryItem(parentItem, repopulate)
        elif parentItem.type() == ProjectBrowserItemFile:
            self.populateFileItem(parentItem, repopulate)
        else:
            BrowserModel.populateItem(self, parentItem, repopulate)

    def populateProjectDirectoryItem(self, parentItem, repopulate=False):
        """
        Public method to populate a directory item's subtree.
        
        @param parentItem reference to the directory item to be populated
        @param repopulate flag indicating a repopulation (boolean)
        """
        self._addWatchedItem(parentItem)
        
        qdir = QDir(parentItem.dirName())
        
        if Preferences.getUI("BrowsersListHiddenFiles"):
            filter = QDir.Filters(QDir.AllEntries |
                                  QDir.Hidden |
                                  QDir.NoDotAndDotDot)
        else:
            filter = QDir.Filters(QDir.AllEntries | QDir.NoDot | QDir.NoDotDot)
        entryInfoList = qdir.entryInfoList(filter)
        
        if len(entryInfoList) > 0:
            if repopulate:
                self.beginInsertRows(self.createIndex(
                    parentItem.row(), 0, parentItem),
                    0, len(entryInfoList) - 1)
            states = {}
            if self.project.vcs is not None:
                for f in entryInfoList:
                    fname = f.absoluteFilePath()
                    states[os.path.normcase(fname)] = 0
                dname = parentItem.dirName()
                self.project.vcs.clearStatusCache()
                states = self.project.vcs.vcsAllRegisteredStates(states, dname)
            
            for f in entryInfoList:
                if f.isDir():
                    node = ProjectBrowserDirectoryItem(
                        parentItem,
                        Utilities.toNativeSeparators(f.absoluteFilePath()),
                        parentItem.getProjectTypes()[0], False)
                else:
                    node = ProjectBrowserFileItem(
                        parentItem,
                        Utilities.toNativeSeparators(f.absoluteFilePath()),
                        parentItem.getProjectTypes()[0])
                if self.project.vcs is not None:
                    fname = f.absoluteFilePath()
                    if states[os.path.normcase(fname)] == \
                            self.project.vcs.canBeCommitted:
                        node.addVcsStatus(self.project.vcs.vcsName())
                        self.project.clearStatusMonitorCachedState(
                            f.absoluteFilePath())
                    else:
                        node.addVcsStatus(self.tr("local"))
                self._addItem(node, parentItem)
            if repopulate:
                self.endInsertRows()

    def projectClosed(self):
        """
        Public method called after a project has been closed.
        """
        self.__vcsStatus = {}
        
        self.watchedItems = {}
        watchedDirs = self.watcher.directories()
        if watchedDirs:
            self.watcher.removePaths(watchedDirs)
        
        self.rootItem.removeChildren()
        self.beginResetModel()
        self.endResetModel()
        
        # reset the module parser cache
        Utilities.ModuleParser.resetParsedModules()
        
    def projectOpened(self):
        """
        Public method used to populate the model after a project has been
        opened.
        """
        self.__vcsStatus = {}
        states = {}
        keys = list(self.projectBrowserTypes.keys())[:]
        
        if self.project.vcs is not None:
            for key in keys:
                for fn in self.project.pdata[key]:
                    states[os.path.normcase(
                        os.path.join(self.project.ppath, fn))] = 0
            
            self.project.vcs.clearStatusCache()
            states = self.project.vcs.vcsAllRegisteredStates(
                states, self.project.ppath)
        
        self.inRefresh = True
        for key in keys:
            # Show the entry in bold in the others browser to make it more
            # distinguishable
            if key == "OTHERS":
                bold = True
            else:
                bold = False
            
            if key == "SOURCES":
                sourceLanguage = self.project.pdata["PROGLANGUAGE"][0]
            else:
                sourceLanguage = ""
            
            for fn in self.project.pdata[key]:
                fname = os.path.join(self.project.ppath, fn)
                parentItem, dt = self.findParentItemByName(
                    self.projectBrowserTypes[key], fn)
                if os.path.isdir(fname):
                    itm = ProjectBrowserDirectoryItem(
                        parentItem, fname, self.projectBrowserTypes[key],
                        False, bold)
                else:
                    itm = ProjectBrowserFileItem(
                        parentItem, fname, self.projectBrowserTypes[key],
                        False, bold, sourceLanguage=sourceLanguage)
                self._addItem(itm, parentItem)
                if self.project.vcs is not None:
                    if states[os.path.normcase(fname)] == \
                            self.project.vcs.canBeCommitted:
                        itm.addVcsStatus(self.project.vcs.vcsName())
                    else:
                        itm.addVcsStatus(self.tr("local"))
                else:
                    itm.addVcsStatus("")
        self.inRefresh = False
        self.beginResetModel()
        self.endResetModel()

    def findParentItemByName(self, type_, name, dontSplit=False):
        """
        Public method to find an item given its name.
        
        <b>Note</b>: This method creates all necessary parent items, if they
        don't exist.
        
        @param type_ type of the item
        @param name name of the item (string)
        @param dontSplit flag indicating the name should not be split (boolean)
        @return reference to the item found and the new display name (string)
        """
        if dontSplit:
            pathlist = []
            pathlist.append(name)
            pathlist.append("ignore_me")
        else:
            pathlist = re.split(r'/|\\', name)
        
        if len(pathlist) > 1:
            olditem = self.rootItem
            path = self.project.ppath
            for p in pathlist[:-1]:
                itm = self.findChildItem(p, 0, olditem)
                path = os.path.join(path, p)
                if itm is None:
                    itm = ProjectBrowserSimpleDirectoryItem(
                        olditem, type_, p, path)
                    self.__addVCSStatus(itm, path)
                    if self.inRefresh:
                        self._addItem(itm, olditem)
                    else:
                        if olditem == self.rootItem:
                            oldindex = QModelIndex()
                        else:
                            oldindex = self.createIndex(
                                olditem.row(), 0, olditem)
                        self.addItem(itm, oldindex)
                else:
                    if type_ and type_ not in itm.getProjectTypes():
                        itm.addProjectType(type_)
                        index = self.createIndex(itm.row(), 0, itm)
                        self.dataChanged.emit(index, index)
                olditem = itm
            return (itm, pathlist[-1])
        else:
            return (self.rootItem, name)
    
    def findChildItem(self, text, column, parentItem=None):
        """
        Public method to find a child item given some text.
        
        @param text text to search for (string)
        @param column column to search in (integer)
        @param parentItem reference to parent item
        @return reference to the item found
        """
        if parentItem is None:
            parentItem = self.rootItem
        
        for itm in parentItem.children():
            if itm.data(column) == text:
                return itm
        
        return None
        
    def addNewItem(self, typeString, name, additionalTypeStrings=[]):
        """
        Public method to add a new item to the model.
        
        @param typeString string denoting the type of the new item (string)
        @param name name of the new item (string)
        @param additionalTypeStrings names of additional types (list of string)
        """
        # Show the entry in bold in the others browser to make it more
        # distinguishable
        if typeString == "OTHERS":
            bold = True
        else:
            bold = False
        
        fname = os.path.join(self.project.ppath, name)
        parentItem, dt = self.findParentItemByName(
            self.projectBrowserTypes[typeString], name)
        if parentItem == self.rootItem:
            parentIndex = QModelIndex()
        else:
            parentIndex = self.createIndex(parentItem.row(), 0, parentItem)
        if os.path.isdir(fname):
            itm = ProjectBrowserDirectoryItem(
                parentItem, fname, self.projectBrowserTypes[typeString],
                False, bold)
        else:
            if typeString == "SOURCES":
                sourceLanguage = self.project.pdata["PROGLANGUAGE"][0]
            else:
                sourceLanguage = ""
            itm = ProjectBrowserFileItem(
                parentItem, fname, self.projectBrowserTypes[typeString],
                False, bold, sourceLanguage=sourceLanguage)
        self.__addVCSStatus(itm, fname)
        if additionalTypeStrings:
            for additionalTypeString in additionalTypeStrings:
                type_ = self.projectBrowserTypes[additionalTypeString]
                itm.addProjectType(type_)
        self.addItem(itm, parentIndex)
    
    def renameItem(self, name, newFilename):
        """
        Public method to rename an item.
        
        @param name the old display name (string)
        @param newFilename new filename of the item (string)
        """
        itm = self.findItem(name)
        if itm is None:
            return
        
        index = self.createIndex(itm.row(), 0, itm)
        itm.setName(newFilename)
        self.dataChanged.emit(index, index)
        self.repopulateItem(newFilename)
    
    def findItem(self, name):
        """
        Public method to find an item given its name.
        
        @param name name of the item (string)
        @return reference to the item found
        """
        if QDir.isAbsolutePath(name):
            name = self.project.getRelativePath(name)
        pathlist = re.split(r'/|\\', name)
        if len(pathlist) > 0:
            olditem = self.rootItem
            for p in pathlist:
                itm = self.findChildItem(p, 0, olditem)
                if itm is None:
                    return None
                olditem = itm
            return itm
        else:
            return None
    
    def itemIndexByName(self, name):
        """
        Public method to find an item's index given its name.
        
        @param name name of the item (string)
        @return index of the item found (QModelIndex)
        """
        itm = self.findItem(name)
        if itm is None:
            index = QModelIndex()
        else:
            index = self.createIndex(itm.row(), 0, itm)
        return index
    
    def itemIndexByNameAndLine(self, name, lineno):
        """
        Public method to find an item's index given its name.
        
        @param name name of the item (string)
        @param lineno one based line number of the item (integer)
        @return index of the item found (QModelIndex)
        """
        index = QModelIndex()
        itm = self.findItem(name)
        if itm is not None and \
           isinstance(itm, ProjectBrowserFileItem):
            olditem = itm
            autoPopulate = Preferences.getProject("AutoPopulateItems")
            while itm is not None:
                if not itm.isPopulated():
                    if itm.isLazyPopulated() and autoPopulate:
                        self.populateItem(itm)
                    else:
                        break
                for child in itm.children():
                    try:
                        start, end = child.boundaries()
                        if end == -1:
                            end = 1000000   # assume end of file
                        if start <= lineno <= end:
                            itm = child
                            break
                    except AttributeError:
                        pass
                else:
                    itm = None
                if itm:
                    olditem = itm
            index = self.createIndex(olditem.row(), 0, olditem)
        
        return index
    
    def directoryChanged(self, path):
        """
        Public slot to handle the directoryChanged signal of the watcher.
        
        @param path path of the directory (string)
        """
        if path not in self.watchedItems:
            # just ignore the situation we don't have a reference to the item
            return
        
        if Preferences.getUI("BrowsersListHiddenFiles"):
            filter = QDir.Filters(QDir.AllEntries |
                                  QDir.Hidden |
                                  QDir.NoDotAndDotDot)
        else:
            filter = QDir.Filters(QDir.AllEntries | QDir.NoDot | QDir.NoDotDot)
        
        for itm in self.watchedItems[path]:
            oldCnt = itm.childCount()
            
            qdir = QDir(itm.dirName())
            
            entryInfoList = qdir.entryInfoList(filter)
            
            # step 1: check for new entries
            children = itm.children()
            for f in entryInfoList:
                fpath = Utilities.toNativeSeparators(f.absoluteFilePath())
                childFound = False
                for child in children:
                    if child.name() == fpath:
                        childFound = True
                        children.remove(child)
                        break
                if childFound:
                    continue
                
                cnt = itm.childCount()
                self.beginInsertRows(
                    self.createIndex(itm.row(), 0, itm), cnt, cnt)
                if f.isDir():
                    node = ProjectBrowserDirectoryItem(
                        itm,
                        Utilities.toNativeSeparators(f.absoluteFilePath()),
                        itm.getProjectTypes()[0],
                        False)
                else:
                    node = ProjectBrowserFileItem(
                        itm,
                        Utilities.toNativeSeparators(f.absoluteFilePath()),
                        itm.getProjectTypes()[0])
                self._addItem(node, itm)
                if self.project.vcs is not None:
                    self.project.vcs.clearStatusCache()
                    state = self.project.vcs.vcsRegisteredState(node.name())
                    if state == self.project.vcs.canBeCommitted:
                        node.addVcsStatus(self.project.vcs.vcsName())
                    else:
                        node.addVcsStatus(self.tr("local"))
                self.endInsertRows()
            
            # step 2: check for removed entries
            if len(entryInfoList) != itm.childCount():
                for row in range(oldCnt - 1, -1, -1):
                    child = itm.child(row)
                    childname = Utilities.fromNativeSeparators(child.name())
                    entryFound = False
                    for f in entryInfoList:
                        if f.absoluteFilePath() == childname:
                            entryFound = True
                            entryInfoList.remove(f)
                            break
                    if entryFound:
                        continue
                    
                    self._removeWatchedItem(child)
                    self.beginRemoveRows(
                        self.createIndex(itm.row(), 0, itm), row, row)
                    itm.removeChild(child)
                    self.endRemoveRows()
    
    def __addVCSStatus(self, item, name):
        """
        Private method used to set the vcs status of a node.
        
        @param item item to work on
        @param name filename belonging to this item (string)
        """
        if self.project.vcs is not None:
            state = self.project.vcs.vcsRegisteredState(name)
            if state == self.project.vcs.canBeCommitted:
                item.addVcsStatus(self.project.vcs.vcsName())
            else:
                item.addVcsStatus(self.tr("local"))
        else:
            item.addVcsStatus("")
    
    def __updateVCSStatus(self, item, name, recursive=True):
        """
        Private method used to update the vcs status of a node.
        
        @param item item to work on
        @param name filename belonging to this item (string)
        @keyparam recursive flag indicating a recursive update (boolean)
        """
        if self.project.vcs is not None:
            self.project.vcs.clearStatusCache()
            state = self.project.vcs.vcsRegisteredState(name)
            if state == self.project.vcs.canBeCommitted:
                item.setVcsStatus(self.project.vcs.vcsName())
            else:
                item.setVcsStatus(self.tr("local"))
            if recursive:
                name = os.path.dirname(name)
                parentItem = item.parent()
                if name and parentItem is not self.rootItem:
                    self.__updateVCSStatus(parentItem, name, recursive)
        else:
            item.setVcsStatus("")
        
        index = self.createIndex(item.row(), 0, item)
        self.dataChanged.emit(index, index)
    
    def updateVCSStatus(self, name, recursive=True):
        """
        Public method used to update the vcs status of a node.
        
        @param name filename belonging to this item (string)
        @param recursive flag indicating a recursive update (boolean)
        """
        item = self.findItem(name)
        if item:
            self.__updateVCSStatus(item, name, recursive)
    
    def removeItem(self, name):
        """
        Public method to remove a named item.
        
        @param name file or directory name of the item (string).
        """
        fname = os.path.basename(name)
        parentItem = self.findParentItemByName(0, name)[0]
        if parentItem == self.rootItem:
            parentIndex = QModelIndex()
        else:
            parentIndex = self.createIndex(parentItem.row(), 0, parentItem)
        childItem = self.findChildItem(fname, 0, parentItem)
        if childItem is not None:
            self.beginRemoveRows(parentIndex, childItem.row(), childItem.row())
            parentItem.removeChild(childItem)
            self.endRemoveRows()
    
    def repopulateItem(self, name):
        """
        Public method to repopulate an item.
        
        @param name name of the file relative to the project root (string)
        """
        itm = self.findItem(name)
        if itm is None:
            return
        
        if itm.isLazyPopulated():
            if not itm.isPopulated():
                # item is not populated yet, nothing to do
                return
            
            if itm.childCount():
                index = self.createIndex(itm.row(), 0, itm)
                self.beginRemoveRows(index, 0, itm.childCount() - 1)
                itm.removeChildren()
                self.endRemoveRows()
            Utilities.ModuleParser.resetParsedModule(
                os.path.join(self.project.ppath, name))
            
            self.populateItem(itm, True)
    
    def projectPropertiesChanged(self):
        """
        Public method to react on a change of the project properties.
        """
        # nothing to do for now
        return

    def changeVCSStates(self, statesList):
        """
        Public slot to record the (non normal) VCS states.
        
        @param statesList list of VCS state entries (list of strings) giving
            the states in the first column and the path relative to the project
            directory starting with the third column. The allowed status flags
            are:
            <ul>
                <li>"A" path was added but not yet comitted</li>
                <li>"M" path has local changes</li>
                <li>"O" path was removed</li>
                <li>"R" path was deleted and then re-added</li>
                <li>"U" path needs an update</li>
                <li>"Z" path contains a conflict</li>
                <li>" " path is back at normal</li>
            </ul>
        """
        statesList.sort()
        lastHead = ""
        itemCache = {}
        if len(statesList) == 1 and statesList[0] == '--RESET--':
            statesList = []
            for name in list(self.__vcsStatus.keys()):
                statesList.append(" {0}".format(name))
        
        for name in statesList:
            state = name[0]
            name = name[1:].strip()
            if state == ' ':
                if name in self.__vcsStatus:
                    del self.__vcsStatus[name]
            else:
                self.__vcsStatus[name] = state
            
            try:
                itm = itemCache[name]
            except KeyError:
                itm = self.findItem(name)
                if itm:
                    itemCache[name] = itm
            if itm:
                itm.setVcsState(state)
                itm.setVcsStatus(self.project.vcs.vcsName())
                index1 = self.createIndex(itm.row(), 0, itm)
                index2 = self.createIndex(
                    itm.row(), self.rootItem.columnCount(), itm)
                self.dataChanged.emit(index1, index2)
            
            head, tail = os.path.split(name)
            if head != lastHead:
                if lastHead:
                    self.__changeParentsVCSState(lastHead, itemCache)
                lastHead = head
        if lastHead:
            self.__changeParentsVCSState(lastHead, itemCache)
        try:
            globalVcsStatus = sorted(self.__vcsStatus.values())[-1]
        except IndexError:
            globalVcsStatus = ' '
        self.vcsStateChanged.emit(globalVcsStatus)

    def __changeParentsVCSState(self, path, itemCache):
        """
        Private method to recursively change the parents VCS state.
        
        @param path pathname of parent item (string)
        @param itemCache reference to the item cache used to store
            references to named items
        """
        while path:
            try:
                itm = itemCache[path]
            except KeyError:
                itm = self.findItem(path)
                if itm:
                    itemCache[path] = itm
            if itm:
                state = " "
                for id_ in itm.children():
                    if state < id_.vcsState:
                        state = id_.vcsState
                if state != itm.vcsState:
                    itm.setVcsState(state)
                    index1 = self.createIndex(itm.row(), 0, itm)
                    index2 = self.createIndex(
                        itm.row(), self.rootItem.columnCount(), itm)
                    self.dataChanged.emit(index1, index2)
            path, tail = os.path.split(path)
    
    def preferencesChanged(self):
        """
        Public method used to handle a change in preferences.
        """
        for code in list(self.colorNames.keys()):
            color = Preferences.getProjectBrowserColour(self.colorNames[code])
            if color.name() == self.itemBackgroundColors[code].name():
                continue
            
            self.itemBackgroundColors[code] = color
        
        color = Preferences.getProjectBrowserColour("Highlighted")
        if self.highLightColor.name() != color.name():
            self.highLightColor = color
Exemple #13
0
class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.setupUi(self)

        self.centralwidget.hide()

        self.parser = argparse.ArgumentParser("playground")
        self.parser.add_argument("-d", "--data-dir", type=str, help="data dir")
        self.parser.add_argument("-a", "--console-address", type=str, help="console address", default='localhost')
        self.parser.add_argument("-p", "--console-port", type=int, help="console port", default=2222)
        self.args = self.parser.parse_args()

        self.data_dir = self.args.data_dir
        self.console_port = self.args.console_port
        self.console_address = self.args.console_address

        self.project = CetechProject()

        self.api = QtConsoleAPI(self.console_address, self.console_port)

        self.setTabPosition(Qt.AllDockWidgetAreas, QTabWidget.North)

        self.script_editor_widget = ScriptEditor(project_manager=self.project, api=self.api)
        self.script_editor_dock_widget = QDockWidget(self)
        self.script_editor_dock_widget.setWindowTitle("Script editor")
        self.script_editor_dock_widget.hide()
        self.script_editor_dock_widget.setFeatures(QDockWidget.AllDockWidgetFeatures)
        self.script_editor_dock_widget.setWidget(self.script_editor_widget)
        self.addDockWidget(Qt.TopDockWidgetArea, self.script_editor_dock_widget)

        self.log_widget = LogWidget(self.api, self.script_editor_widget)
        self.log_dock_widget = QDockWidget(self)
        self.log_dock_widget.hide()
        self.log_dock_widget.setWindowTitle("Log")
        self.log_dock_widget.setWidget(self.log_widget)
        self.addDockWidget(Qt.BottomDockWidgetArea, self.log_dock_widget)

        self.assetb_widget = AssetBrowser()
        self.assetb_dock_widget = QDockWidget(self)
        self.assetb_dock_widget.hide()
        self.assetb_dock_widget.setWindowTitle("Asset browser")
        self.assetb_dock_widget.setFeatures(QDockWidget.AllDockWidgetFeatures)
        self.assetb_dock_widget.setWidget(self.assetb_widget)
        self.addDockWidget(Qt.LeftDockWidgetArea, self.assetb_dock_widget)

        self.recorded_event_widget = RecordEventWidget(api=self.api)
        self.recorded_event_dock_widget = QDockWidget(self)
        self.recorded_event_dock_widget.setWindowTitle("Recorded events")
        self.recorded_event_dock_widget.hide()
        self.recorded_event_dock_widget.setFeatures(QDockWidget.AllDockWidgetFeatures)
        self.recorded_event_dock_widget.setWidget(self.recorded_event_widget)
        self.addDockWidget(Qt.RightDockWidgetArea, self.recorded_event_dock_widget)

        #TODO bug #114 workaround. Disable create sub engine...
        if platform.system().lower() != 'darwin':
            self.ogl_widget = CetechWidget(self, self.api)
            self.ogl_dock = QDockWidget(self)
            self.ogl_dock.hide()
            self.ogl_dock.setWidget(self.ogl_widget)
            self.addDockWidget(Qt.TopDockWidgetArea, self.ogl_dock)

        self.tabifyDockWidget(self.assetb_dock_widget, self.log_dock_widget)

        self.assetb_widget.asset_clicked.connect(self.open_asset)

        self.file_watch = QFileSystemWatcher(self)
        self.file_watch.fileChanged.connect(self.file_changed)
        self.file_watch.directoryChanged.connect(self.dir_changed)

        self.build_file_watch = QFileSystemWatcher(self)
        self.build_file_watch.fileChanged.connect(self.build_file_changed)
        self.build_file_watch.directoryChanged.connect(self.build_dir_changed)

    def open_asset(self, path, ext):
        if self.script_editor_widget.support_ext(ext):
            self.script_editor_widget.open_file(path)
            self.script_editor_dock_widget.show()
            self.script_editor_dock_widget.focusWidget()

    def open_project(self, name, dir):
        self.project.open_project(name, dir)

        # self.project.run_cetech(build_type=CetechProject.BUILD_DEBUG, compile=True, continu=True, daemon=True)

        if platform.system().lower() == 'darwin':
            wid = None
        else:
            wid = self.ogl_widget.winId()

        self.project.run_cetech(build_type=CetechProject.BUILD_DEBUG, compile_=True, continue_=True,
                                wid=wid)


        self.api.start(QThread.LowPriority)

        self.assetb_widget.open_project(self.project.project_dir)
        self.assetb_dock_widget.show()
        self.log_dock_widget.show()

        #TODO bug #114 workaround. Disable create sub engine...
        if platform.system().lower() != 'darwin':
            self.ogl_dock.show()

        self.watch_project_dir()

    def watch_project_dir(self):
        files = self.file_watch.files()
        directories = self.file_watch.directories()

        if len(files):
            self.file_watch.removePaths(files)

        if len(directories):
            self.file_watch.removePaths(directories)

        files = self.build_file_watch.files()
        directories = self.build_file_watch.directories()

        if len(files):
            self.build_file_watch.removePaths(files)

        if len(directories):
            self.build_file_watch.removePaths(directories)

        files = []
        it = QDirIterator(self.project.source_dir, QDirIterator.Subdirectories)
        while it.hasNext():
            files.append(it.next())

        self.file_watch.addPaths(files)

        files = []
        it = QDirIterator(self.project.build_dir, QDirIterator.Subdirectories)
        while it.hasNext():
            files.append(it.next())

        self.build_file_watch.addPaths(files)

    def file_changed(self, path):
        self.api.compile_all()

    def dir_changed(self, path):
        self.watch_project_dir()

    def build_file_changed(self, path):
        self.api.autocomplete_list()

    def build_dir_changed(self, path):
        pass

    def open_script_editor(self):
        self.script_editor_dock_widget.show()

    def open_recorded_events(self):
        self.recorded_event_dock_widget.show()

    def closeEvent(self, evnt):
        self.api.disconnect()

        self.project.killall_process()

        self.statusbar.showMessage("Disconnecting ...")

        while self.api.connected:
            self.api.tick()

        self.statusbar.showMessage("Disconnected")

        evnt.accept()
class ExecuteOptionsPlugin(QWidget, Plugin):
    """
    Handles setting the various arguments for running.
    Signals:
        executableChanged(str): Path of the new executable is emitted when changed
        executableInfoChanged(ExecutableInfo): Emitted when the executable path is changed
        workingDirChanged(str): Path of the current directory is changed
    """
    executableChanged = pyqtSignal(str)
    executableInfoChanged = pyqtSignal(ExecutableInfo)
    workingDirChanged = pyqtSignal(str)
    useTestObjectsChanged = pyqtSignal(bool)

    def __init__(self, **kwds):
        super(ExecuteOptionsPlugin, self).__init__(**kwds)

        self._preferences.addInt("execute/maxRecentWorkingDirs",
                "Max recent working directories",
                10,
                1,
                50,
                "Set the maximum number of recent working directories that have been used.",
                )
        self._preferences.addInt("execute/maxRecentExes",
                "Max recent executables",
                10,
                1,
                50,
                "Set the maximum number of recent executables that have been used.",
                )
        self._preferences.addInt("execute/maxRecentArgs",
                "Max recent command line arguments",
                10,
                1,
                50,
                "Set the maximum number of recent command line arguments that have been used.",
                )
        self._preferences.addBool("execute/allowTestObjects",
                "Allow using test objects",
                False,
                "Allow using test objects by default",
                )
        self._preferences.addBool("execute/mpiEnabled",
                "Enable MPI by default",
                False,
                "Set the MPI checkbox on by default",
                )
        self._preferences.addString("execute/mpiArgs",
                "Default mpi command",
                "mpiexec -n 2",
                "Set the default MPI command to run",
                )
        self._preferences.addBool("execute/threadsEnabled",
                "Enable threads by default",
                False,
                "Set the threads checkbox on by default",
                )
        self._preferences.addString("execute/threadsArgs",
                "Default threads arguments",
                "--n-threads=2",
                "Set the default threads arguments",
                )

        self.all_exe_layout = WidgetUtils.addLayout(grid=True)
        self.setLayout(self.all_exe_layout)

        self.working_label = WidgetUtils.addLabel(None, self, "Working Directory")
        self.all_exe_layout.addWidget(self.working_label, 0, 0)
        self.choose_working_button = WidgetUtils.addButton(None, self, "Choose", self._chooseWorkingDir)
        self.all_exe_layout.addWidget(self.choose_working_button, 0, 1)
        self.working_line = WidgetUtils.addLineEdit(None, self, None, readonly=True)
        self.working_line.setText(os.getcwd())
        self.all_exe_layout.addWidget(self.working_line, 0, 2)

        self.exe_label = WidgetUtils.addLabel(None, self, "Executable")
        self.all_exe_layout.addWidget(self.exe_label, 1, 0)
        self.choose_exe_button = WidgetUtils.addButton(None, self, "Choose", self._chooseExecutable)
        self.all_exe_layout.addWidget(self.choose_exe_button, 1, 1)
        self.exe_line = WidgetUtils.addLineEdit(None, self, None, readonly=True)
        self.all_exe_layout.addWidget(self.exe_line, 1, 2)

        self.args_label = WidgetUtils.addLabel(None, self, "Extra Arguments")
        self.all_exe_layout.addWidget(self.args_label, 2, 0)
        self.args_line = WidgetUtils.addLineEdit(None, self, None)
        self.all_exe_layout.addWidget(self.args_line, 2, 2)

        self.test_label = WidgetUtils.addLabel(None, self, "Allow test objects")
        self.all_exe_layout.addWidget(self.test_label, 3, 0)
        self.test_checkbox = WidgetUtils.addCheckbox(None, self, "", self._allowTestObjects)
        self.test_checkbox.setChecked(self._preferences.value("execute/allowTestObjects"))
        self.all_exe_layout.addWidget(self.test_checkbox, 3, 1, alignment=Qt.AlignHCenter)

        self.mpi_label = WidgetUtils.addLabel(None, self, "Use MPI")
        self.all_exe_layout.addWidget(self.mpi_label, 4, 0)
        self.mpi_checkbox = WidgetUtils.addCheckbox(None, self, "", None)
        self.mpi_checkbox.setChecked(self._preferences.value("execute/mpiEnabled"))
        self.all_exe_layout.addWidget(self.mpi_checkbox, 4, 1, alignment=Qt.AlignHCenter)
        self.mpi_line = WidgetUtils.addLineEdit(None, self, None)
        self.mpi_line.setText(self._preferences.value("execute/mpiArgs"))
        self.mpi_line.cursorPositionChanged.connect(self._mpiLineCursorChanged)
        self.all_exe_layout.addWidget(self.mpi_line, 4, 2)

        self.threads_label = WidgetUtils.addLabel(None, self, "Use Threads")
        self.all_exe_layout.addWidget(self.threads_label, 5, 0)
        self.threads_checkbox = WidgetUtils.addCheckbox(None, self, "", None)
        self.threads_checkbox.setChecked(self._preferences.value("execute/threadsEnabled"))
        self.all_exe_layout.addWidget(self.threads_checkbox, 5, 1, alignment=Qt.AlignHCenter)
        self.threads_line = WidgetUtils.addLineEdit(None, self, None)
        self.threads_line.setText(self._preferences.value("execute/threadsArgs"))
        self.threads_line.cursorPositionChanged.connect(self._threadsLineCursorChanged)
        self.all_exe_layout.addWidget(self.threads_line, 5, 2)

        self.csv_label = WidgetUtils.addLabel(None, self, "Postprocessor CSV Output")
        self.all_exe_layout.addWidget(self.csv_label, 6, 0)
        self.csv_checkbox = WidgetUtils.addCheckbox(None, self, "", None)
        self.all_exe_layout.addWidget(self.csv_checkbox, 6, 1, alignment=Qt.AlignHCenter)
        self.csv_checkbox.setCheckState(Qt.Checked)

        self.recover_label = WidgetUtils.addLabel(None, self, "Recover")
        self.all_exe_layout.addWidget(self.recover_label, 7, 0)
        self.recover_checkbox = WidgetUtils.addCheckbox(None, self, "", None)
        self.all_exe_layout.addWidget(self.recover_checkbox, 7, 1, alignment=Qt.AlignHCenter)

        self._recent_exe_menu = None
        self._recent_working_menu = None
        self._recent_args_menu = None
        self._exe_watcher = QFileSystemWatcher()
        self._exe_watcher.fileChanged.connect(self.setExecutablePath)

        self._loading_dialog = QMessageBox(parent=self)
        self._loading_dialog.setWindowTitle("Loading executable")
        self._loading_dialog.setStandardButtons(QMessageBox.NoButton) # get rid of the OK button
        self._loading_dialog.setWindowModality(Qt.ApplicationModal)
        self._loading_dialog.setIcon(QMessageBox.Information)
        self._loading_dialog.setText("Loading executable")

        self.setup()

    def setExecutablePath(self, app_path):
        """
        The user select a new executable path.
        Input:
            app_path: The path of the executable.
        """
        if not app_path:
            return

        self._loading_dialog.setInformativeText(app_path)
        self._loading_dialog.show()
        self._loading_dialog.raise_()
        QApplication.processEvents()

        app_info = ExecutableInfo()
        app_info.setPath(app_path, self.test_checkbox.isChecked())

        QApplication.processEvents()

        if app_info.valid():
            self.exe_line.setText(app_path)
            self.executableInfoChanged.emit(app_info)
            self.executableChanged.emit(app_path)
            files = self._exe_watcher.files()
            if files:
                self._exe_watcher.removePaths(files)
            self._exe_watcher.addPath(app_path)
        self._updateRecentExe(app_path, not app_info.valid())
        self._loading_dialog.hide()

    def _chooseExecutable(self):
        """
        Open a dialog to allow the user to choose an executable.
        """
        #FIXME: QFileDialog seems to be a bit broken. Using
        # .setFilter() to filter only executable files doesn't
        # seem to work. Setting a QSortFilterProxyModel doesn't
        # seem to work either.
        # So just use the static method.
        exe_name, other = QFileDialog.getOpenFileName(self, "Chooose executable")
        self.setExecutablePath(exe_name)

    def _allowTestObjects(self):
        """
        Reload the ExecutableInfo based on whether we are allowing test objects or not.
        """
        self.useTestObjectsChanged.emit(self.test_checkbox.isChecked())
        self.setExecutablePath(self.exe_line.text())

    def _workingDirChanged(self):
        """
        Slot called when working directory changed.
        """
        working = str(self.working_line.text())
        self.setWorkingDir(working)

    def _chooseWorkingDir(self):
        """
        Open dialog to choose a current working directory.
        """
        dirname = QFileDialog.getExistingDirectory(self, "Choose directory")
        self.setWorkingDir(dirname)

    def setWorkingDir(self, dir_name):
        """
        Sets the working directory.
        Input:
            dir_name: The path of the working directory.
        """
        if not dir_name:
            return
        old_dirname = str(self.working_line.text())
        try:
            os.chdir(dir_name)
            self.working_line.setText(dir_name)
            if old_dirname != dir_name:
                self.workingDirChanged.emit(dir_name)
            self._updateRecentWorkingDir(dir_name)
        except OSError:
            mooseutils.mooseError("Invalid directory %s" % dir_name, dialog=True)
            self._updateRecentWorkingDir(dir_name, True)

    def _setExecutableArgs(self, args):
        """
        Set the executable arguments.
        Input:
            args: str: A string of all the arguments.
        """
        self.args_line.setText(args)

    def buildCommand(self, input_file):
        cmd, args = self.buildCommandWithNoInputFile()
        args.append("-i")
        args.append(os.path.relpath(input_file))
        return cmd, args

    def buildCommandWithNoInputFile(self):
        """
        Builds the full command line with arguments.
        Return: <string of command to run>, <list of arguments>
        """
        cmd = ""
        args = []
        if self.mpi_checkbox.isChecked():
            mpi_args = shlex.split(str(self.mpi_line.text()))
            if mpi_args:
                cmd = mpi_args[0]
                args = mpi_args[1:]
                args.append(str(self.exe_line.text()))

        if not cmd:
            cmd = str(self.exe_line.text())

        args += shlex.split(str(self.args_line.text()))

        if self.recover_checkbox.isChecked():
            args.append("--recover")

        if self.csv_checkbox.isChecked():
            #args.append("--no-color")
            args.append("Outputs/csv=true")

        if self.threads_checkbox.isChecked():
            args += shlex.split(str(self.threads_line.text()))

        return cmd, args

    def _updateRecentExe(self, path, remove=False):
        """
        Updates the recently used menu with the current executable
        """
        if self._recent_exe_menu:
            abs_path = os.path.normcase(os.path.abspath(path))
            if remove:
                self._recent_exe_menu.removeEntry(abs_path)
            else:
                self._recent_exe_menu.update(abs_path)

    def _updateRecentWorkingDir(self, path, remove=False):
        """
        Updates the recently used menu with the current executable
        """
        full_path = os.path.abspath(path)
        if self._recent_working_menu:
            if remove:
                self._recent_working_menu.removeEntry(full_path)
            else:
                self._recent_working_menu.update(full_path)

    def onPreferencesSaved(self):
        self._recent_args_menu.updateRecentlyOpened()
        self._recent_working_menu.updateRecentlyOpened()
        self._recent_exe_menu.updateRecentlyOpened()

    def addToMenu(self, menu):
        """
        Adds menu entries specific to the Arguments to the menubar.
        """
        workingMenu = menu.addMenu("Recent &working dirs")
        self._recent_working_menu = RecentlyUsedMenu(workingMenu,
                "execute/recentWorkingDirs",
                "execute/maxRecentWorkingDirs",
                20,
                )
        self._recent_working_menu.selected.connect(self.setWorkingDir)
        self._workingDirChanged()

        exeMenu = menu.addMenu("Recent &executables")
        self._recent_exe_menu = RecentlyUsedMenu(exeMenu,
                "execute/recentExes",
                "execute/maxRecentExes",
                20,
                )
        self._recent_exe_menu.selected.connect(self.setExecutablePath)

        argsMenu = menu.addMenu("Recent &arguments")
        self._recent_args_menu = RecentlyUsedMenu(argsMenu,
                "execute/recentArgs",
                "execute/maxRecentArgs",
                20,
                )
        self._recent_args_menu.selected.connect(self._setExecutableArgs)

    def clearRecentlyUsed(self):
        if self._recent_args_menu:
            self._recent_args_menu.clearValues()
            self._recent_working_menu.clearValues()
            self._recent_exe_menu.clearValues()
            self._workingDirChanged()

    def _mpiLineCursorChanged(self, old, new):
        self.mpi_checkbox.setChecked(True)

    def _threadsLineCursorChanged(self, old, new):
        self.threads_checkbox.setChecked(True)
Exemple #15
0
class MainWindow(QMainWindow):
    OnlineHelpUrl = QUrl("https://eth-cscs.github.io/serialbox2/sdb.html")

    def __init__(self):
        super().__init__()
        Logger.info("Setup main window")

        self.__input_serializer_data = SerializerData("Input Serializer")
        self.__input_stencil_data = StencilData(self.__input_serializer_data)

        self.__reference_serializer_data = SerializerData(
            "Reference Serializer")
        self.__reference_stencil_data = StencilData(
            self.__reference_serializer_data)

        self.__stencil_field_mapper = StencilFieldMapper(
            self.__input_stencil_data, self.__reference_stencil_data,
            GlobalConfig()["async"])

        self.__file_system_watcher = QFileSystemWatcher()
        self.__file_system_watcher.directoryChanged[str].connect(
            self.popup_reload_box)
        self.__file_system_watcher_last_modify = time()

        # Load from session?
        self.__session_manager = SessionManager()

        if GlobalConfig()["default_session"]:
            self.__session_manager.load_from_file()

        self.__session_manager.set_serializer_data(
            self.__input_serializer_data)
        self.__session_manager.set_serializer_data(
            self.__reference_serializer_data)

        # Setup GUI
        self.setWindowTitle('sdb - stencil debugger (%s)' %
                            Version().sdb_version())
        self.resize(1200, 600)

        if GlobalConfig()["center_window"]:
            self.center()

        if GlobalConfig()["move_window"]:
            self.move(GlobalConfig()["move_window"])

        self.setWindowIcon(Icon("logo-small.png"))
        self.init_menu_tool_bar()

        # Tabs
        self.__tab_highest_valid_state = TabState.Setup
        self.__widget_tab = QTabWidget(self)

        # Setup tab
        self.__widget_tab.addTab(
            SetupWindow(self, self.__input_serializer_data,
                        self.__reference_serializer_data), "Setup")

        # Stencil tab
        self.__widget_tab.addTab(
            StencilWindow(self, self.__stencil_field_mapper,
                          self.__input_stencil_data,
                          self.__reference_stencil_data), "Stencil")

        # Result tab
        self.__widget_tab.addTab(
            ResultWindow(self,
                         self.__widget_tab.widget(TabState.Stencil.value),
                         self.__stencil_field_mapper), "Result")

        # Error tab
        self.__widget_tab.addTab(ErrorWindow(self), "Error")

        self.__widget_tab.currentChanged.connect(self.switch_to_tab)

        self.__widget_tab.setTabEnabled(TabState.Setup.value, True)
        self.__widget_tab.setTabEnabled(TabState.Stencil.value, False)
        self.__widget_tab.setTabEnabled(TabState.Result.value, False)
        self.__widget_tab.setTabEnabled(TabState.Error.value, False)

        self.__widget_tab.setTabToolTip(TabState.Setup.value,
                                        "Setup Input and Refrence Serializer")
        self.__widget_tab.setTabToolTip(
            TabState.Stencil.value,
            "Set the stencil to compare and define the mapping of the fields")
        self.__widget_tab.setTabToolTip(TabState.Result.value,
                                        "View to comparison result")
        self.__widget_tab.setTabToolTip(
            TabState.Error.value,
            "Detailed error desscription of the current field")

        self.__tab_current_state = TabState.Setup
        self.set_tab_highest_valid_state(TabState.Setup)
        self.switch_to_tab(TabState.Setup)

        self.setCentralWidget(self.__widget_tab)

        # If the MainWindow is closed, kill all popup windows
        self.setAttribute(Qt.WA_DeleteOnClose)

        Logger.info("Starting main loop")
        self.show()

    def init_menu_tool_bar(self):
        Logger.info("Setup menu toolbar")

        action_exit = QAction("Exit", self)
        action_exit.setShortcut("Ctrl+Q")
        action_exit.setStatusTip("Exit the application")
        action_exit.triggered.connect(self.close)

        action_about = QAction("&About", self)
        action_about.setStatusTip("Show the application's About box")
        action_about.triggered.connect(self.popup_about_box)

        action_save_session = QAction(Icon("filesave.png"), "&Save", self)
        action_save_session.setStatusTip("Save current session")
        action_save_session.setShortcut("Ctrl+S")
        action_save_session.triggered.connect(self.save_session)

        action_open_session = QAction(Icon("fileopen.png"), "&Open", self)
        action_open_session.setShortcut("Ctrl+O")
        action_open_session.setStatusTip("Open session")
        action_open_session.triggered.connect(self.open_session)

        action_help = QAction(Icon("help.png"), "&Online Help", self)
        action_help.setStatusTip("Online Help")
        action_help.setToolTip("Online Help")
        action_help.triggered.connect(self.go_to_online_help)

        self.__action_continue = QAction(Icon("next_cursor.png"), "Continue",
                                         self)
        self.__action_continue.setStatusTip("Continue to next tab")
        self.__action_continue.triggered.connect(self.switch_to_next_tab)
        self.__action_continue.setEnabled(True)

        self.__action_back = QAction(Icon("prev_cursor.png"), "Back", self)
        self.__action_back.setStatusTip("Back to previous tab")
        self.__action_back.triggered.connect(self.switch_to_previous_tab)
        self.__action_back.setEnabled(False)

        self.__action_reload = QAction(Icon("step_in.png"), "Reload", self)
        self.__action_reload.setStatusTip(
            "Reload Input and Reference Serializer")
        self.__action_reload.setShortcut("Ctrl+R")
        self.__action_reload.triggered.connect(self.reload_serializer)
        self.__action_reload.setEnabled(False)

        self.__action_try_switch_to_error_tab = QAction(
            Icon("visualize.png"), "Detailed error description", self)
        self.__action_try_switch_to_error_tab.setStatusTip(
            "Detailed error desscription of the current field")
        self.__action_try_switch_to_error_tab.triggered.connect(
            self.try_switch_to_error_tab)
        self.__action_try_switch_to_error_tab.setEnabled(False)

        menubar = self.menuBar()
        menubar.setNativeMenuBar(False)
        self.statusBar()

        file_menu = menubar.addMenu('&File')
        file_menu.addAction(action_open_session)
        file_menu.addAction(action_save_session)
        file_menu.addAction(action_exit)

        edit_menu = menubar.addMenu('&Edit')
        edit_menu.addAction(self.__action_back)
        edit_menu.addAction(self.__action_continue)
        edit_menu.addAction(self.__action_reload)

        help_menu = menubar.addMenu('&Help')
        help_menu.addAction(action_about)
        help_menu.addAction(action_help)

        toolbar = self.addToolBar("Toolbar")
        toolbar.addAction(action_help)
        toolbar.addAction(action_open_session)
        toolbar.addAction(action_save_session)
        toolbar.addAction(self.__action_back)
        toolbar.addAction(self.__action_continue)
        toolbar.addAction(self.__action_reload)
        toolbar.addAction(self.__action_try_switch_to_error_tab)

    def center(self):
        qr = self.frameGeometry()
        cp = QDesktopWidget().availableGeometry().center()
        qr.moveCenter(cp)
        self.move(qr.topLeft())

    def closeEvent(self, event):
        self.__session_manager.update_serializer_data(
            self.__input_serializer_data)
        self.__session_manager.update_serializer_data(
            self.__reference_serializer_data)

        if GlobalConfig()["default_session"]:
            self.__session_manager.store_to_file()

    # ===----------------------------------------------------------------------------------------===
    #   TabWidgets
    # ==-----------------------------------------------------------------------------------------===

    def tab_widget(self, idx):
        return self.__widget_tab.widget(
            idx if not isinstance(idx, TabState) else idx.value)

    def switch_to_tab(self, tab):
        idx = tab.value if isinstance(tab, TabState) else tab
        if self.__tab_current_state == TabState(idx):
            return

        Logger.info("Switching to %s tab" % TabState(idx).name)
        self.__tab_current_state = TabState(idx)

        self.__widget_tab.setCurrentIndex(idx)
        self.tab_widget(idx).make_update()

        self.__action_try_switch_to_error_tab.setEnabled(
            TabState(idx) == TabState.Result)

        # Error tab is always disabled if not in "Error"
        self.__widget_tab.setTabEnabled(TabState.Error.value,
                                        TabState(idx) == TabState.Error)

        # First tab
        if idx == 0:
            self.__action_continue.setEnabled(True)
            self.__action_back.setEnabled(False)
        # Last tab
        elif idx == self.__widget_tab.count() - 1:
            self.__action_continue.setEnabled(False)
            self.__action_back.setEnabled(True)
        # Middle tab
        else:
            self.__action_continue.setEnabled(True)
            self.__action_back.setEnabled(True)

    def set_tab_highest_valid_state(self, state):
        """Set the state at which the data is valid i.e everything <= self.valid_tab_state is valid
        """
        self.__tab_highest_valid_state = state
        self.enable_tabs_according_to_tab_highest_valid_state()

    def enable_tabs_according_to_tab_highest_valid_state(self):
        """Enable/Disable tabs according to self.__tab_highest_valid_state
        """
        if self.__tab_highest_valid_state == TabState.Setup:
            self.__widget_tab.setTabEnabled(TabState.Setup.value, True)
            self.__widget_tab.setTabEnabled(TabState.Stencil.value, False)
            self.__widget_tab.setTabEnabled(TabState.Result.value, False)
            self.__widget_tab.setTabEnabled(TabState.Error.value, False)

            self.__action_try_switch_to_error_tab.setEnabled(False)

            watched_directories = self.__file_system_watcher.directories()
            if watched_directories:
                self.__file_system_watcher.removePaths(
                    self.__file_system_watcher.directories())

        elif self.__tab_highest_valid_state == TabState.Stencil:

            self.__file_system_watcher.addPath(
                self.__input_serializer_data.serializer.directory)
            self.__file_system_watcher.addPath(
                self.__reference_stencil_data.serializer.directory)

            self.__widget_tab.setTabEnabled(TabState.Setup.value, True)
            self.__widget_tab.setTabEnabled(TabState.Stencil.value, True)
            self.__widget_tab.setTabEnabled(TabState.Result.value, False)
            self.__widget_tab.setTabEnabled(TabState.Error.value, False)

            self.__widget_tab.widget(
                TabState.Stencil.value).initial_field_match()

            self.__action_reload.setEnabled(True)
            self.__action_try_switch_to_error_tab.setEnabled(False)

        elif self.__tab_highest_valid_state == TabState.Result:
            self.__widget_tab.setTabEnabled(TabState.Setup.value, True)
            self.__widget_tab.setTabEnabled(TabState.Stencil.value, True)
            self.__widget_tab.setTabEnabled(TabState.Result.value, True)
            self.__widget_tab.setTabEnabled(TabState.Error.value, True)

            self.__action_try_switch_to_error_tab.setEnabled(True)

        elif self.__tab_highest_valid_state == TabState.Error:
            self.__widget_tab.setTabEnabled(TabState.Setup.value, True)
            self.__widget_tab.setTabEnabled(TabState.Stencil.value, True)
            self.__widget_tab.setTabEnabled(TabState.Result.value, True)
            self.__action_try_switch_to_error_tab.setEnabled(False)

    def switch_to_next_tab(self):
        self.__widget_tab.currentWidget().make_continue()

    def switch_to_previous_tab(self):
        self.__widget_tab.currentWidget().make_back()

    def try_switch_to_error_tab(self):
        if self.__widget_tab.widget(
                TabState.Result.value).try_switch_to_error_tab():
            self.__widget_tab.setTabEnabled(TabState.Error.value, True)

    def error_window_set_result_data(self, result_data):
        self.__widget_tab.widget(
            TabState.Error.value).set_result_data(result_data)

    # ===----------------------------------------------------------------------------------------===
    #   PopupWidgets
    # ==-----------------------------------------------------------------------------------------===

    def popup_about_box(self):
        self.__about_widget = PopupAboutWidget(self)

    def popup_error_box(self, msg):
        Logger.error(
            msg.replace("<b>",
                        "").replace("</b>",
                                    "").replace("<br />",
                                                ":").replace("<br/>", ":"))

        msg_box = QMessageBox()
        msg_box.setWindowTitle("Error")
        msg_box.setIcon(QMessageBox.Critical)
        msg_box.setText(msg)
        msg_box.setStandardButtons(QMessageBox.Ok)
        reply = msg_box.exec_()  # Blocking

    def popup_reload_box(self, path):
        self.__file_system_watcher.blockSignals(True)
        reply = QMessageBox.question(
            self, "Reload serializer?",
            "The path \"%s\" has changed.\nDo want to reload the serializers?"
            % path, QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
        if reply == QMessageBox.Yes:
            self.reload_serializer()

        self.__file_system_watcher.blockSignals(False)

    # ===----------------------------------------------------------------------------------------===
    #   Session manager
    # ==-----------------------------------------------------------------------------------------===

    def save_session(self):
        Logger.info("Try saving current session")

        dialog = QFileDialog(self, "Save current session")
        dialog.setAcceptMode(QFileDialog.AcceptSave)
        dialog.setDefaultSuffix("json")
        dialog.setDirectory(getcwd())

        if not dialog.exec_():
            Logger.info("Abort saving current session")
            return

        filename = dialog.selectedFiles()
        self.__session_manager.update_serializer_data(
            self.__input_serializer_data)
        self.__session_manager.update_serializer_data(
            self.__reference_serializer_data)

        ret, msglist = self.__session_manager.store_to_file(filename[0])
        if not ret:
            self.popup_error_box("Failed to save configuration file: %s\n%s " %
                                 (filename[0], msglist[0]))

    def open_session(self):
        Logger.info("Try opening session")
        filename = QFileDialog.getOpenFileName(
            self, "Open Session", getcwd(), "JSON configuration (*.json)")[0]

        if filename is None or filename is "":
            Logger.info("Abort opening session")
            return

        ret, msglist = self.__session_manager.load_from_file(filename)
        if not ret:
            self.popup_error_box("Failed to load configuration file: %s\n%s " %
                                 (filename, msglist[0]))
        else:
            Logger.info("Successfully opened session")
            self.__session_manager.set_serializer_data(
                self.__input_serializer_data)
            self.__session_manager.set_serializer_data(
                self.__reference_serializer_data)
            self.switch_to_tab(TabState.Setup)

    @property
    def session_manager(self):
        return self.__session_manager

    # ===----------------------------------------------------------------------------------------===
    #   Reload Serializer
    # ==-----------------------------------------------------------------------------------------===

    def reload_serializer(self):
        Logger.info("Reloading serializers")
        try:
            self.__input_serializer_data.reload()
            self.__reference_serializer_data.reload()

            if self.__widget_tab.currentIndex() == TabState.Error.value:
                self.switch_to_tab(TabState.Result)

            self.__widget_tab.currentWidget().make_update()

        except RuntimeError as e:
            self.popup_error_box(str(e))
            self.set_tab_highest_valid_state(TabState.Setup)
            self.switch_to_tab(TabState.Setup)
            self.__widget_tab.currentWidget().make_update()

    # ===----------------------------------------------------------------------------------------===
    #   Online help
    # ==-----------------------------------------------------------------------------------------===

    def go_to_online_help(self):
        Logger.info("Opening online help")
        QDesktopServices.openUrl(MainWindow.OnlineHelpUrl)
Exemple #16
0
class QmyMainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)  #调用父类构造函数,创建窗体
        self.ui = Ui_MainWindow()  #创建UI对象
        self.ui.setupUi(self)  #构造UI界面

        self.ui.toolBox.setCurrentIndex(0)
        self.fileWatcher = QFileSystemWatcher()
        self.fileWatcher.directoryChanged.connect(self.do_directoryChanged)
        self.fileWatcher.fileChanged.connect(self.do_fileChanged)

##  ==============自定义功能函数========================

    def __showBtnInfo(self, btn):  ##显示按钮的text()和toolTip()
        self.ui.textEdit.appendPlainText("====" + btn.text())
        self.ui.textEdit.appendPlainText(btn.toolTip() + "\n")

##  ==============event处理函数==========================

##  ==========由connectSlotsByName()自动连接的槽函数============

    @pyqtSlot()  ##"选择文件"按钮
    def on_btnOpenFile_clicked(self):
        curDir = QDir.currentPath()  #获取当前路径
        aFile, filt = QFileDialog.getOpenFileName(self, "打开文件", curDir,
                                                  "所有文件(*.*)")
        self.ui.editFile.setText(aFile)

    @pyqtSlot()  ##"选择目录"按钮
    def on_btnOpenDir_clicked(self):
        curDir = QDir.currentPath()
        aDir = QFileDialog.getExistingDirectory(self, "选择一个目录", curDir,
                                                QFileDialog.ShowDirsOnly)
        self.ui.editDir.setText(aDir)

    @pyqtSlot()  ##"清空"按钮
    def on_btnClear_clicked(self):
        self.ui.textEdit.clear()

## =========QFile类 的静态函数===========

    @pyqtSlot()  ##类函数copy()
    def on_btnFile_copy_clicked(self):
        self.__showBtnInfo(self.sender())
        sous = self.ui.editFile.text().strip()  #源文件
        if sous == "":
            self.ui.textEdit.appendPlainText("请先选择一个文件")
            return

        fileInfo = QFileInfo(sous)
        newFile = fileInfo.path() + "/" + fileInfo.baseName(
        ) + "--副本." + fileInfo.suffix()
        if QFile.copy(sous, newFile):
            self.ui.textEdit.appendPlainText("源文件:" + sous)
            self.ui.textEdit.appendPlainText("复制为文件:" + newFile + "\n")
        else:
            self.ui.textEdit.appendPlainText("复制文件失败")

    @pyqtSlot()  ##类函数exists()
    def on_btnFile_exists_clicked(self):
        self.__showBtnInfo(self.sender())
        sous = self.ui.editFile.text().strip()  #源文件
        if QFile.exists(sous):
            self.ui.textEdit.appendPlainText("True \n")
        else:
            self.ui.textEdit.appendPlainText("False \n")

    @pyqtSlot()  ##类函数remove()
    def on_btnFile_remove_clicked(self):
        self.__showBtnInfo(self.sender())
        sous = self.ui.editFile.text().strip()  #源文件
        if sous == "":
            self.ui.textEdit.appendPlainText("请先选择一个文件")
            return

        ret = QMessageBox.question(self, "确认删除", "确定要删除这个文件吗\n\n" + sous)
        if (ret != QMessageBox.Yes):
            return

        if QFile.remove(sous):
            self.ui.textEdit.appendPlainText("成功删除文件:" + sous + "\n")
        else:
            self.ui.textEdit.appendPlainText("删除文件失败:" + sous + "\n")

    @pyqtSlot()  ##类函数rename()
    def on_btnFile_rename_clicked(self):
        self.__showBtnInfo(self.sender())
        sous = self.ui.editFile.text().strip()  #源文件
        if sous == "":
            self.ui.textEdit.appendPlainText("请先选择一个文件")
            return

        fileInfo = QFileInfo(sous)
        newFile = fileInfo.path() + "/" + fileInfo.baseName(
        ) + ".XZY"  #更改文件后缀为".XYZ"
        if QFile.rename(sous, newFile):
            self.ui.textEdit.appendPlainText("源文件:" + sous)
            self.ui.textEdit.appendPlainText("重命名为:" + newFile + "\n")
        else:
            self.ui.textEdit.appendPlainText("重命名文件失败\n")

## =========QFileInfo类===========

    @pyqtSlot()  ##absoluteFilePath()
    def on_btnInfo_absFilePath_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editFile.text())
        text = fileInfo.absoluteFilePath()
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##absolutePath()
    def on_btnInfo_absPath_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editFile.text())
        text = fileInfo.absolutePath()
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##fileName()
    def on_btnInfo_fileName_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editFile.text())
        text = fileInfo.fileName()
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##filePath()
    def on_btnInfo_filePath_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editFile.text())
        text = fileInfo.filePath()
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##size()
    def on_btnInfo_size_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editFile.text())
        btCount = fileInfo.size()  #字节数
        text = "%d Bytes" % btCount
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##path()
    def on_btnInfo_path_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editFile.text())
        text = fileInfo.path()
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##baseName()
    def on_btnInfo_baseName_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editFile.text())
        text = fileInfo.baseName()
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##completeBaseName()
    def on_btnInfo_baseName2_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editFile.text())
        text = fileInfo.completeBaseName()
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##suffix()
    def on_btnInfo_suffix_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editFile.text())
        text = fileInfo.suffix()
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##completeSuffix()
    def on_btnInfo_suffix2_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editFile.text())
        text = fileInfo.completeSuffix()
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##isDir()
    def on_btnInfo_isDir_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editDir.text())
        if fileInfo.isDir():
            self.ui.textEdit.appendPlainText("True \n")
        else:
            self.ui.textEdit.appendPlainText("False \n")

    @pyqtSlot()  ##isFile()
    def on_btnInfo_isFile_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editFile.text())
        if fileInfo.isFile():
            self.ui.textEdit.appendPlainText("True \n")
        else:
            self.ui.textEdit.appendPlainText("False \n")

    @pyqtSlot()  ##isExecutable()
    def on_btnInfo_isExec_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editFile.text())
        if fileInfo.isExecutable():
            self.ui.textEdit.appendPlainText("True \n")
        else:
            self.ui.textEdit.appendPlainText("False \n")

    @pyqtSlot()  ##birthTime() ,替代了过时的created()函数
    def on_btnInfo_birthTime_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editFile.text())
        dt = fileInfo.birthTime()  # QDateTime
        text = dt.toString("yyyy-MM-dd hh:mm:ss")
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##lastModified()
    def on_btnInfo_lastModified_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editFile.text())
        dt = fileInfo.lastModified()  # QDateTime
        text = dt.toString("yyyy-MM-dd hh:mm:ss")
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##lastRead()
    def on_btnInfo_lastRead_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editFile.text())
        dt = fileInfo.lastRead()  # QDateTime
        text = dt.toString("yyyy-MM-dd hh:mm:ss")
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##类函数exists()
    def on_btnInfo_exists_clicked(self):
        self.__showBtnInfo(self.sender())

        if QFileInfo.exists(self.ui.editFile.text()):
            self.ui.textEdit.appendPlainText("True \n")
        else:
            self.ui.textEdit.appendPlainText("False \n")

    @pyqtSlot()  ##接口函数exists()
    def on_btnInfo_exists2_clicked(self):
        self.__showBtnInfo(self.sender())
        fileInfo = QFileInfo(self.ui.editFile.text())
        if fileInfo.exists():
            self.ui.textEdit.appendPlainText("True \n")
        else:
            self.ui.textEdit.appendPlainText("False \n")

## ==================QDir类========================

    @pyqtSlot()  ##tempPath()
    def on_btnDir_tempPath_clicked(self):
        self.__showBtnInfo(self.sender())
        text = QDir.tempPath()
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##rootPath()
    def on_btnDir_rootPath_clicked(self):
        self.__showBtnInfo(self.sender())
        text = QDir.rootPath()
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##homePath()
    def on_btnDir_homePath_clicked(self):
        self.__showBtnInfo(self.sender())
        text = QDir.homePath()
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##drives()
    def on_btnDir_drives_clicked(self):
        self.__showBtnInfo(self.sender())
        strList = QDir.drives()  #QFileInfoList
        for line in strList:  #line 是QFileInfo类型
            self.ui.textEdit.appendPlainText(line.path())
        self.ui.textEdit.appendPlainText("")

    @pyqtSlot()  ##currentPath()
    def on_btnDir_curPath_clicked(self):
        self.__showBtnInfo(self.sender())
        text = QDir.currentPath()
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##setCurrent()
    def on_btnDir_setCurPath_clicked(self):
        self.__showBtnInfo(self.sender())

        curDir = QDir.currentPath()
        text = QFileDialog.getExistingDirectory(self, "选择一个目录", curDir,
                                                QFileDialog.ShowDirsOnly)
        QDir.setCurrent(text)
        self.ui.textEdit.appendPlainText("选择了一个目录作为当前目录:\n" + text + "\n")

    @pyqtSlot()  ##mkdir()
    def on_btnDir_mkdir_clicked(self):
        self.__showBtnInfo(self.sender())
        sous = self.ui.editDir.text().strip()
        if sous == "":
            self.ui.textEdit.appendPlainText("请先选择一个目录")
            return

        subDir = "subdir1"
        dirObj = QDir(sous)
        if dirObj.mkdir(subDir):
            self.ui.textEdit.appendPlainText("新建一个子目录: " + subDir + "\n")
        else:
            self.ui.textEdit.appendPlainText("创建目录失败\n")

    @pyqtSlot()  ##rmdir()
    def on_btnDir_rmdir_clicked(self):
        self.__showBtnInfo(self.sender())
        sous = self.ui.editDir.text().strip()
        if sous == "":
            self.ui.textEdit.appendPlainText("请先选择一个目录")
            return

        dirObj = QDir(sous)
        if dirObj.rmdir(sous):
            self.ui.textEdit.appendPlainText("成功删除所选目录\n" + sous + "\n")
        else:
            self.ui.textEdit.appendPlainText("删除目录失败,目录下必须为空\n")

    @pyqtSlot()  ##remove()
    def on_btnDir_remove_clicked(self):
        self.__showBtnInfo(self.sender())
        sous = self.ui.editFile.text().strip()
        if sous == "":
            self.ui.textEdit.appendPlainText("请先选择一个文件")
            return

        parDir = self.ui.editDir.text().strip()
        if parDir == "":
            self.ui.textEdit.appendPlainText("请先选择一个目录")
            return

        dirObj = QDir(parDir)
        if dirObj.remove(sous):
            self.ui.textEdit.appendPlainText("成功删除文件:\n" + sous + "\n")
        else:
            self.ui.textEdit.appendPlainText("删除文件失败\n")

    @pyqtSlot()  ##rename()
    def on_btnDir_rename_clicked(self):
        self.__showBtnInfo(self.sender())

        sous = self.ui.editFile.text()
        if sous == "":
            self.ui.textEdit.appendPlainText("请先选择一个文件")
            return

        parDir = self.ui.editDir.text().strip()
        if parDir == "":
            self.ui.textEdit.appendPlainText("请先选择一个目录")
            return

        dirObj = QDir(parDir)

        fileInfo = QFileInfo(sous)
        newFile = fileInfo.path() + "/" + fileInfo.baseName() + ".XYZ"

        if dirObj.rename(sous, newFile):
            self.ui.textEdit.appendPlainText("源文件:" + sous)
            self.ui.textEdit.appendPlainText("重命名为:" + newFile + "\n")
        else:
            self.ui.textEdit.appendPlainText("重命名文件失败\n")

    @pyqtSlot()  ##setPath(),改换QDir所指的目录
    def on_btnDir_setPath_clicked(self):
        self.__showBtnInfo(self.sender())
        curDir = QDir.currentPath()
        lastDir = QDir(curDir)
        self.ui.textEdit.appendPlainText("选择目录之前,QDir所指目录是:" +
                                         lastDir.absolutePath())

        aDir = QFileDialog.getExistingDirectory(self, "选择一个目录", curDir,
                                                QFileDialog.ShowDirsOnly)
        if aDir == "":
            return

        lastDir.setPath(aDir)
        self.ui.textEdit.appendPlainText("\n选择目录之后,QDir所指目录是:" +
                                         lastDir.absolutePath() + "\n")

    @pyqtSlot()  ##removeRecursively()
    def on_btnDir_removeALL_clicked(self):
        self.__showBtnInfo(self.sender())
        sous = self.ui.editDir.text().strip()
        if sous == "":
            self.ui.textEdit.appendPlainText("请先选择一个目录")
            return

        dirObj = QDir(sous)
        ret = QMessageBox.question(self, "确认删除", "确认删除目录下的所有文件及目录吗?\n" + sous)
        if ret != QMessageBox.Yes:
            return

        if dirObj.removeRecursively():
            self.ui.textEdit.appendPlainText("删除目录及文件成功\n")
        else:
            self.ui.textEdit.appendPlainText("删除目录及文件失败\n")

    @pyqtSlot()  ##absoluteFilePath()
    def on_btnDir_absFilePath_clicked(self):
        self.__showBtnInfo(self.sender())
        sous = self.ui.editFile.text()
        if sous == "":
            self.ui.textEdit.appendPlainText("请先选择一个文件")
            return

        parDir = QDir.currentPath()
        dirObj = QDir(parDir)
        text = dirObj.absoluteFilePath(sous)
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##absolutePath()
    def on_btnDir_absPath_clicked(self):
        self.__showBtnInfo(self.sender())
        sous = self.ui.editDir.text()
        if sous == "":
            self.ui.textEdit.appendPlainText("请先选择一个目录")
            return

        dirObj = QDir(sous)
        text = dirObj.absolutePath()
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##canonicalPath()
    def on_btnDir_canonPath_clicked(self):
        self.__showBtnInfo(self.sender())
        sous = self.ui.editDir.text()
        if sous == "":
            self.ui.textEdit.appendPlainText("请先选择一个目录")
            return

        dirObj = QDir(sous)
        text = dirObj.canonicalPath()
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##filePath()
    def on_btnDir_filePath_clicked(self):
        self.__showBtnInfo(self.sender())
        sous = self.ui.editFile.text()
        if sous == "":
            self.ui.textEdit.appendPlainText("请先选择一个文件")
            return

        parDir = QDir.currentPath()
        dirObj = QDir(parDir)
        text = dirObj.filePath(sous)
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##exists()
    def on_btnDir_exists_clicked(self):
        self.__showBtnInfo(self.sender())
        sous = self.ui.editDir.text()
        ##      if sous=="":
        ##         self.ui.textEdit.appendPlainText("请先选择一个目录")
        ##         return

        dirObj = QDir(sous)  #若sous为空,则使用其当前目录
        self.ui.textEdit.appendPlainText(dirObj.absolutePath() + "\n")
        if dirObj.exists():
            self.ui.textEdit.appendPlainText("True \n")
        else:
            self.ui.textEdit.appendPlainText("False \n")

    @pyqtSlot()  ##dirName()
    def on_btnDir_dirName_clicked(self):
        self.__showBtnInfo(self.sender())
        sous = self.ui.editDir.text()
        ##      if sous=="":
        ##         self.ui.textEdit.appendPlainText("请先选择一个目录")
        ##         return

        dirObj = QDir(sous)  #若sous为空,则使用其当前目录
        text = dirObj.dirName()
        self.ui.textEdit.appendPlainText(text + "\n")

    @pyqtSlot()  ##entryList()dirs
    def on_btnDir_listDir_clicked(self):
        self.__showBtnInfo(self.sender())
        sous = self.ui.editDir.text()
        dirObj = QDir(sous)  #若sous为空,则使用其当前目录
        strList = dirObj.entryList(QDir.Dirs | QDir.NoDotAndDotDot)
        self.ui.textEdit.appendPlainText("所选目录下的所有目录:")
        for line in strList:
            self.ui.textEdit.appendPlainText(line)
        self.ui.textEdit.appendPlainText("\n")

    @pyqtSlot()  ##entryList()files
    def on_btnDir_listFile_clicked(self):
        self.__showBtnInfo(self.sender())
        sous = self.ui.editDir.text()
        dirObj = QDir(sous)  #若sous为空,则使用其当前目录
        strList = dirObj.entryList(QDir.Files)
        self.ui.textEdit.appendPlainText("所选目录下的所有文件:")
        for line in strList:
            self.ui.textEdit.appendPlainText(line)
        self.ui.textEdit.appendPlainText("\n")

## ==========QFileSystemWatcher类===================

    @pyqtSlot()  ##addPath()添加监听目录
    def on_btnWatch_addDir_clicked(self):
        self.__showBtnInfo(self.sender())
        curDir = QDir.currentPath()
        aDir = QFileDialog.getExistingDirectory(self, "选择一个需要监听的目录", curDir,
                                                QFileDialog.ShowDirsOnly)
        self.fileWatcher.addPath(aDir)  #添加监听目录
        self.ui.textEdit.appendPlainText("添加的监听目录:")
        self.ui.textEdit.appendPlainText(aDir + "\n")

    @pyqtSlot()  ##addPaths()添加监听文件
    def on_btnWatch_addFiles_clicked(self):
        self.__showBtnInfo(self.sender())

        curDir = QDir.currentPath()
        fileList, flt = QFileDialog.getOpenFileNames(self, "选择需要监听的文件", curDir,
                                                     "所有文件 (*.*)")
        self.fileWatcher.addPaths(fileList)  #添加监听文件列表

        self.ui.textEdit.appendPlainText("添加的监听文件:")
        for lineStr in fileList:
            self.ui.textEdit.appendPlainText(lineStr)
        self.ui.textEdit.appendPlainText("")

    @pyqtSlot()  ##removePaths()移除所有监听的文件和目录
    def on_btnWatch_remove_clicked(self):
        self.__showBtnInfo(self.sender())
        self.ui.textEdit.appendPlainText("移除所有监听的目录和文件\n")

        dirList = self.fileWatcher.directories()
        self.fileWatcher.removePaths(dirList)

        fileList = self.fileWatcher.files()
        self.fileWatcher.removePaths(fileList)

    @pyqtSlot()  ##显示监听目录,directories()
    def on_btnWatch_dirs_clicked(self):
        self.__showBtnInfo(self.sender())
        strList = self.fileWatcher.directories()
        self.ui.textEdit.appendPlainText("正在监听的目录:")
        for line in strList:
            self.ui.textEdit.appendPlainText(line)
        self.ui.textEdit.appendPlainText("\n")

    @pyqtSlot()  ##显示监听文件,files()
    def on_btnWatch_files_clicked(self):
        self.__showBtnInfo(self.sender())
        strList = self.fileWatcher.files()
        self.ui.textEdit.appendPlainText("正在监听的文件:")
        for line in strList:
            self.ui.textEdit.appendPlainText(line)
        self.ui.textEdit.appendPlainText("\n")

##  =============自定义槽函数===============================

    def do_directoryChanged(self, path):  ##目录发生变化
        self.ui.textEdit.appendPlainText(path)
        self.ui.textEdit.appendPlainText("目录发生了变化\n")

    def do_fileChanged(self, path):  ##文件发生变化
        self.ui.textEdit.appendPlainText(path)
        self.ui.textEdit.appendPlainText("文件发生了变化\n")
class ExternalEditor(QObject):
    """Class to simplify editing a text in an external editor.

    Attributes:
        _text: The current text before the editor is opened.
        _filename: The name of the file to be edited.
        _remove_file: Whether the file should be removed when the editor is
                      closed.
        _proc: The GUIProcess of the editor.
        _watcher: A QFileSystemWatcher to watch the edited file for changes.
                  Only set if watch=True.
        _content: The last-saved text of the editor.

    Signals:
        file_updated: The text in the edited file was updated.
                      arg: The new text.
        editing_finished: The editor process was closed.
    """

    file_updated = pyqtSignal(str)
    editing_finished = pyqtSignal()

    def __init__(self, parent=None, watch=False):
        super().__init__(parent)
        self._filename = None
        self._proc = None
        self._remove_file = None
        self._watcher = QFileSystemWatcher(parent=self) if watch else None
        self._content = None

    def _cleanup(self, *, successful):
        """Clean up temporary files after the editor closed.

        Args:
            successful: Whether the editor exited successfully, i.e. the file can be
                        deleted.
        """
        assert self._remove_file is not None
        if (self._watcher is not None and not sip.isdeleted(self._watcher)
                and self._watcher.files()):
            failed = self._watcher.removePaths(self._watcher.files())
            if failed:
                log.procs.error("Failed to unwatch paths: {}".format(failed))

        if self._filename is None or not self._remove_file:
            # Could not create initial file.
            return

        assert self._proc is not None

        if successful:
            try:
                os.remove(self._filename)
            except OSError as e:
                # NOTE: Do not replace this with "raise CommandError" as it's
                # executed async.
                message.error("Failed to delete tempfile... ({})".format(e))
        else:
            message.info(
                f"Keeping file {self._filename} as the editor process exited "
                "abnormally")

    @pyqtSlot(int, QProcess.ExitStatus)
    def _on_proc_closed(self, _exitcode, exitstatus):
        """Write the editor text into the form field and clean up tempfile.

        Callback for QProcess when the editor was closed.
        """
        if sip.isdeleted(self):  # pragma: no cover
            log.procs.debug("Ignoring _on_proc_closed for deleted editor")
            return

        log.procs.debug("Editor closed")
        if exitstatus != QProcess.NormalExit:
            # No error/cleanup here, since we already handle this in
            # on_proc_error.
            return

        # do a final read to make sure we don't miss the last signal
        assert self._proc is not None
        self._on_file_changed(self._filename)
        self.editing_finished.emit()
        self._cleanup(successful=self._proc.outcome.was_successful())

    @pyqtSlot(QProcess.ProcessError)
    def _on_proc_error(self, _err):
        self._cleanup(successful=False)

    def edit(self, text, caret_position=None):
        """Edit a given text.

        Args:
            text: The initial text to edit.
            caret_position: The position of the caret in the text.
        """
        if self._filename is not None:
            raise ValueError("Already editing a file!")
        try:
            self._filename = self._create_tempfile(text, 'qutebrowser-editor-')
        except (OSError, UnicodeEncodeError) as e:
            message.error("Failed to create initial file: {}".format(e))
            return

        self._remove_file = True

        line, column = self._calc_line_and_column(text, caret_position)
        self._start_editor(line=line, column=column)

    def backup(self):
        """Create a backup if the content has changed from the original."""
        if not self._content:
            return
        try:
            fname = self._create_tempfile(self._content,
                                          'qutebrowser-editor-backup-')
            message.info('Editor backup at {}'.format(fname))
        except OSError as e:
            message.error('Failed to create editor backup: {}'.format(e))

    def _create_tempfile(self, text, prefix):
        # Close while the external process is running, as otherwise systems
        # with exclusive write access (e.g. Windows) may fail to update
        # the file from the external editor, see
        # https://github.com/qutebrowser/qutebrowser/issues/1767
        with tempfile.NamedTemporaryFile(mode='w',
                                         prefix=prefix,
                                         encoding=config.val.editor.encoding,
                                         delete=False) as fobj:
            if text:
                fobj.write(text)
            return fobj.name

    @pyqtSlot(str)
    def _on_file_changed(self, path):
        try:
            with open(path, 'r', encoding=config.val.editor.encoding) as f:
                text = f.read()
        except OSError as e:
            # NOTE: Do not replace this with "raise CommandError" as it's
            # executed async.
            message.error("Failed to read back edited file: {}".format(e))
            return
        log.procs.debug("Read back: {}".format(text))
        if self._content != text:
            self._content = text
            self.file_updated.emit(text)

    def edit_file(self, filename):
        """Edit the file with the given filename."""
        if not os.path.exists(filename):
            with open(filename, 'w', encoding='utf-8'):
                pass
        self._filename = filename
        self._remove_file = False
        self._start_editor()

    def _start_editor(self, line=1, column=1):
        """Start the editor with the file opened as self._filename.

        Args:
            line: the line number to pass to the editor
            column: the column number to pass to the editor
        """
        self._proc = guiprocess.GUIProcess(what='editor', parent=self)
        self._proc.finished.connect(self._on_proc_closed)
        self._proc.error.connect(self._on_proc_error)
        editor = config.val.editor.command
        executable = editor[0]

        if self._watcher:
            assert self._filename is not None
            ok = self._watcher.addPath(self._filename)
            if not ok:
                log.procs.error("Failed to watch path: {}".format(
                    self._filename))
            self._watcher.fileChanged.connect(  # type: ignore[attr-defined]
                self._on_file_changed)

        args = [self._sub_placeholder(arg, line, column) for arg in editor[1:]]
        log.procs.debug("Calling \"{}\" with args {}".format(executable, args))
        self._proc.start(executable, args)

    def _calc_line_and_column(self, text, caret_position):
        r"""Calculate line and column numbers given a text and caret position.

        Both line and column are 1-based indexes, because that's what most
        editors use as line and column starting index.  By "most" we mean at
        least vim, nvim, gvim, emacs, atom, sublimetext, notepad++, brackets,
        visual studio, QtCreator and so on.

        To find the line we just count how many newlines there are before the
        caret and add 1.

        To find the column we calculate the difference between the caret and
        the last newline before the caret.

        For example in the text `aaa\nbb|bbb` (| represents the caret):
        caret_position = 6
        text[:caret_position] = `aaa\nbb`
        text[:caret_position].count('\n') = 1
        caret_position - text[:caret_position].rfind('\n') = 3

        Thus line, column = 2, 3, and the caret is indeed in the second
        line, third column

        Args:
            text: the text for which the numbers must be calculated
            caret_position: the position of the caret in the text, or None

        Return:
            A (line, column) tuple of (int, int)
        """
        if caret_position is None:
            return 1, 1
        line = text[:caret_position].count('\n') + 1
        column = caret_position - text[:caret_position].rfind('\n')
        return line, column

    def _sub_placeholder(self, arg, line, column):
        """Substitute a single placeholder.

        If the `arg` input to this function is a valid placeholder it will
        be substituted with the appropriate value, otherwise it will be left
        unchanged.

        Args:
            arg: an argument of editor.command.
            line: the previously-calculated line number for the text caret.
            column: the previously-calculated column number for the text caret.

        Return:
            The substituted placeholder or the original argument.
        """
        replacements = {
            '{}': self._filename,
            '{file}': self._filename,
            '{line}': str(line),
            '{line0}': str(line - 1),
            '{column}': str(column),
            '{column0}': str(column - 1)
        }

        for old, new in replacements.items():
            arg = arg.replace(old, new)

        return arg
Exemple #18
0
class Editor(CodeEditor, ComponentMixin):

    name = 'Code Editor'

    # This signal is emitted whenever the currently-open file changes and
    # autoreload is enabled.
    triggerRerender = pyqtSignal(bool)
    sigFilenameChanged = pyqtSignal(str)

    preferences = Parameter.create(name='Preferences',
                                   children=[{
                                       'name': 'Font size',
                                       'type': 'int',
                                       'value': 12
                                   }, {
                                       'name': 'Autoreload',
                                       'type': 'bool',
                                       'value': False
                                   }, {
                                       'name': 'Autoreload delay',
                                       'type': 'int',
                                       'value': 50
                                   }, {
                                       'name':
                                       'Autoreload: watch imported modules',
                                       'type': 'bool',
                                       'value': False
                                   }, {
                                       'name': 'Line wrap',
                                       'type': 'bool',
                                       'value': False
                                   }, {
                                       'name':
                                       'Color scheme',
                                       'type':
                                       'list',
                                       'values':
                                       ['Spyder', 'Monokai', 'Zenburn'],
                                       'value':
                                       'Spyder'
                                   }])

    EXTENSIONS = 'py'

    def __init__(self, parent=None):

        self._watched_file = None

        super(Editor, self).__init__(parent)
        ComponentMixin.__init__(self)

        self.setup_editor(linenumbers=True,
                          markers=True,
                          edge_line=False,
                          tab_mode=False,
                          show_blanks=True,
                          font=QFontDatabase.systemFont(
                              QFontDatabase.FixedFont),
                          language='Python',
                          filename='')

        self._actions =  \
                {'File' : [QAction(icon('new'),
                                  'New',
                                  self,
                                  shortcut='ctrl+N',
                                  triggered=self.new),
                          QAction(icon('open'),
                                  'Open',
                                  self,
                                  shortcut='ctrl+O',
                                  triggered=self.open),
                          QAction(icon('save'),
                                  'Save',
                                  self,
                                  shortcut='ctrl+S',
                                  triggered=self.save),
                          QAction(icon('save_as'),
                                  'Save as',
                                  self,
                                  shortcut='ctrl+shift+S',
                                  triggered=self.save_as),
                          QAction(icon('autoreload'),
                                  'Automatic reload and preview',
                                  self,triggered=self.autoreload,
                                  checkable=True,
                                  checked=False,
                                  objectName='autoreload'),
                          ]}

        for a in self._actions.values():
            self.addActions(a)

        self._fixContextMenu()

        # autoreload support
        self._file_watcher = QFileSystemWatcher(self)
        # we wait for 50ms after a file change for the file to be written completely
        self._file_watch_timer = QTimer(self)
        self._file_watch_timer.setInterval(
            self.preferences['Autoreload delay'])
        self._file_watch_timer.setSingleShot(True)
        self._file_watcher.fileChanged.connect(
            lambda val: self._file_watch_timer.start())
        self._file_watch_timer.timeout.connect(self._file_changed)

        self.updatePreferences()

    def _fixContextMenu(self):

        menu = self.menu

        menu.removeAction(self.run_cell_action)
        menu.removeAction(self.run_cell_and_advance_action)
        menu.removeAction(self.run_selection_action)
        menu.removeAction(self.re_run_last_cell_action)

    def updatePreferences(self, *args):

        self.set_color_scheme(self.preferences['Color scheme'])

        font = self.font()
        font.setPointSize(self.preferences['Font size'])
        self.set_font(font)

        self.findChild(QAction, 'autoreload') \
            .setChecked(self.preferences['Autoreload'])

        self._file_watch_timer.setInterval(
            self.preferences['Autoreload delay'])

        self.toggle_wrap_mode(self.preferences['Line wrap'])

        self._clear_watched_paths()
        self._watch_paths()

    def confirm_discard(self):

        if self.modified:
            rv = confirm(
                self, 'Please confirm',
                'Current document is not saved - do you want to continue?')
        else:
            rv = True

        return rv

    def new(self):

        if not self.confirm_discard(): return

        self.set_text('')
        self.filename = ''
        self.reset_modified()

    def open(self):

        if not self.confirm_discard(): return

        curr_dir = Path(self.filename).abspath().dirname()
        fname = get_open_filename(self.EXTENSIONS, curr_dir)
        if fname != '':
            self.load_from_file(fname)

    def load_from_file(self, fname):

        self.set_text_from_file(fname)
        self.filename = fname
        self.reset_modified()

    def save(self):

        if self._filename != '':

            if self.preferences['Autoreload']:
                self._file_watcher.blockSignals(True)
                self._file_watch_timer.stop()

            with open(self._filename, 'w') as f:
                f.write(self.toPlainText())

            if self.preferences['Autoreload']:
                self._file_watcher.blockSignals(False)
                self.triggerRerender.emit(True)

            self.reset_modified()

        else:
            self.save_as()

    def save_as(self):

        fname = get_save_filename(self.EXTENSIONS)
        if fname != '':
            with open(fname, 'w') as f:
                f.write(self.toPlainText())
                self.filename = fname

            self.reset_modified()

    def _update_filewatcher(self):
        if self._watched_file and (self._watched_file != self.filename
                                   or not self.preferences['Autoreload']):
            self._clear_watched_paths()
            self._watched_file = None
        if self.preferences[
                'Autoreload'] and self.filename and self.filename != self._watched_file:
            self._watched_file = self._filename
            self._watch_paths()

    @property
    def filename(self):
        return self._filename

    @filename.setter
    def filename(self, fname):
        self._filename = fname
        self._update_filewatcher()
        self.sigFilenameChanged.emit(fname)

    def _clear_watched_paths(self):
        paths = self._file_watcher.files()
        if paths:
            self._file_watcher.removePaths(paths)

    def _watch_paths(self):
        if Path(self._filename).exists():
            self._file_watcher.addPath(self._filename)
            if self.preferences['Autoreload: watch imported modules']:
                module_paths = self.get_imported_module_paths(self._filename)
                if module_paths:
                    self._file_watcher.addPaths(module_paths)

    # callback triggered by QFileSystemWatcher
    def _file_changed(self):
        # neovim writes a file by removing it first so must re-add each time
        self._watch_paths()
        self.set_text_from_file(self._filename)
        self.triggerRerender.emit(True)

    # Turn autoreload on/off.
    def autoreload(self, enabled):
        self.preferences['Autoreload'] = enabled
        self._update_filewatcher()

    def reset_modified(self):

        self.document().setModified(False)

    @property
    def modified(self):

        return self.document().isModified()

    def saveComponentState(self, store):

        if self.filename != '':
            store.setValue(self.name + '/state', self.filename)

    def restoreComponentState(self, store):

        filename = store.value(self.name + '/state')

        if filename and self.filename == '':
            try:
                self.load_from_file(filename)
            except IOError:
                self._logger.warning(f'could not open {filename}')

    def get_imported_module_paths(self, module_path):

        finder = ModuleFinder([os.path.dirname(module_path)])
        imported_modules = []

        try:
            finder.run_script(module_path)
        except SyntaxError as err:
            self._logger.warning(f'Syntax error in {module_path}: {err}')
        except Exception as err:
            self._logger.warning(
                f'Cannot determine imported modules in {module_path}: {type(err).__name__} {err}'
            )
        else:
            for module_name, module in finder.modules.items():
                if module_name != '__main__':
                    path = getattr(module, '__file__', None)
                    if path is not None and os.path.isfile(path):
                        imported_modules.append(path)

        return imported_modules