class EditorPidWatcher(QObject):

    appeared = pyqtSignal()

    def __init__(self, directory, parent=None):
        super().__init__(parent)
        self._pidfile = directory / 'editor_pid'
        self._watcher = QFileSystemWatcher(self)
        self._watcher.addPath(str(directory))
        self._watcher.directoryChanged.connect(self._check_update)
        self.has_pidfile = False
        self._check_update()

    @pyqtSlot()
    def _check_update(self):
        if self.has_pidfile:
            return

        if self._pidfile.check():
            if self._pidfile.read():
                self.has_pidfile = True
                self.appeared.emit()
            else:
                self._watcher.addPath(str(self._pidfile))

    def manual_check(self):
        return self._pidfile.check()
def test_killed_command(qtbot, tmpdir, py_proc, runner):
    text_file = tmpdir / 'text'
    text_file.write('This is text')

    pidfile = tmpdir / 'pid'
    watcher = QFileSystemWatcher()
    watcher.addPath(str(tmpdir))

    env = {'QUTE_TEXT': str(text_file)}
    cmd, args = py_proc(r"""
        import os
        import time
        import sys

        # We can't use QUTE_FIFO to transmit the PID because that wouldn't work
        # on Windows, where QUTE_FIFO is only monitored after the script has
        # exited.

        with open(sys.argv[1], 'w') as f:
            f.write(str(os.getpid()))

        time.sleep(30)
    """)
    args.append(str(pidfile))

    with qtbot.waitSignal(watcher.directoryChanged, timeout=10000):
        runner.run(cmd, *args, env=env)

    # Make sure the PID was written to the file, not just the file created
    time.sleep(0.5)

    with qtbot.waitSignal(runner.finished):
        os.kill(int(pidfile.read()), signal.SIGTERM)

    assert not text_file.exists()
Exemple #3
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 #4
0
def test_killed_command(qtbot, tmpdir, py_proc, runner):
    text_file = tmpdir / 'text'
    text_file.write('This is text')

    pidfile = tmpdir / 'pid'
    watcher = QFileSystemWatcher()
    watcher.addPath(str(tmpdir))

    env = {'QUTE_TEXT': str(text_file)}
    cmd, args = py_proc(r"""
        import os
        import time
        import sys

        # We can't use QUTE_FIFO to transmit the PID because that wouldn't work
        # on Windows, where QUTE_FIFO is only monitored after the script has
        # exited.

        with open(sys.argv[1], 'w') as f:
            f.write(str(os.getpid()))

        time.sleep(30)
    """)
    args.append(str(pidfile))

    with qtbot.waitSignal(watcher.directoryChanged, timeout=10000):
        runner.run(cmd, *args, env=env)

    # Make sure the PID was written to the file, not just the file created
    time.sleep(0.5)

    with qtbot.waitSignal(runner.finished):
        os.kill(int(pidfile.read()), signal.SIGTERM)

    assert not text_file.exists()
Exemple #5
0
    def run_masscan(self):
        self.process = QProcess(self)
        response = IpAsnRangeDoc().search().query('match', owner='amazon.com').execute()

        tmp_file = '/tmp/masscan_in.tmp'
        with open(tmp_file, 'a') as f:
            for iprange_doc in response:
                for range in iprange_doc.ranges:
                    debug('scan cidr %s' % range.cidr)
                    f.write('%s\n' % range.cidr)

        params = ['--output-format', 'json', '--output-filename', '/tmp/masscan_out.tmp', '--source-port', '60000',
                  '-p%s' % ''.join(str(self.scan_ports)[1:-1]),'--rate', '%s' % self.spinBox.value(), '-iL',
                  '/tmp/masscan_in.tmp']
        debug('masscan params' % params)

        self.process.start('masscan', params)

        self.startScanButton.setEnabled(False)
        self.startScanButton.setText('starting..')
        self.process.waitForStarted(1)
        self.startScanButton.setText('stop scan')
        self.startScanButton.setEnabled(True)
        self.spinBox.setEnabled(False)

        watch = QFileSystemWatcher(self)
        watch.addPath('/tmp/masscan_out.tmp')
        watch.fileChanged.connect(functools.partial(self.data_available))

        self.process.setProcessChannelMode(QProcess.MergedChannels)
        self.process.readyReadStandardError.connect(functools.partial(self.stderrReady))
        self.process.stateChanged.connect(functools.partial(self.process_state_change))
Exemple #6
0
class AppBuffer(BrowserBuffer):
    def __init__(self, buffer_id, url, config_dir, arguments, emacs_var_dict,
                 module_path):
        BrowserBuffer.__init__(self, buffer_id, url, config_dir, arguments,
                               emacs_var_dict, module_path, False)

        self.url = url
        self.preview_file = tempfile.mkstemp(prefix='eaf-',
                                             suffix='.html',
                                             text=True)[1]
        self.render_js = os.path.join(os.path.dirname(__file__), "render.js")
        self.server_port = get_free_port()
        self.dark_mode = "false"
        if emacs_var_dict["eaf-markdown-dark-mode"] == "true" or \
           (emacs_var_dict["eaf-markdown-dark-mode"] == "follow" and emacs_var_dict["eaf-emacs-theme-mode"] == "dark"):
            self.dark_mode = "true"

        self.draw_progressbar = True

        self.run_render_server()
        self.render()

        self.file_watcher = QFileSystemWatcher()
        self.file_watcher.fileChanged.connect(self.on_file_changed)
        self.file_watcher.addPath(url)

    def run_render_server(self):
        args = ["node", self.render_js, str(self.server_port)]
        subprocess.Popen(args,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.STDOUT,
                         shell=False)

    def on_file_changed(self, *args):
        self.render()

    def retry_if_connection_refused(ex):
        return isinstance(ex, URLError) and isinstance(ex.reason,
                                                       ConnectionRefusedError)

    @retry(wait_fixed=500,
           stop_max_attempt_number=10,
           retry_on_exception=retry_if_connection_refused)
    def render(self):
        params = {
            "input_file": self.url,
            "output_file": self.preview_file,
            "dark_mode": self.dark_mode
        }
        url = 'http://localhost:{}?{}'.format(self.server_port,
                                              urlencode(params))
        with urlopen(url) as f:
            resp = f.read().decode("utf-8")
            if resp == "ok":
                self.buffer_widget.load(QUrl.fromLocalFile(self.preview_file))
                if platform.system() == "Windows":
                    eval_in_emacs('eaf-activate-emacs-window', [])
            else:
                message_to_emacs("preview failed: {}".format(resp))
Exemple #7
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)
class AutocutView(QMainWindow):
    """Class that displays the view for the autocut feature."""
    def __init__(self):
        """Loads the UI-file and sets up the GUI."""
        super(AutocutView, self).__init__()
        uic.loadUi(Resources.files.autocut_view, self)

        self.setWindowFlags(Qt.WindowCloseButtonHint
                            | Qt.WindowMinimizeButtonHint)
        self.setFixedSize(self.size())

        self.init_stylesheet()

        "QSS HOT RELOAD"
        self.__qss_watcher = QFileSystemWatcher()
        self.__qss_watcher.addPath(Resources.files.qss_dark)
        self.__qss_watcher.fileChanged.connect(self.update_qss)

        # centering the window
        rectangle = self.frameGeometry()
        center_point = QDesktopWidget().availableGeometry().center()
        rectangle.moveCenter(center_point)
        self.move(rectangle.topLeft())

        cross_path = Resources.images.cross
        tick_path = Resources.images.tick
        self.cross = QPixmap(cross_path).scaledToHeight(50, mode=1)
        self.tick = QPixmap(tick_path).scaledToHeight(50, mode=1)

        self.video_image_label.setPixmap(self.cross)
        self.pdf_image_label.setPixmap(self.cross)

    def init_stylesheet(self):
        current_stylesheet = Settings.get_instance().get_settings(
        ).design.color_theme.current
        if current_stylesheet == 0:
            self.setStyleSheet(open(Resources.files.qss_dark, "r").read())
        elif current_stylesheet == 1:
            self.setStyleSheet(open(Resources.files.qss_light, "r").read())

    def show(self):
        """Starts the window normal (not maximized)."""
        self.showNormal()

    def change_icon(self, label):
        """Changes the icon of a label to a tick icon."""
        label.setPixmap(self.tick)

    def update_qss(self):
        """ Updates the View when stylesheet changed, can be removed in production"""
        self.setStyleSheet(open(Resources.files.qss_dark, "r").read())
        self.__qss_watcher = QFileSystemWatcher()
        self.__qss_watcher.addPath(Resources.files.qss_dark)
        self.__qss_watcher.fileChanged.connect(self.update_qss)
Exemple #9
0
class SettingsView(QMainWindow):
    """
    A class used as the View for the settings window.

    In this class the Settings from the json file get displayed.
    If you want to add a setting go to the "config.py" file and simply
    add the desired setting to the dictionary that you'll find there.
    """

    saved = pyqtSignal()

    def __init__(self, parent=None):
        """Loads the UI-file and the shortcuts."""

        super(SettingsView, self).__init__(parent)
        uic.loadUi(Resources.files.settingsview, self)

        self.setWindowFlags(Qt.WindowCloseButtonHint
                            | Qt.WindowMinimizeButtonHint)
        self.init_stylesheet()
        "QSS HOT RELOAD"
        self.__qss_watcher = QFileSystemWatcher()
        self.__qss_watcher.addPath(Resources.files.qss_dark)
        """ centering the window """
        rectangle = self.frameGeometry()
        center_point = QDesktopWidget().availableGeometry().center()
        rectangle.moveCenter(center_point)
        self.move(rectangle.topLeft())

        self.saveButton = self.findChild(QPushButton, "saveButton")
        self.cancelButton = self.findChild(QPushButton, "cancelButton")

    def init_stylesheet(self):
        current_stylesheet = Settings.get_instance().get_settings(
        ).design.color_theme.current
        if current_stylesheet == 0:
            self.setStyleSheet(open(Resources.files.qss_dark, "r").read())
        elif current_stylesheet == 1:
            self.setStyleSheet(open(Resources.files.qss_light, "r").read())

    def show(self):
        """Starts the settings window maximized."""
        self.showNormal()

    def update_qss(self):
        """ Updates the View when stylesheet changed, can be removed in production"""
        self.init_stylesheet()
        self.__qss_watcher = QFileSystemWatcher()
        self.__qss_watcher.addPath(Resources.files.qss_dark)
        self.__qss_watcher.fileChanged.connect(self.update_qss)

    def update_view(self):
        self.parent().update_window()
Exemple #10
0
class DirectoryImageWatcher(QObject):

    image_updates = pyqtSignal(tuple, tuple)

    def __init__(self, directory=None):
        """Watches a given directory for added, removed and/or updated nifti files.

        Args:
            directory (str): the initial directory to watch. You can set a new one with set_directory().

        Signals
            images_updates (list, list, dict): sent when images are updated in the watch directory. It contains
                the list of additions, removals and changes where changes is a dict mapping the old name to a new one.
        """
        super(DirectoryImageWatcher, self).__init__()
        self._watched_dir = None
        self._current_files = []
        self._watcher = QFileSystemWatcher()
        self._watcher.directoryChanged.connect(self._directory_changed)
        self._timer = QTimer()
        self._timer.timeout.connect(self._timer_event)
        self._timer.timeout.connect(self._timer.stop)

        if directory:
            self.set_directory(directory)

    def set_directory(self, directory):
        """Set the watched directory to the given directory.

        Args:
            directory (str): the new directory to watch for added, removed and/or changed nifti files.
        """
        if self._watched_dir:
            self._watcher.removePath(self._watched_dir)
        self._watched_dir = directory
        self._watcher.addPath(directory)
        self._current_files = list(el[1] for el in yield_nifti_info(directory))

    @pyqtSlot(str)
    def _directory_changed(self, directory):
        if directory == self._watched_dir:
            self._timer.start(100)

    @pyqtSlot()
    def _timer_event(self):
        new_file_list = list(el[1] for el in yield_nifti_info(self._watched_dir))

        removals = set(self._current_files).difference(new_file_list)
        additions = set(new_file_list).difference(self._current_files)

        self._current_files = new_file_list
        self.image_updates.emit(tuple(additions), tuple(removals))
class FileSystemWatcher(QObject):
    fileChanged = pyqtSignal(str)
    directoryChanged = pyqtSignal(str)

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

        self.mWatchCount = QMap()
        self.mWatcher = QFileSystemWatcher(self)
        self.mWatcher.fileChanged.connect(self.onFileChanged)
        self.mWatcher.directoryChanged.connect(self.onDirectoryChanged)

    def addPath(self, path):
        # Just silently ignore the request when the file doesn't exist
        if (not QFile.exists(path)):
            return
        entry = self.mWatchCount.find(path)
        if not entry:
            self.mWatcher.addPath(path)
            self.mWatchCount.insert(path, 1)
        else:
            # Path is already being watched, increment watch count
            self.mWatchCount[path] += 1

    def removePath(self, path):
        entry = self.mWatchCount.find(path)
        if (entry == self.mWatchCount.end()):
            if (QFile.exists(path)):
                qWarning("FileSystemWatcher: Path was never added:\n"+path)
            return

        # Decrement watch count
        entry -= 1
        self.mWatchCount[path] = entry
        if (entry == 0):
            self.mWatchCount.erase(path)
            self.mWatcher.removePath(path)

    def onFileChanged(self, path):
        # If the file was replaced, the watcher is automatically removed and needs
        # to be re-added to keep watching it for changes. This happens commonly
        # with applications that do atomic saving.
        if (not self.mWatcher.files().__contains__(path)):
            if (QFile.exists(path)):
                self.mWatcher.addPath(path)
        self.fileChanged.emit(path)

    def onDirectoryChanged(self, path):
        self.directoryChanged.emit(path)
class FileSystemWatcher(QObject):
    fileChanged = pyqtSignal(str)
    directoryChanged = pyqtSignal(str)

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

        self.mWatchCount = QMap()
        self.mWatcher = QFileSystemWatcher(self)
        self.mWatcher.fileChanged.connect(self.onFileChanged)
        self.mWatcher.directoryChanged.connect(self.onDirectoryChanged)

    def addPath(self, path):
        # Just silently ignore the request when the file doesn't exist
        if (not QFile.exists(path)):
            return
        entry = self.mWatchCount.find(path)
        if not entry:
            self.mWatcher.addPath(path)
            self.mWatchCount.insert(path, 1)
        else:
            # Path is already being watched, increment watch count
            self.mWatchCount[path] += 1

    def removePath(self, path):
        entry = self.mWatchCount.find(path)
        if (entry == self.mWatchCount.end()):
            if (QFile.exists(path)):
                qWarning("FileSystemWatcher: Path was never added:\n" + path)
            return

        # Decrement watch count
        entry -= 1
        self.mWatchCount[path] = entry
        if (entry == 0):
            self.mWatchCount.erase(path)
            self.mWatcher.removePath(path)

    def onFileChanged(self, path):
        # If the file was replaced, the watcher is automatically removed and needs
        # to be re-added to keep watching it for changes. This happens commonly
        # with applications that do atomic saving.
        if (not self.mWatcher.files().__contains__(path)):
            if (QFile.exists(path)):
                self.mWatcher.addPath(path)
        self.fileChanged.emit(path)

    def onDirectoryChanged(self, path):
        self.directoryChanged.emit(path)
Exemple #13
0
class cssreloader():
    def __init__(self, csspath):

        self.path = csspath
        #Watch the file so we can auto reload
        self.csswatcher = QFileSystemWatcher()
        self.csswatcher.addPath(csspath)
        self.csswatcher.fileChanged.connect(self.rlcss)
        print("Watching " + self.path)

    #Handle auto reloading the CSS theme
    def rlcss(self, *args):
        global style
        #Read the notebook specific style.css which overrides that if it exists
        if os.path.exists(self.path):
            print("reloaded " + self.path)
            with open(self.path) as f:
                style = f.read()
def test_killed_command(qtbot, tmpdir, py_proc, runner):
    data_file = tmpdir / "data"
    watcher = QFileSystemWatcher()
    watcher.addPath(str(tmpdir))

    cmd, args = py_proc(
        r"""
        import os
        import time
        import sys
        import json

        data = {
            'pid': os.getpid(),
            'text_file': os.environ['QUTE_TEXT'],
        }

        # We can't use QUTE_FIFO to transmit the PID because that wouldn't work
        # on Windows, where QUTE_FIFO is only monitored after the script has
        # exited.

        with open(sys.argv[1], 'w') as f:
            json.dump(data, f)

        time.sleep(30)
    """
    )
    args.append(str(data_file))

    with qtbot.waitSignal(watcher.directoryChanged, timeout=10000):
        runner.prepare_run(cmd, *args)
        runner.store_text("Hello World")
        runner.store_html("")

    # Make sure the PID was written to the file, not just the file created
    time.sleep(0.5)

    data = json.load(data_file)

    with qtbot.waitSignal(runner.finished):
        os.kill(int(data["pid"]), signal.SIGTERM)

    assert not os.path.exists(data["text_file"])
def test_killed_command(qtbot, tmp_path, py_proc, runner, caplog):
    data_file = tmp_path / 'data'
    watcher = QFileSystemWatcher()
    watcher.addPath(str(tmp_path))

    cmd, args = py_proc(r"""
        import os
        import time
        import sys
        import json

        data = {
            'pid': os.getpid(),
            'text_file': os.environ['QUTE_TEXT'],
        }

        # We can't use QUTE_FIFO to transmit the PID because that wouldn't work
        # on Windows, where QUTE_FIFO is only monitored after the script has
        # exited.

        with open(sys.argv[1], 'w') as f:
            json.dump(data, f)

        time.sleep(30)
    """)
    args.append(str(data_file))

    with qtbot.wait_signal(watcher.directoryChanged, timeout=10000):
        runner.prepare_run(cmd, *args)
        runner.store_text('Hello World')
        runner.store_html('')

    # Make sure the PID was written to the file, not just the file created
    time.sleep(0.5)

    with data_file.open() as f:
        data = json.load(f)

    with caplog.at_level(logging.ERROR):
        with qtbot.wait_signal(runner.finished):
            os.kill(int(data['pid']), signal.SIGTERM)

    assert not pathlib.Path(data['text_file']).exists()
Exemple #16
0
class FileWatcher(QObject):
    pathChanged = pyqtSignal(str)

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

        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 watch(self, path):
        self._watcher.addPath(path)

    def onChanged(self, path):
        self.pathChanged.emit(path)
Exemple #17
0
def main(argv):
    signal.signal(signal.SIGINT, signal.SIG_DFL)

    app = QCoreApplication([])
    watcher = QFileSystemWatcher()

    print("Watching /tmp/")

    watcher.addPath("/tmp/")
    watcher.addPath("/tmp/foo")

    # Files have to be watched specifically for this to trigger.
    # Deleting and recreating a file makes this no longer trigger.
    watcher.fileChanged.connect(file_changed)

    # This triggers on file creation and deletion
    watcher.directoryChanged.connect(directory_changed)

    print("files:", watcher.files())
    print("directories:", watcher.directories())

    sys.exit(app.exec())
Exemple #18
0
def main(argv):
    signal.signal(signal.SIGINT, signal.SIG_DFL)

    app = QCoreApplication([])
    watcher = QFileSystemWatcher()

    print("Watching /tmp/")

    watcher.addPath("/tmp/")
    watcher.addPath("/tmp/foo")

    # Files have to be watched specifically for this to trigger.
    # Deleting and recreating a file makes this no longer trigger.
    watcher.fileChanged.connect(file_changed)

    # This triggers on file creation and deletion
    watcher.directoryChanged.connect(directory_changed)

    print("files:", watcher.files())
    print("directories:", watcher.directories())

    sys.exit(app.exec())
Exemple #19
0
class ReTextWindow(QMainWindow):
	def __init__(self, parent=None):
		QMainWindow.__init__(self, parent)
		self.resize(950, 700)
		screenRect = QDesktopWidget().screenGeometry()
		if globalSettings.windowGeometry:
			self.restoreGeometry(globalSettings.windowGeometry)
		else:
			self.move((screenRect.width()-self.width())/2, (screenRect.height()-self.height())/2)
		if not screenRect.contains(self.geometry()):
			self.showMaximized()
		if globalSettings.iconTheme:
			QIcon.setThemeName(globalSettings.iconTheme)
		if QIcon.themeName() in ('hicolor', ''):
			if not QFile.exists(icon_path + 'document-new.png'):
				QIcon.setThemeName(get_icon_theme())
		if QFile.exists(icon_path+'retext.png'):
			self.setWindowIcon(QIcon(icon_path+'retext.png'))
		elif QFile.exists('/usr/share/pixmaps/retext.png'):
			self.setWindowIcon(QIcon('/usr/share/pixmaps/retext.png'))
		else:
			self.setWindowIcon(QIcon.fromTheme('retext',
				QIcon.fromTheme('accessories-text-editor')))
		self.editBoxes = []
		self.previewBoxes = []
		self.highlighters = []
		self.markups = []
		self.fileNames = []
		self.actionPreviewChecked = []
		self.actionLivePreviewChecked = []
		self.tabWidget = QTabWidget(self)
		self.initTabWidget()
		self.setCentralWidget(self.tabWidget)
		self.tabWidget.currentChanged.connect(self.changeIndex)
		self.tabWidget.tabCloseRequested.connect(self.closeTab)
		toolBar = QToolBar(self.tr('File toolbar'), self)
		self.addToolBar(Qt.TopToolBarArea, toolBar)
		self.editBar = QToolBar(self.tr('Edit toolbar'), self)
		self.addToolBar(Qt.TopToolBarArea, self.editBar)
		self.searchBar = QToolBar(self.tr('Search toolbar'), self)
		self.addToolBar(Qt.BottomToolBarArea, self.searchBar)
		toolBar.setVisible(not globalSettings.hideToolBar)
		self.editBar.setVisible(not globalSettings.hideToolBar)
		self.actionNew = self.act(self.tr('New'), 'document-new',
			self.createNew, shct=QKeySequence.New)
		self.actionNew.setPriority(QAction.LowPriority)
		self.actionOpen = self.act(self.tr('Open'), 'document-open',
			self.openFile, shct=QKeySequence.Open)
		self.actionOpen.setPriority(QAction.LowPriority)
		self.actionSetEncoding = self.act(self.tr('Set encoding'),
			trig=self.showEncodingDialog)
		self.actionSetEncoding.setEnabled(False)
		self.actionReload = self.act(self.tr('Reload'), 'view-refresh', trig=self.openFileMain)
		self.actionReload.setEnabled(False)
		self.actionSave = self.act(self.tr('Save'), 'document-save',
			self.saveFile, shct=QKeySequence.Save)
		self.actionSave.setEnabled(False)
		self.actionSave.setPriority(QAction.LowPriority)
		self.actionSaveAs = self.act(self.tr('Save as'), 'document-save-as',
			self.saveFileAs, shct=QKeySequence.SaveAs)
		self.actionNextTab = self.act(self.tr('Next tab'), 'go-next',
			lambda: self.switchTab(1), shct=Qt.CTRL+Qt.Key_PageDown)
		self.actionPrevTab = self.act(self.tr('Previous tab'), 'go-previous',
			lambda: self.switchTab(-1), shct=Qt.CTRL+Qt.Key_PageUp)
		self.actionPrint = self.act(self.tr('Print'), 'document-print',
			self.printFile, shct=QKeySequence.Print)
		self.actionPrint.setPriority(QAction.LowPriority)
		self.actionPrintPreview = self.act(self.tr('Print preview'), 'document-print-preview',
			self.printPreview)
		self.actionViewHtml = self.act(self.tr('View HTML code'), 'text-html', self.viewHtml)
		self.actionChangeEditorFont = self.act(self.tr('Change editor font'),
			trig=self.changeEditorFont)
		self.actionChangePreviewFont = self.act(self.tr('Change preview font'),
			trig=self.changePreviewFont)
		self.actionSearch = self.act(self.tr('Find text'), 'edit-find', shct=QKeySequence.Find)
		self.actionSearch.setCheckable(True)
		self.actionSearch.triggered[bool].connect(self.searchBar.setVisible)
		self.searchBar.visibilityChanged.connect(self.searchBarVisibilityChanged)
		self.actionPreview = self.act(self.tr('Preview'), shct=Qt.CTRL+Qt.Key_E,
			trigbool=self.preview)
		if QIcon.hasThemeIcon('document-preview'):
			self.actionPreview.setIcon(QIcon.fromTheme('document-preview'))
		elif QIcon.hasThemeIcon('preview-file'):
			self.actionPreview.setIcon(QIcon.fromTheme('preview-file'))
		elif QIcon.hasThemeIcon('x-office-document'):
			self.actionPreview.setIcon(QIcon.fromTheme('x-office-document'))
		else:
			self.actionPreview.setIcon(QIcon(icon_path+'document-preview.png'))
		self.actionLivePreview = self.act(self.tr('Live preview'), shct=Qt.CTRL+Qt.Key_L,
		trigbool=self.enableLivePreview)
		self.actionTableMode = self.act(self.tr('Table mode'), shct=Qt.CTRL+Qt.Key_T,
			trigbool=lambda x: self.editBoxes[self.ind].enableTableMode(x))
		if ReTextFakeVimHandler:
			self.actionFakeVimMode = self.act(self.tr('FakeVim mode'),
				shct=Qt.CTRL+Qt.ALT+Qt.Key_V, trigbool=self.enableFakeVimMode)
			if globalSettings.useFakeVim:
				self.actionFakeVimMode.setChecked(True)
				self.enableFakeVimMode(True)
		self.actionFullScreen = self.act(self.tr('Fullscreen mode'), 'view-fullscreen',
			shct=Qt.Key_F11, trigbool=self.enableFullScreen)
		self.actionFullScreen.setPriority(QAction.LowPriority)
		self.actionConfig = self.act(self.tr('Preferences'), icon='preferences-system',
			trig=self.openConfigDialog)
		self.actionConfig.setMenuRole(QAction.PreferencesRole)
		self.actionSaveHtml = self.act('HTML', 'text-html', self.saveFileHtml)
		self.actionPdf = self.act('PDF', 'application-pdf', self.savePdf)
		self.actionOdf = self.act('ODT', 'x-office-document', self.saveOdf)
		self.getExportExtensionsList()
		self.actionQuit = self.act(self.tr('Quit'), 'application-exit', shct=QKeySequence.Quit)
		self.actionQuit.setMenuRole(QAction.QuitRole)
		self.actionQuit.triggered.connect(self.close)
		self.actionUndo = self.act(self.tr('Undo'), 'edit-undo',
			lambda: self.editBoxes[self.ind].undo(), shct=QKeySequence.Undo)
		self.actionRedo = self.act(self.tr('Redo'), 'edit-redo',
			lambda: self.editBoxes[self.ind].redo(), shct=QKeySequence.Redo)
		self.actionCopy = self.act(self.tr('Copy'), 'edit-copy',
			lambda: self.editBoxes[self.ind].copy(), shct=QKeySequence.Copy)
		self.actionCut = self.act(self.tr('Cut'), 'edit-cut',
			lambda: self.editBoxes[self.ind].cut(), shct=QKeySequence.Cut)
		self.actionPaste = self.act(self.tr('Paste'), 'edit-paste',
			lambda: self.editBoxes[self.ind].paste(), shct=QKeySequence.Paste)
		self.actionUndo.setEnabled(False)
		self.actionRedo.setEnabled(False)
		self.actionCopy.setEnabled(False)
		self.actionCut.setEnabled(False)
		qApp = QApplication.instance()
		qApp.clipboard().dataChanged.connect(self.clipboardDataChanged)
		self.clipboardDataChanged()
		if enchant_available:
			self.actionEnableSC = self.act(self.tr('Enable'), trigbool=self.enableSpellCheck)
			self.actionSetLocale = self.act(self.tr('Set locale'), trig=self.changeLocale)
		self.actionWebKit = self.act(self.tr('Use WebKit renderer'), trigbool=self.enableWebKit)
		self.actionWebKit.setChecked(globalSettings.useWebKit)
		self.actionShow = self.act(self.tr('Show directory'), 'system-file-manager', self.showInDir)
		self.actionFind = self.act(self.tr('Next'), 'go-next', self.find,
			shct=QKeySequence.FindNext)
		self.actionFindPrev = self.act(self.tr('Previous'), 'go-previous',
			lambda: self.find(back=True), shct=QKeySequence.FindPrevious)
		self.actionHelp = self.act(self.tr('Get help online'), 'help-contents', self.openHelp)
		self.aboutWindowTitle = self.tr('About ReText')
		self.actionAbout = self.act(self.aboutWindowTitle, 'help-about', self.aboutDialog)
		self.actionAbout.setMenuRole(QAction.AboutRole)
		self.actionAboutQt = self.act(self.tr('About Qt'))
		self.actionAboutQt.setMenuRole(QAction.AboutQtRole)
		self.actionAboutQt.triggered.connect(qApp.aboutQt)
		availableMarkups = markups.get_available_markups()
		if not availableMarkups:
			print('Warning: no markups are available!')
		self.defaultMarkup = availableMarkups[0] if availableMarkups else None
		if globalSettings.defaultMarkup:
			mc = markups.find_markup_class_by_name(globalSettings.defaultMarkup)
			if mc and mc.available():
				self.defaultMarkup = mc
		if len(availableMarkups) > 1:
			self.chooseGroup = QActionGroup(self)
			markupActions = []
			for markup in availableMarkups:
				markupAction = self.act(markup.name, trigbool=self.markupFunction(markup))
				if markup == self.defaultMarkup:
					markupAction.setChecked(True)
				self.chooseGroup.addAction(markupAction)
				markupActions.append(markupAction)
		self.actionBold = self.act(self.tr('Bold'), shct=QKeySequence.Bold,
			trig=lambda: self.insertChars('**'))
		self.actionItalic = self.act(self.tr('Italic'), shct=QKeySequence.Italic,
			trig=lambda: self.insertChars('*'))
		self.actionUnderline = self.act(self.tr('Underline'), shct=QKeySequence.Underline,
			trig=lambda: self.insertTag('u'))
		self.usefulTags = ('a', 'big', 'center', 'img', 's', 'small', 'span',
			'table', 'td', 'tr', 'u')
		self.usefulChars = ('deg', 'divide', 'dollar', 'hellip', 'laquo', 'larr',
			'lsquo', 'mdash', 'middot', 'minus', 'nbsp', 'ndash', 'raquo',
			'rarr', 'rsquo', 'times')
		self.tagsBox = QComboBox(self.editBar)
		self.tagsBox.addItem(self.tr('Tags'))
		self.tagsBox.addItems(self.usefulTags)
		self.tagsBox.activated.connect(self.insertTag)
		self.symbolBox = QComboBox(self.editBar)
		self.symbolBox.addItem(self.tr('Symbols'))
		self.symbolBox.addItems(self.usefulChars)
		self.symbolBox.activated.connect(self.insertSymbol)
		self.updateStyleSheet()
		menubar = QMenuBar(self)
		menubar.setGeometry(QRect(0, 0, 800, 25))
		self.setMenuBar(menubar)
		menuFile = menubar.addMenu(self.tr('File'))
		menuEdit = menubar.addMenu(self.tr('Edit'))
		menuHelp = menubar.addMenu(self.tr('Help'))
		menuFile.addAction(self.actionNew)
		menuFile.addAction(self.actionOpen)
		self.menuRecentFiles = menuFile.addMenu(self.tr('Open recent'))
		self.menuRecentFiles.aboutToShow.connect(self.updateRecentFiles)
		menuFile.addMenu(self.menuRecentFiles)
		menuFile.addAction(self.actionShow)
		menuFile.addAction(self.actionSetEncoding)
		menuFile.addAction(self.actionReload)
		menuFile.addSeparator()
		menuFile.addAction(self.actionSave)
		menuFile.addAction(self.actionSaveAs)
		menuFile.addSeparator()
		menuFile.addAction(self.actionNextTab)
		menuFile.addAction(self.actionPrevTab)
		menuFile.addSeparator()
		menuExport = menuFile.addMenu(self.tr('Export'))
		menuExport.addAction(self.actionSaveHtml)
		menuExport.addAction(self.actionOdf)
		menuExport.addAction(self.actionPdf)
		if self.extensionActions:
			menuExport.addSeparator()
			for action, mimetype in self.extensionActions:
				menuExport.addAction(action)
			menuExport.aboutToShow.connect(self.updateExtensionsVisibility)
		menuFile.addAction(self.actionPrint)
		menuFile.addAction(self.actionPrintPreview)
		menuFile.addSeparator()
		menuFile.addAction(self.actionQuit)
		menuEdit.addAction(self.actionUndo)
		menuEdit.addAction(self.actionRedo)
		menuEdit.addSeparator()
		menuEdit.addAction(self.actionCut)
		menuEdit.addAction(self.actionCopy)
		menuEdit.addAction(self.actionPaste)
		menuEdit.addSeparator()
		if enchant_available:
			menuSC = menuEdit.addMenu(self.tr('Spell check'))
			menuSC.addAction(self.actionEnableSC)
			menuSC.addAction(self.actionSetLocale)
		menuEdit.addAction(self.actionSearch)
		menuEdit.addAction(self.actionChangeEditorFont)
		menuEdit.addAction(self.actionChangePreviewFont)
		menuEdit.addSeparator()
		if len(availableMarkups) > 1:
			self.menuMode = menuEdit.addMenu(self.tr('Default markup'))
			for markupAction in markupActions:
				self.menuMode.addAction(markupAction)
		menuFormat = menuEdit.addMenu(self.tr('Formatting'))
		menuFormat.addAction(self.actionBold)
		menuFormat.addAction(self.actionItalic)
		menuFormat.addAction(self.actionUnderline)
		menuEdit.addAction(self.actionWebKit)
		menuEdit.addSeparator()
		menuEdit.addAction(self.actionViewHtml)
		menuEdit.addAction(self.actionLivePreview)
		menuEdit.addAction(self.actionPreview)
		menuEdit.addAction(self.actionTableMode)
		if ReTextFakeVimHandler:
			menuEdit.addAction(self.actionFakeVimMode)
		menuEdit.addSeparator()
		menuEdit.addAction(self.actionFullScreen)
		menuEdit.addAction(self.actionConfig)
		menuHelp.addAction(self.actionHelp)
		menuHelp.addSeparator()
		menuHelp.addAction(self.actionAbout)
		menuHelp.addAction(self.actionAboutQt)
		menubar.addMenu(menuFile)
		menubar.addMenu(menuEdit)
		menubar.addMenu(menuHelp)
		toolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
		toolBar.addAction(self.actionNew)
		toolBar.addSeparator()
		toolBar.addAction(self.actionOpen)
		toolBar.addAction(self.actionSave)
		toolBar.addAction(self.actionPrint)
		toolBar.addSeparator()
		toolBar.addAction(self.actionPreview)
		toolBar.addAction(self.actionFullScreen)
		self.editBar.addAction(self.actionUndo)
		self.editBar.addAction(self.actionRedo)
		self.editBar.addSeparator()
		self.editBar.addAction(self.actionCut)
		self.editBar.addAction(self.actionCopy)
		self.editBar.addAction(self.actionPaste)
		self.editBar.addSeparator()
		self.editBar.addWidget(self.tagsBox)
		self.editBar.addWidget(self.symbolBox)
		self.searchEdit = QLineEdit(self.searchBar)
		self.searchEdit.setPlaceholderText(self.tr('Search'))
		self.searchEdit.returnPressed.connect(self.find)
		self.csBox = QCheckBox(self.tr('Case sensitively'), self.searchBar)
		self.searchBar.addWidget(self.searchEdit)
		self.searchBar.addSeparator()
		self.searchBar.addWidget(self.csBox)
		self.searchBar.addAction(self.actionFindPrev)
		self.searchBar.addAction(self.actionFind)
		self.searchBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
		self.searchBar.setVisible(False)
		self.autoSaveEnabled = globalSettings.autoSave
		if self.autoSaveEnabled:
			timer = QTimer(self)
			timer.start(60000)
			timer.timeout.connect(self.saveAll)
		self.ind = None
		if enchant_available:
			self.sl = globalSettings.spellCheckLocale
			if self.sl:
				try:
					enchant.Dict(self.sl)
				except Exception as e:
					print(e, file=sys.stderr)
					self.sl = None
			if globalSettings.spellCheck:
				self.actionEnableSC.setChecked(True)
				self.enableSpellCheck(True)
		self.fileSystemWatcher = QFileSystemWatcher()
		self.fileSystemWatcher.fileChanged.connect(self.fileChanged)

	def updateStyleSheet(self):
		if globalSettings.styleSheet:
			sheetfile = QFile(globalSettings.styleSheet)
			sheetfile.open(QIODevice.ReadOnly)
			self.ss = QTextStream(sheetfile).readAll()
			sheetfile.close()
		else:
			self.ss = ''

	def initTabWidget(self):
		def dragEnterEvent(e):
			e.acceptProposedAction()
		def dropEvent(e):
			fn = bytes(e.mimeData().data('text/plain')).decode().rstrip()
			if fn.startswith('file:'):
				fn = QUrl(fn).toLocalFile()
			self.openFileWrapper(fn)
		self.tabWidget.setTabsClosable(True)
		self.tabWidget.setAcceptDrops(True)
		self.tabWidget.dragEnterEvent = dragEnterEvent
		self.tabWidget.dropEvent = dropEvent

	def act(self, name, icon=None, trig=None, trigbool=None, shct=None):
		if not isinstance(shct, QKeySequence):
			shct = QKeySequence(shct)
		if icon:
			action = QAction(self.actIcon(icon), name, self)
		else:
			action = QAction(name, self)
		if trig:
			action.triggered.connect(trig)
		elif trigbool:
			action.setCheckable(True)
			action.triggered[bool].connect(trigbool)
		if shct:
			action.setShortcut(shct)
		return action

	def actIcon(self, name):
		return QIcon.fromTheme(name, QIcon(icon_path+name+'.png'))

	def printError(self):
		import traceback
		print('Exception occured while parsing document:', file=sys.stderr)
		traceback.print_exc()

	def getSplitter(self, index):
		splitter = QSplitter(Qt.Horizontal)
		# Give both boxes a minimum size so the minimumSizeHint will be
		# ignored when splitter.setSizes is called below
		for widget in self.editBoxes[index], self.previewBoxes[index]:
			widget.setMinimumWidth(125)
			splitter.addWidget(widget)
		splitter.setSizes((50, 50))
		splitter.setChildrenCollapsible(False)
		return splitter

	def getWebView(self):
		webView = QWebView()
		if not globalSettings.handleWebLinks:
			webView.page().setLinkDelegationPolicy(QWebPage.DelegateExternalLinks)
			webView.page().linkClicked.connect(QDesktopServices.openUrl)
		settings = webView.settings()
		settings.setAttribute(QWebSettings.LocalContentCanAccessFileUrls, False)
		settings.setDefaultTextEncoding('utf-8')
		return webView

	def createTab(self, fileName):
		self.previewBlocked = False
		self.editBoxes.append(ReTextEdit(self))
		self.highlighters.append(ReTextHighlighter(self.editBoxes[-1].document()))
		if enchant_available and self.actionEnableSC.isChecked():
			self.highlighters[-1].dictionary = \
			enchant.Dict(self.sl) if self.sl else enchant.Dict()
			self.highlighters[-1].rehighlight()
		if globalSettings.useWebKit:
			self.previewBoxes.append(self.getWebView())
		else:
			self.previewBoxes.append(QTextBrowser())
			self.previewBoxes[-1].setOpenExternalLinks(True)
		self.previewBoxes[-1].setVisible(False)
		self.fileNames.append(fileName)
		markupClass = self.getMarkupClass(fileName)
		self.markups.append(self.getMarkup(fileName))
		self.highlighters[-1].docType = (markupClass.name if markupClass else '')
		liveMode = globalSettings.restorePreviewState and globalSettings.previewState
		self.actionPreviewChecked.append(liveMode)
		self.actionLivePreviewChecked.append(liveMode)
		metrics = QFontMetrics(self.editBoxes[-1].font())
		self.editBoxes[-1].setTabStopWidth(globalSettings.tabWidth * metrics.width(' '))
		self.editBoxes[-1].textChanged.connect(self.updateLivePreviewBox)
		self.editBoxes[-1].undoAvailable.connect(self.actionUndo.setEnabled)
		self.editBoxes[-1].redoAvailable.connect(self.actionRedo.setEnabled)
		self.editBoxes[-1].copyAvailable.connect(self.enableCopy)
		self.editBoxes[-1].document().modificationChanged.connect(self.modificationChanged)
		if globalSettings.useFakeVim:
			self.installFakeVimHandler(self.editBoxes[-1])
		return self.getSplitter(-1)

	def closeTab(self, ind):
		if self.maybeSave(ind):
			if self.tabWidget.count() == 1:
				self.tabWidget.addTab(self.createTab(""), self.tr("New document"))
			if self.fileNames[ind]:
				self.fileSystemWatcher.removePath(self.fileNames[ind])
			del self.editBoxes[ind]
			del self.previewBoxes[ind]
			del self.highlighters[ind]
			del self.markups[ind]
			del self.fileNames[ind]
			del self.actionPreviewChecked[ind]
			del self.actionLivePreviewChecked[ind]
			self.tabWidget.removeTab(ind)

	def getMarkupClass(self, fileName=None):
		if fileName is None:
			fileName = self.fileNames[self.ind]
		if fileName:
			markupClass = markups.get_markup_for_file_name(
				fileName, return_class=True)
			if markupClass:
				return markupClass
		return self.defaultMarkup

	def getMarkup(self, fileName=None):
		if fileName is None:
			fileName = self.fileNames[self.ind]
		markupClass = self.getMarkupClass(fileName=fileName)
		if markupClass and markupClass.available():
			return markupClass(filename=fileName)

	def docTypeChanged(self):
		oldType = self.highlighters[self.ind].docType
		markupClass = self.getMarkupClass()
		newType = markupClass.name if markupClass else ''
		if oldType != newType:
			self.markups[self.ind] = self.getMarkup()
			self.updatePreviewBox()
			self.highlighters[self.ind].docType = newType
			self.highlighters[self.ind].rehighlight()
		dtMarkdown = (newType == DOCTYPE_MARKDOWN)
		dtMkdOrReST = (newType in (DOCTYPE_MARKDOWN, DOCTYPE_REST))
		self.tagsBox.setEnabled(dtMarkdown)
		self.symbolBox.setEnabled(dtMarkdown)
		self.actionUnderline.setEnabled(dtMarkdown)
		self.actionBold.setEnabled(dtMkdOrReST)
		self.actionItalic.setEnabled(dtMkdOrReST)
		canReload = bool(self.fileNames[self.ind]) and not self.autoSaveActive()
		self.actionSetEncoding.setEnabled(canReload)
		self.actionReload.setEnabled(canReload)

	def changeIndex(self, ind):
		if ind > -1:
			self.actionUndo.setEnabled(self.editBoxes[ind].document().isUndoAvailable())
			self.actionRedo.setEnabled(self.editBoxes[ind].document().isRedoAvailable())
			self.actionCopy.setEnabled(self.editBoxes[ind].textCursor().hasSelection())
			self.actionCut.setEnabled(self.editBoxes[ind].textCursor().hasSelection())
			self.actionPreview.setChecked(self.actionPreviewChecked[ind])
			self.actionLivePreview.setChecked(self.actionLivePreviewChecked[ind])
			self.actionTableMode.setChecked(self.editBoxes[ind].tableModeEnabled)
			self.editBar.setDisabled(self.actionPreviewChecked[ind])
		self.ind = ind
		if self.fileNames[ind]:
			self.setCurrentFile()
		else:
			self.setWindowTitle(self.tr('New document') + '[*]')
			self.docTypeChanged()
		self.modificationChanged(self.editBoxes[ind].document().isModified())
		if globalSettings.restorePreviewState:
			globalSettings.previewState = self.actionLivePreviewChecked[ind]
		if self.actionLivePreviewChecked[ind]:
			self.enableLivePreview(True)
		self.editBoxes[self.ind].setFocus(Qt.OtherFocusReason)

	def changeEditorFont(self):
		font, ok = QFontDialog.getFont(globalSettings.editorFont, self)
		if ok:
			globalSettings.editorFont = font
			for editor in self.editBoxes:
				editor.updateFont()

	def changePreviewFont(self):
		font, ok = QFontDialog.getFont(globalSettings.font, self)
		if ok:
			globalSettings.font = font
			self.updatePreviewBox()

	def preview(self, viewmode):
		self.actionPreviewChecked[self.ind] = viewmode
		if self.actionLivePreview.isChecked():
			self.actionLivePreview.setChecked(False)
			return self.enableLivePreview(False)
		self.editBar.setDisabled(viewmode)
		self.editBoxes[self.ind].setVisible(not viewmode)
		self.previewBoxes[self.ind].setVisible(viewmode)
		if viewmode:
			self.updatePreviewBox()

	def enableLivePreview(self, livemode):
		if globalSettings.restorePreviewState:
			globalSettings.previewState = livemode
		self.actionLivePreviewChecked[self.ind] = livemode
		self.actionPreviewChecked[self.ind] = livemode
		self.actionPreview.setChecked(livemode)
		self.editBar.setEnabled(True)
		self.previewBoxes[self.ind].setVisible(livemode)
		self.editBoxes[self.ind].setVisible(True)
		if livemode:
			self.updatePreviewBox()

	def enableWebKit(self, enable):
		globalSettings.useWebKit = enable
		oldind = self.ind
		self.tabWidget.clear()
		for self.ind in range(len(self.editBoxes)):
			if enable:
				self.previewBoxes[self.ind] = self.getWebView()
			else:
				self.previewBoxes[self.ind] = QTextBrowser()
				self.previewBoxes[self.ind].setOpenExternalLinks(True)
			splitter = self.getSplitter(self.ind)
			self.tabWidget.addTab(splitter, self.getDocumentTitle(baseName=True))
			self.updatePreviewBox()
			self.previewBoxes[self.ind].setVisible(self.actionPreviewChecked[self.ind])
		self.ind = oldind
		self.tabWidget.setCurrentIndex(self.ind)

	def enableCopy(self, copymode):
		self.actionCopy.setEnabled(copymode)
		self.actionCut.setEnabled(copymode)

	def enableFullScreen(self, yes):
		if yes:
			self.showFullScreen()
		else:
			self.showNormal()

	def openConfigDialog(self):
		dlg = ConfigDialog(self)
		dlg.setWindowTitle(self.tr('Preferences'))
		dlg.show()

	def installFakeVimHandler(self, editor):
		if ReTextFakeVimHandler:
			fakeVimEditor = ReTextFakeVimHandler(editor, self)
			fakeVimEditor.setSaveAction(self.actionSave)
			fakeVimEditor.setQuitAction(self.actionQuit)
			self.actionFakeVimMode.triggered.connect(fakeVimEditor.remove)

	def enableFakeVimMode(self, yes):
		globalSettings.useFakeVim = yes
		if yes:
			FakeVimMode.init(self)
			for editor in self.editBoxes:
				self.installFakeVimHandler(editor)
		else:
			FakeVimMode.exit(self)

	def enableSpellCheck(self, yes):
		if yes:
			if self.sl:
				self.setAllDictionaries(enchant.Dict(self.sl))
			else:
				self.setAllDictionaries(enchant.Dict())
		else:
			self.setAllDictionaries(None)
		globalSettings.spellCheck = yes

	def setAllDictionaries(self, dictionary):
		for hl in self.highlighters:
			hl.dictionary = dictionary
			hl.rehighlight()

	def changeLocale(self):
		if self.sl:
			localedlg = LocaleDialog(self, defaultText=self.sl)
		else:
			localedlg = LocaleDialog(self)
		if localedlg.exec() != QDialog.Accepted:
			return
		sl = localedlg.localeEdit.text()
		setdefault = localedlg.checkBox.isChecked()
		if sl:
			try:
				sl = str(sl)
				enchant.Dict(sl)
			except Exception as e:
				QMessageBox.warning(self, '', str(e))
			else:
				self.sl = sl
				self.enableSpellCheck(self.actionEnableSC.isChecked())
		else:
			self.sl = None
			self.enableSpellCheck(self.actionEnableSC.isChecked())
		if setdefault:
			globalSettings.spellCheckLocale = sl

	def searchBarVisibilityChanged(self, visible):
		self.actionSearch.setChecked(visible)
		if visible:
			self.searchEdit.setFocus(Qt.ShortcutFocusReason)

	def find(self, back=False):
		flags = QTextDocument.FindFlags()
		if back:
			flags |= QTextDocument.FindBackward
		if self.csBox.isChecked():
			flags |= QTextDocument.FindCaseSensitively
		text = self.searchEdit.text()
		editBox = self.editBoxes[self.ind]
		cursor = editBox.textCursor()
		newCursor = editBox.document().find(text, cursor, flags)
		if not newCursor.isNull():
			editBox.setTextCursor(newCursor)
			return self.setSearchEditColor(True)
		cursor.movePosition(QTextCursor.End if back else QTextCursor.Start)
		newCursor = editBox.document().find(text, cursor, flags)
		if not newCursor.isNull():
			editBox.setTextCursor(newCursor)
			return self.setSearchEditColor(True)
		self.setSearchEditColor(False)

	def setSearchEditColor(self, found):
		palette = self.searchEdit.palette()
		palette.setColor(QPalette.Active, QPalette.Base,
		                 Qt.white if found else QColor(255, 102, 102))
		self.searchEdit.setPalette(palette)

	def getHtml(self, includeStyleSheet=True, includeTitle=True,
	            includeMeta=False, webenv=False):
		if self.markups[self.ind] is None:
			markupClass = self.getMarkupClass()
			errMsg = self.tr('Could not parse file contents, check if '
			'you have the <a href="%s">necessary module</a> installed!')
			try:
				errMsg %= markupClass.attributes[MODULE_HOME_PAGE]
			except (AttributeError, KeyError):
				# Remove the link if markupClass doesn't have the needed attribute
				errMsg = errMsg.replace('<a href="%s">', '')
				errMsg = errMsg.replace('</a>', '')
			return '<p style="color: red">%s</p>' % errMsg
		text = self.editBoxes[self.ind].toPlainText()
		headers = ''
		if includeStyleSheet:
			headers += '<style type="text/css">\n' + self.ss + '</style>\n'
		cssFileName = self.getDocumentTitle(baseName=True)+'.css'
		if QFile(cssFileName).exists():
			headers += '<link rel="stylesheet" type="text/css" href="%s">\n' \
			% cssFileName
		if includeMeta:
			headers += ('<meta name="generator" content="ReText %s">\n' %
			            app_version)
		fallbackTitle = self.getDocumentTitle() if includeTitle else ''
		return self.markups[self.ind].get_whole_html(text,
			custom_headers=headers, include_stylesheet=includeStyleSheet,
			fallback_title=fallbackTitle, webenv=webenv)

	def updatePreviewBox(self):
		self.previewBlocked = False
		pb = self.previewBoxes[self.ind]
		textedit = isinstance(pb, QTextEdit)
		if textedit:
			scrollbar = pb.verticalScrollBar()
			disttobottom = scrollbar.maximum() - scrollbar.value()
		else:
			frame = pb.page().mainFrame()
			scrollpos = frame.scrollPosition()
		try:
			html = self.getHtml()
		except Exception:
			return self.printError()
		if textedit:
			pb.setHtml(html)
			pb.document().setDefaultFont(globalSettings.font)
			scrollbar.setValue(scrollbar.maximum() - disttobottom)
		else:
			pb.settings().setFontFamily(QWebSettings.StandardFont,
			                            globalSettings.font.family())
			pb.settings().setFontSize(QWebSettings.DefaultFontSize,
			                          globalSettings.font.pointSize())
			pb.setHtml(html, QUrl.fromLocalFile(self.fileNames[self.ind]))
			frame.setScrollPosition(scrollpos)

	def updateLivePreviewBox(self):
		if self.actionLivePreview.isChecked() and self.previewBlocked == False:
			self.previewBlocked = True
			QTimer.singleShot(1000, self.updatePreviewBox)

	def showInDir(self):
		if self.fileNames[self.ind]:
			path = QFileInfo(self.fileNames[self.ind]).path()
			QDesktopServices.openUrl(QUrl.fromLocalFile(path))
		else:
			QMessageBox.warning(self, '', self.tr("Please, save the file somewhere."))

	def setCurrentFile(self):
		self.setWindowTitle("")
		self.tabWidget.setTabText(self.ind, self.getDocumentTitle(baseName=True))
		self.setWindowFilePath(self.fileNames[self.ind])
		files = readListFromSettings("recentFileList")
		while self.fileNames[self.ind] in files:
			files.remove(self.fileNames[self.ind])
		files.insert(0, self.fileNames[self.ind])
		if len(files) > 10:
			del files[10:]
		writeListToSettings("recentFileList", files)
		QDir.setCurrent(QFileInfo(self.fileNames[self.ind]).dir().path())
		self.docTypeChanged()

	def createNew(self, text=None):
		self.tabWidget.addTab(self.createTab(""), self.tr("New document"))
		self.ind = self.tabWidget.count()-1
		self.tabWidget.setCurrentIndex(self.ind)
		if text:
			self.editBoxes[self.ind].textCursor().insertText(text)

	def switchTab(self, shift=1):
		self.tabWidget.setCurrentIndex((self.ind + shift) % self.tabWidget.count())

	def updateRecentFiles(self):
		self.menuRecentFiles.clear()
		self.recentFilesActions = []
		filesOld = readListFromSettings("recentFileList")
		files = []
		for f in filesOld:
			if QFile.exists(f):
				files.append(f)
				self.recentFilesActions.append(self.act(f, trig=self.openFunction(f)))
		writeListToSettings("recentFileList", files)
		for action in self.recentFilesActions:
			self.menuRecentFiles.addAction(action)

	def markupFunction(self, markup):
		return lambda: self.setDefaultMarkup(markup)

	def openFunction(self, fileName):
		return lambda: self.openFileWrapper(fileName)

	def extensionFuntion(self, data):
		return lambda: \
		self.runExtensionCommand(data['Exec'], data['FileFilter'], data['DefaultExtension'])

	def getExportExtensionsList(self):
		extensions = []
		for extsprefix in datadirs:
			extsdir = QDir(extsprefix+'/export-extensions/')
			if extsdir.exists():
				for fileInfo in extsdir.entryInfoList(['*.desktop', '*.ini'],
				QDir.Files | QDir.Readable):
					extensions.append(self.readExtension(fileInfo.filePath()))
		locale = QLocale.system().name()
		self.extensionActions = []
		for extension in extensions:
			try:
				if ('Name[%s]' % locale) in extension:
					name = extension['Name[%s]' % locale]
				elif ('Name[%s]' % locale.split('_')[0]) in extension:
					name = extension['Name[%s]' % locale.split('_')[0]]
				else:
					name = extension['Name']
				data = {}
				for prop in ('FileFilter', 'DefaultExtension', 'Exec'):
					if 'X-ReText-'+prop in extension:
						data[prop] = extension['X-ReText-'+prop]
					elif prop in extension:
						data[prop] = extension[prop]
					else:
						data[prop] = ''
				action = self.act(name, trig=self.extensionFuntion(data))
				if 'Icon' in extension:
					action.setIcon(self.actIcon(extension['Icon']))
				mimetype = extension['MimeType'] if 'MimeType' in extension else None
			except KeyError:
				print('Failed to parse extension: Name is required', file=sys.stderr)
			else:
				self.extensionActions.append((action, mimetype))

	def updateExtensionsVisibility(self):
		markupClass = self.getMarkupClass()
		for action in self.extensionActions:
			if markupClass is None:
				action[0].setEnabled(False)
				continue
			mimetype = action[1]
			if mimetype == None:
				enabled = True
			elif markupClass == markups.MarkdownMarkup:
				enabled = (mimetype in ("text/x-retext-markdown", "text/x-markdown"))
			elif markupClass == markups.ReStructuredTextMarkup:
				enabled = (mimetype in ("text/x-retext-rst", "text/x-rst"))
			else:
				enabled = False
			action[0].setEnabled(enabled)

	def readExtension(self, fileName):
		extFile = QFile(fileName)
		extFile.open(QIODevice.ReadOnly)
		extension = {}
		stream = QTextStream(extFile)
		while not stream.atEnd():
			line = stream.readLine()
			if '=' in line:
				index = line.index('=')
				extension[line[:index].rstrip()] = line[index+1:].lstrip()
		extFile.close()
		return extension

	def openFile(self):
		supportedExtensions = ['.txt']
		for markup in markups.get_all_markups():
			supportedExtensions += markup.file_extensions
		fileFilter = ' (' + str.join(' ', ['*'+ext for ext in supportedExtensions]) + ');;'
		fileNames = QFileDialog.getOpenFileNames(self,
			self.tr("Select one or several files to open"), "",
			self.tr("Supported files") + fileFilter + self.tr("All files (*)"))
		for fileName in fileNames[0]:
			self.openFileWrapper(fileName)

	def openFileWrapper(self, fileName):
		if not fileName:
			return
		fileName = QFileInfo(fileName).canonicalFilePath()
		exists = False
		for i in range(self.tabWidget.count()):
			if self.fileNames[i] == fileName:
				exists = True
				ex = i
		if exists:
			self.tabWidget.setCurrentIndex(ex)
		elif QFile.exists(fileName):
			noEmptyTab = (
				(self.ind is None) or
				self.fileNames[self.ind] or
				self.editBoxes[self.ind].toPlainText() or
				self.editBoxes[self.ind].document().isModified()
			)
			if noEmptyTab:
				self.tabWidget.addTab(self.createTab(fileName), "")
				self.ind = self.tabWidget.count()-1
				self.tabWidget.setCurrentIndex(self.ind)
			if fileName:
				self.fileSystemWatcher.addPath(fileName)
			self.fileNames[self.ind] = fileName
			self.openFileMain()

	def openFileMain(self, encoding=None):
		openfile = QFile(self.fileNames[self.ind])
		openfile.open(QIODevice.ReadOnly)
		stream = QTextStream(openfile)
		if encoding:
			stream.setCodec(encoding)
		elif globalSettings.defaultCodec:
			stream.setCodec(globalSettings.defaultCodec)
		text = stream.readAll()
		openfile.close()
		markupClass = markups.get_markup_for_file_name(
			self.fileNames[self.ind], return_class=True)
		self.highlighters[self.ind].docType = (markupClass.name if markupClass else '')
		self.markups[self.ind] = self.getMarkup()
		if self.defaultMarkup:
			self.highlighters[self.ind].docType = self.defaultMarkup.name
		editBox = self.editBoxes[self.ind]
		modified = bool(encoding) and (editBox.toPlainText() != text)
		editBox.setPlainText(text)
		self.setCurrentFile()
		editBox.document().setModified(modified)
		self.setWindowModified(modified)

	def showEncodingDialog(self):
		if not self.maybeSave(self.ind):
			return
		encoding, ok = QInputDialog.getItem(self, '',
			self.tr('Select file encoding from the list:'),
			[bytes(b).decode() for b in QTextCodec.availableCodecs()],
			0, False)
		if ok:
			self.openFileMain(encoding)

	def saveFile(self):
		self.saveFileMain(dlg=False)

	def saveFileAs(self):
		self.saveFileMain(dlg=True)

	def saveAll(self):
		oldind = self.ind
		for self.ind in range(self.tabWidget.count()):
			if self.fileNames[self.ind] and QFileInfo(self.fileNames[self.ind]).isWritable():
				self.saveFileCore(self.fileNames[self.ind])
				self.editBoxes[self.ind].document().setModified(False)
		self.ind = oldind

	def saveFileMain(self, dlg):
		if (not self.fileNames[self.ind]) or dlg:
			markupClass = self.getMarkupClass()
			if (markupClass is None) or not hasattr(markupClass, 'default_extension'):
				defaultExt = self.tr("Plain text (*.txt)")
				ext = ".txt"
			else:
				defaultExt = self.tr('%s files',
					'Example of final string: Markdown files') \
					% markupClass.name + ' (' + str.join(' ',
					('*'+extension for extension in markupClass.file_extensions)) + ')'
				if markupClass == markups.MarkdownMarkup:
					ext = globalSettings.markdownDefaultFileExtension
				elif markupClass == markups.ReStructuredTextMarkup:
					ext = globalSettings.restDefaultFileExtension
				else:
					ext = markupClass.default_extension
			newFileName = QFileDialog.getSaveFileName(self,
				self.tr("Save file"), "", defaultExt)[0]
			if newFileName:
				if not QFileInfo(newFileName).suffix():
					newFileName += ext
				if self.fileNames[self.ind]:
					self.fileSystemWatcher.removePath(self.fileNames[self.ind])
				self.fileNames[self.ind] = newFileName
				self.actionSetEncoding.setDisabled(self.autoSaveActive())
		if self.fileNames[self.ind]:
			result = self.saveFileCore(self.fileNames[self.ind])
			if result:
				self.setCurrentFile()
				self.editBoxes[self.ind].document().setModified(False)
				self.setWindowModified(False)
				return True
			else:
				QMessageBox.warning(self, '',
				self.tr("Cannot save to file because it is read-only!"))
		return False

	def saveFileCore(self, fn, addToWatcher=True):
		self.fileSystemWatcher.removePath(fn)
		savefile = QFile(fn)
		result = savefile.open(QIODevice.WriteOnly)
		if result:
			savestream = QTextStream(savefile)
			if globalSettings.defaultCodec:
				savestream.setCodec(globalSettings.defaultCodec)
			savestream << self.editBoxes[self.ind].toPlainText()
			savefile.close()
		if result and addToWatcher:
			self.fileSystemWatcher.addPath(fn)
		return result

	def saveHtml(self, fileName):
		if not QFileInfo(fileName).suffix():
			fileName += ".html"
		try:
			htmltext = self.getHtml(includeStyleSheet=False, includeMeta=True,
			webenv=True)
		except Exception:
			return self.printError()
		htmlFile = QFile(fileName)
		htmlFile.open(QIODevice.WriteOnly)
		html = QTextStream(htmlFile)
		if globalSettings.defaultCodec:
			html.setCodec(globalSettings.defaultCodec)
		html << htmltext
		htmlFile.close()

	def textDocument(self):
		td = QTextDocument()
		td.setMetaInformation(QTextDocument.DocumentTitle, self.getDocumentTitle())
		if self.ss:
			td.setDefaultStyleSheet(self.ss)
		td.setHtml(self.getHtml())
		td.setDefaultFont(globalSettings.font)
		return td

	def saveOdf(self):
		try:
			document = self.textDocument()
		except Exception:
			return self.printError()
		fileName = QFileDialog.getSaveFileName(self,
			self.tr("Export document to ODT"), "",
			self.tr("OpenDocument text files (*.odt)"))[0]
		if not QFileInfo(fileName).suffix():
			fileName += ".odt"
		writer = QTextDocumentWriter(fileName)
		writer.setFormat("odf")
		writer.write(document)

	def saveFileHtml(self):
		fileName = QFileDialog.getSaveFileName(self,
			self.tr("Save file"), "",
			self.tr("HTML files (*.html *.htm)"))[0]
		if fileName:
			self.saveHtml(fileName)

	def getDocumentForPrint(self):
		if globalSettings.useWebKit:
			return self.previewBoxes[self.ind]
		try:
			return self.textDocument()
		except Exception:
			self.printError()

	def standardPrinter(self):
		printer = QPrinter(QPrinter.HighResolution)
		printer.setDocName(self.getDocumentTitle())
		printer.setCreator('ReText %s' % app_version)
		return printer

	def savePdf(self):
		self.updatePreviewBox()
		fileName = QFileDialog.getSaveFileName(self,
			self.tr("Export document to PDF"),
			"", self.tr("PDF files (*.pdf)"))[0]
		if fileName:
			if not QFileInfo(fileName).suffix():
				fileName += ".pdf"
			printer = self.standardPrinter()
			printer.setOutputFormat(QPrinter.PdfFormat)
			printer.setOutputFileName(fileName)
			document = self.getDocumentForPrint()
			if document != None:
				document.print(printer)

	def printFile(self):
		self.updatePreviewBox()
		printer = self.standardPrinter()
		dlg = QPrintDialog(printer, self)
		dlg.setWindowTitle(self.tr("Print document"))
		if (dlg.exec() == QDialog.Accepted):
			document = self.getDocumentForPrint()
			if document != None:
				document.print(printer)

	def printPreview(self):
		document = self.getDocumentForPrint()
		if document == None:
			return
		printer = self.standardPrinter()
		preview = QPrintPreviewDialog(printer, self)
		preview.paintRequested.connect(document.print)
		preview.exec()

	def runExtensionCommand(self, command, filefilter, defaultext):
		of = ('%of' in command)
		html = ('%html' in command)
		if of:
			if defaultext and not filefilter:
				filefilter = '*'+defaultext
			fileName = QFileDialog.getSaveFileName(self,
				self.tr('Export document'), '', filefilter)[0]
			if not fileName:
				return
			if defaultext and not QFileInfo(fileName).suffix():
				fileName += defaultext
		basename = '.%s.retext-temp' % self.getDocumentTitle(baseName=True)
		if html:
			tmpname = basename+'.html'
			self.saveHtml(tmpname)
		else:
			tmpname = basename+self.getMarkupClass().default_extension
			self.saveFileCore(tmpname, addToWatcher=False)
		command = command.replace('%of', '"out'+defaultext+'"')
		command = command.replace('%html' if html else '%if', '"'+tmpname+'"')
		try:
			Popen(str(command), shell=True).wait()
		except Exception as error:
			errorstr = str(error)
			QMessageBox.warning(self, '', self.tr('Failed to execute the command:')
			+ '\n' + errorstr)
		QFile(tmpname).remove()
		if of:
			QFile('out'+defaultext).rename(fileName)

	def getDocumentTitle(self, baseName=False):
		markup = self.markups[self.ind]
		realTitle = ''
		if markup and not baseName:
			text = self.editBoxes[self.ind].toPlainText()
			try:
				realTitle = markup.get_document_title(text)
			except Exception:
				self.printError()
		if realTitle:
			return realTitle
		elif self.fileNames[self.ind]:
			fileinfo = QFileInfo(self.fileNames[self.ind])
			basename = fileinfo.completeBaseName()
			return (basename if basename else fileinfo.fileName())
		return self.tr("New document")

	def autoSaveActive(self):
		return self.autoSaveEnabled and self.fileNames[self.ind] and \
		QFileInfo(self.fileNames[self.ind]).isWritable()

	def modificationChanged(self, changed):
		if self.autoSaveActive():
			changed = False
		self.actionSave.setEnabled(changed)
		self.setWindowModified(changed)

	def clipboardDataChanged(self):
		mimeData = QApplication.instance().clipboard().mimeData()
		if mimeData is not None:
			self.actionPaste.setEnabled(mimeData.hasText())

	def insertChars(self, chars):
		tc = self.editBoxes[self.ind].textCursor()
		if tc.hasSelection():
			selection = tc.selectedText()
			if selection.startswith(chars) and selection.endswith(chars):
				if len(selection) > 2*len(chars):
					selection = selection[len(chars):-len(chars)]
					tc.insertText(selection)
			else:
				tc.insertText(chars+tc.selectedText()+chars)
		else:
			tc.insertText(chars)

	def insertTag(self, ut):
		if not ut:
			return
		if isinstance(ut, int):
			ut = self.usefulTags[ut - 1]
		arg = ' style=""' if ut == 'span' else ''
		tc = self.editBoxes[self.ind].textCursor()
		if ut == 'img':
			toinsert = ('<a href="' + tc.selectedText() +
			'" target="_blank"><img src="' + tc.selectedText() + '"/></a>')
		elif ut == 'a':
			toinsert = ('<a href="' + tc.selectedText() +
			'" target="_blank">' + tc.selectedText() + '</a>')
		else:
			toinsert = '<'+ut+arg+'>'+tc.selectedText()+'</'+ut+'>'
		tc.insertText(toinsert)
		self.tagsBox.setCurrentIndex(0)

	def insertSymbol(self, num):
		if num:
			self.editBoxes[self.ind].insertPlainText('&'+self.usefulChars[num-1]+';')
		self.symbolBox.setCurrentIndex(0)

	def fileChanged(self, fileName):
		ind = self.fileNames.index(fileName)
		self.tabWidget.setCurrentIndex(ind)
		if not QFile.exists(fileName):
			self.editBoxes[ind].document().setModified(True)
			QMessageBox.warning(self, '', self.tr(
				'This file has been deleted by other application.\n'
				'Please make sure you save the file before exit.'))
		elif not self.editBoxes[ind].document().isModified():
			# File was not modified in ReText, reload silently
			self.openFileMain()
			self.updatePreviewBox()
		else:
			text = self.tr(
				'This document has been modified by other application.\n'
				'Do you want to reload the file (this will discard all '
				'your changes)?\n')
			if self.autoSaveEnabled:
				text += self.tr(
					'If you choose to not reload the file, auto save mode will '
					'be disabled for this session to prevent data loss.')
			messageBox = QMessageBox(QMessageBox.Warning, '', text)
			reloadButton = messageBox.addButton(self.tr('Reload'), QMessageBox.YesRole)
			messageBox.addButton(QMessageBox.Cancel)
			messageBox.exec()
			if messageBox.clickedButton() is reloadButton:
				self.openFileMain()
				self.updatePreviewBox()
			else:
				self.autoSaveEnabled = False
				self.editBoxes[ind].document().setModified(True)
		if fileName not in self.fileSystemWatcher.files():
			# https://github.com/retext-project/retext/issues/137
			self.fileSystemWatcher.addPath(fileName)

	def maybeSave(self, ind):
		if self.autoSaveActive():
			self.saveFileCore(self.fileNames[self.ind])
			return True
		if not self.editBoxes[ind].document().isModified():
			return True
		self.tabWidget.setCurrentIndex(ind)
		ret = QMessageBox.warning(self, '',
			self.tr("The document has been modified.\nDo you want to save your changes?"),
			QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel)
		if ret == QMessageBox.Save:
			return self.saveFileMain(False)
		elif ret == QMessageBox.Cancel:
			return False
		return True

	def closeEvent(self, closeevent):
		for self.ind in range(self.tabWidget.count()):
			if not self.maybeSave(self.ind):
				return closeevent.ignore()
		if globalSettings.saveWindowGeometry and not self.isMaximized():
			globalSettings.windowGeometry = self.saveGeometry()
		closeevent.accept()

	def viewHtml(self):
		htmlDlg = HtmlDialog(self)
		try:
			htmltext = self.getHtml(includeStyleSheet=False, includeTitle=False)
		except Exception:
			return self.printError()
		winTitle = self.getDocumentTitle(baseName=True)
		htmlDlg.setWindowTitle(winTitle+" ("+self.tr("HTML code")+")")
		htmlDlg.textEdit.setPlainText(htmltext.rstrip())
		htmlDlg.hl.rehighlight()
		htmlDlg.show()
		htmlDlg.raise_()
		htmlDlg.activateWindow()

	def openHelp(self):
		QDesktopServices.openUrl(QUrl('https://github.com/retext-project/retext/wiki'))

	def aboutDialog(self):
		QMessageBox.about(self, self.aboutWindowTitle,
		'<p><b>' + (self.tr('ReText %s (using PyMarkups %s)') % (app_version, markups.__version__))
		+'</b></p>' + self.tr('Simple but powerful editor'
		' for Markdown and reStructuredText')
		+'</p><p>'+self.tr('Author: Dmitry Shachnev, 2011').replace('2011', '2011\u2013' '2015')
		+'<br><a href="https://github.com/retext-project/retext">'+self.tr('Website')
		+'</a> | <a href="http://daringfireball.net/projects/markdown/syntax">'
		+self.tr('Markdown syntax')
		+'</a> | <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">'
		+self.tr('reStructuredText syntax')+'</a></p>')

	def setDefaultMarkup(self, markup):
		self.defaultMarkup = markup
		defaultName = markups.get_available_markups()[0].name
		writeToSettings('defaultMarkup', markup.name, defaultName)
		oldind = self.ind
		for self.ind in range(len(self.previewBoxes)):
			self.docTypeChanged()
		self.ind = oldind
Exemple #20
0
class Scene:
    """Container object for the scene graph

    The main purpose of this class is to provide the root SceneNode.
    """

    def __init__(self) -> None:
        super().__init__()

        from UM.Scene.SceneNode import SceneNode
        self._root = SceneNode(name = "Root")
        self._root.setCalculateBoundingBox(False)
        self._connectSignalsRoot()
        self._active_camera = None  # type: Optional[Camera]
        self._ignore_scene_changes = False
        self._lock = threading.Lock()

        # Watching file for changes.
        self._file_watcher = QFileSystemWatcher()
        self._file_watcher.fileChanged.connect(self._onFileChanged)

        self._reload_message = None  # type: Optional[Message]
        self._callbacks = set() # type: Set[Callable] # Need to keep these in memory. This is a memory leak every time you refresh, but a tiny one.

    def _connectSignalsRoot(self) -> None:
        self._root.transformationChanged.connect(self.sceneChanged)
        self._root.childrenChanged.connect(self.sceneChanged)
        self._root.meshDataChanged.connect(self.sceneChanged)

    def _disconnectSignalsRoot(self) -> None:
        self._root.transformationChanged.disconnect(self.sceneChanged)
        self._root.childrenChanged.disconnect(self.sceneChanged)
        self._root.meshDataChanged.disconnect(self.sceneChanged)

    def setIgnoreSceneChanges(self, ignore_scene_changes: bool) -> None:
        if self._ignore_scene_changes != ignore_scene_changes:
            self._ignore_scene_changes = ignore_scene_changes
            if self._ignore_scene_changes:
                self._disconnectSignalsRoot()
            else:
                self._connectSignalsRoot()

    @deprecated("Scene lock is no longer used", "4.5")
    def getSceneLock(self) -> threading.Lock:
        return self._lock

    def getRoot(self) -> "SceneNode":
        """Get the root node of the scene."""

        return self._root

    def setRoot(self, node: "SceneNode") -> None:
        """Change the root node of the scene"""

        if self._root != node:
            if not self._ignore_scene_changes:
                self._disconnectSignalsRoot()
            self._root = node
            if not self._ignore_scene_changes:
                self._connectSignalsRoot()
            self.rootChanged.emit()

    rootChanged = Signal()

    def getActiveCamera(self) -> Optional[Camera]:
        """Get the camera that should be used for rendering."""

        return self._active_camera

    def getAllCameras(self) -> List[Camera]:
        cameras = []
        for node in BreadthFirstIterator(self._root):
            if isinstance(node, Camera):
                cameras.append(node)
        return cameras

    def setActiveCamera(self, name: str) -> None:
        """Set the camera that should be used for rendering.

        :param name: The name of the camera to use.
        """

        camera = self.findCamera(name)
        if camera and camera != self._active_camera:
            if self._active_camera:
                self._active_camera.perspectiveChanged.disconnect(self.sceneChanged)
            self._active_camera = camera
            self._active_camera.perspectiveChanged.connect(self.sceneChanged)
        else:
            Logger.log("w", "Couldn't find camera with name [%s] to activate!" % name)

    sceneChanged = Signal()
    """Signal that is emitted whenever something in the scene changes.

    :param object: The object that triggered the change.
    """

    def findObject(self, object_id: int) -> Optional["SceneNode"]:
        """Find an object by id.

        :param object_id: The id of the object to search for, as returned by the python id() method.

        :return: The object if found, or None if not.
        """

        for node in BreadthFirstIterator(self._root):
            if id(node) == object_id:
                return node
        return None

    def findCamera(self, name: str) -> Optional[Camera]:
        for node in BreadthFirstIterator(self._root):
            if isinstance(node, Camera) and node.getName() == name:
                return node
        return None

    def addWatchedFile(self, file_path: str) -> None:
        """Add a file to be watched for changes.

        :param file_path: The path to the file that must be watched.
        """

        # File watcher causes cura to crash on windows if threaded from removable device (usb, ...). Create QEventLoop earlier to fix this.
        if Platform.isWindows():
            QEventLoop()
        self._file_watcher.addPath(file_path)

    def removeWatchedFile(self, file_path: str) -> None:
        """Remove a file so that it will no longer be watched for changes.

        :param file_path: The path to the file that must no longer be watched.
        """

        self._file_watcher.removePath(file_path)

    def _onFileChanged(self, file_path: str) -> None:
        """Triggered whenever a file is changed that we currently have loaded."""

        try:
            if os.path.getsize(file_path) == 0:  # File is empty.
                return
        except EnvironmentError:  # Or it doesn't exist any more, or we have no access any more.
            return

        # Multiple nodes may be loaded from the same file at different stages. Reload them all.
        from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator  # To find which nodes to reload when files have changed.
        modified_nodes = [node for node in DepthFirstIterator(self.getRoot()) if node.getMeshData() and node.getMeshData().getFileName() == file_path]  # type: ignore

        if modified_nodes:
            # Hide the message if it was already visible
            if self._reload_message is not None:
                self._reload_message.hide()

            self._reload_message = Message(i18n_catalog.i18nc("@info", "Would you like to reload {filename}?").format(filename = os.path.basename(file_path)),
                              title = i18n_catalog.i18nc("@info:title", "File has been modified"))
            self._reload_message.addAction("reload", i18n_catalog.i18nc("@action:button", "Reload"), icon = "", description = i18n_catalog.i18nc("@action:description", "This will trigger the modified files to reload from disk."))
            self._reload_callback = functools.partial(self._reloadNodes, modified_nodes)
            self._reload_message.actionTriggered.connect(self._reload_callback)
            self._reload_message.show()

    def _reloadNodes(self, nodes: List["SceneNode"], message: str, action: str) -> None:
        """Reloads a list of nodes after the user pressed the "Reload" button.

        :param nodes: The list of nodes that needs to be reloaded.
        :param message: The message that triggered the action to reload them.
        :param action: The button that triggered the action to reload them.
        """

        if action != "reload":
            return
        if self._reload_message is not None:
            self._reload_message.hide()
        for node in nodes:
            meshdata = node.getMeshData()
            if meshdata:
                filename = meshdata.getFileName()
                if not filename or not os.path.isfile(filename):  # File doesn't exist any more.
                    continue
                job = ReadMeshJob(filename)
                reload_finished_callback = functools.partial(self._reloadJobFinished, node)
                self._callbacks.add(reload_finished_callback) #Store it so it won't get garbage collected. This is a memory leak, but just one partial per reload so it's not much.
                job.finished.connect(reload_finished_callback)
                job.start()

    def _reloadJobFinished(self, replaced_node: SceneNode, job: ReadMeshJob) -> None:
        """Triggered when reloading has finished.

        This then puts the resulting mesh data in the node.
        """

        for node in job.getResult():
            mesh_data = node.getMeshData()
            if mesh_data:
                replaced_node.setMeshData(mesh_data)
            else:
                Logger.log("w", "Could not find a mesh in reloaded node.")
class ThunderDownloader(QObject):
    """
    a class for download using thunder on windows
    """
    WIN_THUNDER_EXE = "Thunder.exe"
    WIN_THUNDER_TASK_EXE = "ThunderNewTask.exe"

    # emit when the url is downloaded
    # parame: url(str), path(str)
    sigUrlDownloaded = pyqtSignal(str, str)

    def __init__(self, *args):
        super(ThunderDownloader, self).__init__(*args)
        self._thunder_path = AppConfig().get_download_config().get(
            "thunder_path", "")
        self._save_path = AppConfig().get_download_config().get(
            "save_path", "")
        self._thunder_process = None
        self._thunder_task_process = None
        self._download_urls = []
        self._file_watcher = QFileSystemWatcher(self)
        self._file_watcher.addPath(self._save_path)
        self._file_watcher.directoryChanged.connect(self._on_file_dir_changed)

    def thunder_exe_download(self, url):
        """
        execute the download task by progame installed in windows system
        @parame: url
        """
        if self._thunder_process is None:
            self._thunder_process = QProcess(self)
            self._thunder_process.start(
                os.path.join(self._thunder_path,
                             ThunderDownloader.WIN_THUNDER_EXE),
                [
                    "-silent", "-noshowfloatpanel", "-noshownewtaskdlg",
                    "-StartFrom:XLDownloadClient",
                    "-StartType:XLDownloadClient",
                    "-ClientProcess:ThunderNewTask.exe"
                ])

        if self._thunder_process.waitForStarted(60000):
            self.thunder_add_task(url)
        else:
            print("failt to start thunder process %s" % url)

    def thunder_add_task(self, url):
        """
        @parame url
        """
        QProcess.execute(
            os.path.join(self._thunder_path,
                         ThunderDownloader.WIN_THUNDER_TASK_EXE), [url])
        print("start add task %s" % url)

    def thunder_sdk_download(self, url):
        """
        TODO: download url by sdk interface
        """
        pass

    # def download(self, urls):
    #     """
    #     download scheduler
    #     """
    #     event_loop = asyncio.get_event_loop()

    #     try:
    #         coroutine = [self.thunder_exe_download(url) for url in urls]
    #         event_loop.run_until_complete(asyncio.wait(coroutine))
    #     finally:
    #         event_loop.close()

    def _on_file_dir_changed(self, path):
        """
        @parame path
        """
        files = os.listdir(path)
        for url in iter(self._download_urls):
            file_name = os.path.basename(url)
            if file_name in files:
                self.sigUrlDownloaded.emit(url, os.path.join(path, file_name))
                self._download_urls.remove(url)
                print("file(%s) is downloaded." % file_name)

    def download(self, url):
        """
        download scheduler
        """
        # check whether file is existed
        file_name = os.path.basename(url)
        if file_name in os.listdir(self._save_path):
            return False

        try:
            self.thunder_exe_download(url)
        except:
            print("some error happend when download %s" % url)
            return False
        else:
            self._download_urls.append(url)
            return True
class ImageLabel(QLabel):

	sizeChanged = pyqtSignal(int, int)

	def __init__(self, parent=None):
		super(ImageLabel, self).__init__(parent)
		self.setBackgroundRole(QPalette.Base)
		self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
		self.setScaledContents(True)
		self.setAcceptDrops(True)
		self.setAlignment(Qt.AlignCenter)

	def dragEnterEvent(self, event):
		self.setBackgroundRole(QPalette.Highlight)
		event.acceptProposedAction()

	def dragMoveEvent(self, event):
		event.acceptProposedAction()

	def dropEvent(self, event):
		# Get path of dropped file
		mimeData = event.mimeData()
		if mimeData.hasUrls():
			filename = mimeData.urls()[0].toLocalFile()
			self.load(filename)
		event.acceptProposedAction()

	def dragLeaveEvent(self, event):
		self.clear()
		event.accept()

	def load(self, filename):
		# Load HDR image with FreeImage
		image = self.loadHDRImage(filename)
		if image is None:
			return False

		# Change window size
		self.sizeChanged.emit(image.width(), image.height())

		# Set the image to imageLabel
		self.setPixmap(QPixmap.fromImage(image))
		self.adjustSize()

		# Begin to watch modification
		self.watcher = QFileSystemWatcher()
		self.watcher.addPath(filename)
		self.watcher.fileChanged.connect(self.onFileChanged)

		return True

	def loadHDRImage(self, filename):
		try:
			# Load image
			img = fi.Image(filename).flipVertical()
			floats = array.array("f", img.getRaw())
			imageArray = np.array(floats).reshape((img.width, img.height, 3))

			# HDR compression
			imageArray_RGB8 = (np.clip(np.power(imageArray, 1/2.2), 0, 1) * 255).astype(np.uint8)

			# Convert to QImage
			return QImage(imageArray_RGB8.tostring(), img.width, img.height, QImage.Format_RGB888)

		except fi.FreeImageError:
			return None

	def onFileChanged(self, path):
		if os.path.isfile(path):
			self.load(path)
		else:
			self.clear()
			self.sizeChanged.emit(200, 200)
Exemple #23
0
class PugdebugDocuments(QObject):

    watcher = None

    open_documents = {}

    document_changed = pyqtSignal(object)

    def __init__(self):
        super(PugdebugDocuments, self).__init__()

        self.watcher = QFileSystemWatcher()
        self.watcher.fileChanged.connect(self.handle_file_changed)

    def open_document(self, path):
        path_key = self.get_path_key(path)

        document = PugdebugDocument(path)
        self.open_documents[path_key] = document

        self.watcher.addPath(path)

        return document

    def close_document(self, path):
        path_key = self.get_path_key(path)
        self.open_documents.pop(path_key, None)

        self.watcher.removePath(path)

    def refresh_document(self, path):
        """Refresh a document

        Gets called when the file system watcher notices
        a change to an open document.
        """
        path_key = self.get_path_key(path)
        document = self.open_documents[path_key]

        document.read_file(path)

        self.document_changed.emit(document)

    def is_document_open(self, path):
        path_key = self.get_path_key(path)
        return path_key in self.open_documents

    def handle_file_changed(self, path):
        """Handle when a watched file gets changed

        Crazy stuff ahead.

        If a file is modified, some editors (systems?) will first remove
        the file and then write it back to the disk. And for that split
        second, the watcher will drop the file from being watched.

        But then again, maybe that file really got deleted? Who knows?!

        Anyway, when a file gets modified, we sleep a short while to
        see if that file will "get back" and if so, add it back to the
        watcher. If not, we'll assume the file got deleted.
        """
        if not self.__is_path_watched(path):
            fileinfo = QFileInfo(path)

            total_slept = 0
            file_exists = fileinfo.exists()

            while not file_exists:
                sleep_for = 0.1
                total_slept += sleep_for

                if total_slept > 1:
                    break

                time.sleep(sleep_for)
                file_exists = fileinfo.exists()

            if file_exists:
                self.watcher.addPath(path)

                self.refresh_document(path)
            else:
                # file got deleted?
                pass

    def get_path_key(self, path):
        path_key = hashlib.md5(path.encode("utf-8"))
        return path_key.hexdigest()

    def __is_path_watched(self, path):
        return path in self.watcher.files()
Exemple #24
0
class Okuyucu(QDialog):
    MESAJ_DIZINI = "./mesajlar/"
    BILDIRIM_SUREC = os.getcwd() + "/" + "bildirim"
    BILDIRIM_SUREC_KAPAT = "killall -9 " + os.getcwd() + "/" + "bildirim"

    def __init__(self, ebeveyn=None):
        super(Okuyucu, self).__init__(ebeveyn)
        kutu = QVBoxLayout()
        self.setLayout(kutu)
        kutu.setContentsMargins(0, 0, 0, 0)

        self.sistem_cekmecesi = QSystemTrayIcon(self)
        self.sistem_cekmecesi.setIcon(QIcon("./icons/milis-bildirim.png"))
        self.sistem_cekmecesi.activated.connect(self.sistem_cekmecesi_tiklandi)
        self.sistem_cekmecesi.show()

        self.menu_cekmece = QMenu(self)
        self.mesaj_oku_aksiyon = QAction("Mesaj Oku",
                                         self,
                                         statusTip="MesajOku",
                                         triggered=self.mesaj_oku_fonk)
        self.mesaj_gonder_aksiyon = QAction("Mesaj Gönder",
                                            self,
                                            statusTip="MesajGonder",
                                            triggered=self.mesaj_gonder_fonk)
        self.ayarlar_aksiyon = QAction("Ayarlar",
                                       self,
                                       statusTip="Ayarlar",
                                       triggered=self.ayarlar_fonk)
        self.kapat_aksiyon = QAction("Kapat",
                                     self,
                                     statusTip="Kapat",
                                     triggered=self.kapat_fonk)
        self.menu_cekmece.addAction(self.mesaj_oku_aksiyon)
        self.menu_cekmece.addAction(self.mesaj_gonder_aksiyon)
        self.menu_cekmece.addAction(self.ayarlar_aksiyon)
        self.menu_cekmece.addAction(self.kapat_aksiyon)
        self.sistem_cekmecesi.setContextMenu(self.menu_cekmece)
        self.sistem_cekmecesi.showMessage(
            "Çalıştı", "Milis Bildirim Başarıyla Çalıştırıldı",
            QSystemTrayIcon.MessageIcon(1), 5000)

        self.mesaj_liste = QListWidget()
        self.mesaj_liste.setSelectionMode(QListView.ExtendedSelection)
        kutu.addWidget(self.mesaj_liste)

        self.setFixedWidth(285)
        s = QDesktopWidget().screenGeometry(0)
        self.setFixedHeight(s.height())
        self.setWindowFlags(Qt.Popup)
        self.move(s.width() - 285, 0)

        ################################
        self.settings = QSettings()
        self.okunmus_mesajlar = self.settings.value("liste", [], str) or []
        self.tum_mesajlar = os.listdir(self.MESAJ_DIZINI)
        self.varolan_mesajlar = []
        try:
            self.gecersizleri_goster = int(
                self.settings.value("gecersizleri_goster"))
            self.anonimleri_goster = int(
                self.settings.value("anonimleri_goster"))
        except:
            self.gecersizleri_goster = 1
            self.anonimleri_goster = 1
        ################################
        self.tum_mesajlar_fonk()
        self.dosya_izleyici = QFileSystemWatcher()
        self.dosya_izleyici.addPath(self.MESAJ_DIZINI)
        self.dosya_izleyici.directoryChanged.connect(self.tum_mesajlar_fonk)
        #################################
        # Bildirim çalıştırılıyor       #
        #################################
        zamanlayici = QTimer(self)
        zamanlayici.setInterval(6000)
        zamanlayici.timeout.connect(self.bildirim_calistir)
        zamanlayici.start()

    def mesaj_gonder_fonk(self):
        pencere_gonder = gonder.Gonderici(self)
        pencere_gonder.show()

    def closeEvent(self, event):
        self.settings.setValue('liste', self.okunmus_mesajlar)
        self.settings.sync()

    def ayarlar_fonk(self):
        ayarlar_pencere = ayarlarui.Ayarlar(self)
        ayarlar_pencere.show()

    def kapat_fonk(self):
        os.system(self.BILDIRIM_SUREC_KAPAT)
        qApp.quit()

    def sistem_cekmecesi_tiklandi(self, value):
        if value == self.sistem_cekmecesi.DoubleClick:
            self.mesaj_oku_fonk()

    def mesaj_oku_fonk(self):
        self.sistem_cekmecesi.setIcon(QIcon("./icons/milis-bildirim.png"))
        self.show()

    def bildirim_calistir(self):
        print("######################")
        print(self.surec_kontrol())
        if not self.surec_kontrol():
            subprocess.Popen([self.BILDIRIM_SUREC])

    def surec_kontrol(self):
        _surec = os.popen("ps -Af").read()
        surec_adet = _surec.count(self.BILDIRIM_SUREC)
        if surec_adet > 0:
            return True
        return False

    def imza_kontrol(self, konum, dosya):
        sgd = "/tmp/" + dosya + ".cikti"
        dogrulama = "/tmp/" + dosya + ".dogrula"
        os.system("rm -rf " + sgd)
        os.system("rm -rf " + dogrulama)
        os.system("gpg --output " + sgd + " " + konum + dosya)
        os.system("gpg --status-fd 1 --no-default-keyring --verify " + konum +
                  dosya + " > " + dogrulama)
        yol = konum + dosya
        gonderen = "anonim"
        gonderen_onay = "geçersiz"
        with open(dogrulama) as f:
            satirlar = f.readlines()
        durum = False
        if os.path.isfile(sgd):
            yol = sgd
            durum = True
            for satir in satirlar:
                if "ERRSIG" in satir:
                    gonderen = satir.split()[2]
                    break
                if "GOODSIG" in satir:
                    gonderen = satir.split()[-1]
                    gonderen_onay = "geçerli"
                    break
        os.system("rm -rf " + dogrulama)
        return durum, yol, gonderen, gonderen_onay

    def yaml_oku(self, dosya):
        durum, yol, gonderen, gonderen_onay = self.imza_kontrol(
            self.MESAJ_DIZINI, dosya)
        with open(yol, 'r') as f:
            okunan = yaml.load(f)
        return okunan, gonderen, gonderen_onay

    def tum_mesajlar_fonk(self):
        self.varolan_mesajlar = self.tum_mesajlar
        self.mesaj_liste.clear()

        duzenli_mesajlar = self.mesajlar_oku_sirala()
        mesajlar = duzenli_mesajlar.keys()
        sirali_mesajlar = list(mesajlar)
        sirali_mesajlar.sort()
        sirali_mesajlar = sirali_mesajlar[::-1]
        if len(sirali_mesajlar) != 0:
            for mesaj in sirali_mesajlar:
                mesaj_ = duzenli_mesajlar[mesaj]
                ozel_widget = listemadddesi.OzelListeMaddesi(self)
                ozel_widget.okuyucu()
                ozel_widget.mesaj_id_ekle(mesaj_[2])
                ozel_widget.mesaj_tipi_ekle(mesaj_[0])
                ozel_widget.mesaj_ekle(mesaj_[1])
                ozel_widget.tarih_ekle(mesaj)
                ozel_widget.gonderen_ekle(mesaj_[3])
                ozel_widget.gonderen_onay_ekle(mesaj_[4])
                if mesaj_[2] in self.okunmus_mesajlar:
                    ozel_widget.okunma_degistir("okundu")
                else:
                    ozel_widget.okunma_degistir("okunmadi")
                ozel_widget_item = QListWidgetItem(self.mesaj_liste)
                ozel_widget_item.setSizeHint(ozel_widget.sizeHint())
                self.mesaj_liste.setItemWidget(ozel_widget_item, ozel_widget)

                if mesaj_[2] not in self.varolan_mesajlar:
                    if mesaj_[0] == "bilgi":
                        icon = 1
                    elif mesaj_[0] == "sistem":
                        icon = 2
                    elif mesaj_[0] == "kritik":
                        icon = 3
                    else:
                        icon = 0
                    self.sistem_cekmecesi.showMessage(
                        mesaj_[0], mesaj_[1],
                        QSystemTrayIcon.MessageIcon(icon), 5000)
                    self.sistem_cekmecesi.setIcon(
                        QIcon("./icons/milis-bildirim-m.png"))

    def mesajlar_oku_sirala(self):
        duzenli_mesajlar = {}
        mesajlar = os.listdir(self.MESAJ_DIZINI)
        self.tum_mesajlar = mesajlar
        for mesaj in mesajlar:
            okunan, gonderen, gonderen_onay = self.yaml_oku(mesaj)
            if gonderen_onay == "geçersiz" and self.gecersizleri_goster == False:
                pass
            elif gonderen == "anonim" and self.anonimleri_goster == False:
                pass
            else:
                if okunan == None:
                    pass
                try:
                    mesaj_tipi = okunan["mesaj_tipi"]
                except:
                    mesaj_tipi = ""
                try:
                    mesaj_metni = okunan["mesaj"]
                except:
                    mesaj_metni = ""
                try:
                    mesaj_tarihi = okunan["tarih"]
                except:
                    mesaj_tarihi = ""
                if gonderen[0] == "<" and gonderen[-1] == ">":
                    gonderen = gonderen[1:-1]
                duzenli_mesajlar[mesaj_tarihi] = [
                    mesaj_tipi, mesaj_metni, mesaj, gonderen, gonderen_onay
                ]
        return duzenli_mesajlar
Exemple #25
0
class Scene:
    def __init__(self) -> None:
        super().__init__()  # Call super to make multiple inheritance work.

        from UM.Scene.SceneNode import SceneNode
        self._root = SceneNode(name="Root")
        self._root.setCalculateBoundingBox(False)
        self._connectSignalsRoot()
        self._active_camera = None  # type: Optional[Camera]
        self._ignore_scene_changes = False
        self._lock = threading.Lock()

        # Watching file for changes.
        self._file_watcher = QFileSystemWatcher()
        self._file_watcher.fileChanged.connect(self._onFileChanged)

    def _connectSignalsRoot(self) -> None:
        self._root.transformationChanged.connect(self.sceneChanged)
        self._root.childrenChanged.connect(self.sceneChanged)
        self._root.meshDataChanged.connect(self.sceneChanged)

    def _disconnectSignalsRoot(self) -> None:
        self._root.transformationChanged.disconnect(self.sceneChanged)
        self._root.childrenChanged.disconnect(self.sceneChanged)
        self._root.meshDataChanged.disconnect(self.sceneChanged)

    def setIgnoreSceneChanges(self, ignore_scene_changes: bool) -> None:
        if self._ignore_scene_changes != ignore_scene_changes:
            self._ignore_scene_changes = ignore_scene_changes
            if self._ignore_scene_changes:
                self._disconnectSignalsRoot()
            else:
                self._connectSignalsRoot()

    ##  Acquire the global scene lock.
    #
    #   This will prevent any read or write actions on the scene from other threads,
    #   assuming those threads also properly acquire the lock. Most notably, this
    #   prevents the rendering thread from rendering the scene while it is changing.
    #   Deprecated, use getSceneLock() instead.
    @deprecated("Please use the getSceneLock instead", "3.3")
    def acquireLock(self) -> None:
        self._lock.acquire()

    ##  Release the global scene lock.
    #   Deprecated, use getSceneLock() instead.
    @deprecated("Please use the getSceneLock instead", "3.3")
    def releaseLock(self) -> None:
        self._lock.release()

    ##  Gets the global scene lock.
    #
    #   Use this lock to prevent any read or write actions on the scene from other threads,
    #   assuming those threads also properly acquire the lock. Most notably, this
    #   prevents the rendering thread from rendering the scene while it is changing.
    def getSceneLock(self) -> threading.Lock:
        return self._lock

    ##  Get the root node of the scene.
    def getRoot(self) -> "SceneNode":
        return self._root

    ##  Change the root node of the scene
    def setRoot(self, node: "SceneNode") -> None:
        if self._root != node:
            if not self._ignore_scene_changes:
                self._disconnectSignalsRoot()
            self._root = node
            if not self._ignore_scene_changes:
                self._connectSignalsRoot()
            self.rootChanged.emit()

    rootChanged = Signal()

    ##  Get the camera that should be used for rendering.
    def getActiveCamera(self) -> Optional[Camera]:
        return self._active_camera

    def getAllCameras(self) -> List[Camera]:
        cameras = []
        for node in BreadthFirstIterator(self._root):  # type: ignore
            if isinstance(node, Camera):
                cameras.append(node)
        return cameras

    ##  Set the camera that should be used for rendering.
    #   \param name The name of the camera to use.
    def setActiveCamera(self, name: str) -> None:
        camera = self.findCamera(name)
        if camera:
            self._active_camera = camera
        else:
            Logger.log(
                "w", "Couldn't find camera with name [%s] to activate!" % name)

    ##  Signal that is emitted whenever something in the scene changes.
    #   \param object The object that triggered the change.
    sceneChanged = Signal()

    ##  Find an object by id.
    #
    #   \param object_id The id of the object to search for, as returned by the python id() method.
    #
    #   \return The object if found, or None if not.
    def findObject(self, object_id: int) -> Optional["SceneNode"]:
        for node in BreadthFirstIterator(self._root):  # type: ignore
            if id(node) == object_id:
                return node
        return None

    def findCamera(self, name: str) -> Optional[Camera]:
        for node in BreadthFirstIterator(self._root):  # type: ignore
            if isinstance(node, Camera) and node.getName() == name:
                return node
        return None

    ##  Add a file to be watched for changes.
    #   \param file_path The path to the file that must be watched.
    def addWatchedFile(self, file_path: str) -> None:
        # The QT 5.10.0 issue, only on Windows. Cura crashes after loading a stl file from USB/sd-card/Cloud-based drive
        if not Platform.isWindows():
            self._file_watcher.addPath(file_path)

    ##  Remove a file so that it will no longer be watched for changes.
    #   \param file_path The path to the file that must no longer be watched.
    def removeWatchedFile(self, file_path: str) -> None:
        # The QT 5.10.0 issue, only on Windows. Cura crashes after loading a stl file from USB/sd-card/Cloud-based drive
        if not Platform.isWindows():
            self._file_watcher.removePath(file_path)

    ##  Triggered whenever a file is changed that we currently have loaded.
    def _onFileChanged(self, file_path: str) -> None:
        if not os.path.isfile(file_path) or os.path.getsize(
                file_path) == 0:  # File doesn't exist any more, or it is empty
            return

        # Multiple nodes may be loaded from the same file at different stages. Reload them all.
        from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator  # To find which nodes to reload when files have changed.
        modified_nodes = [
            node for node in DepthFirstIterator(self.getRoot())
            if node.getMeshData()
            and node.getMeshData().getFileName() == file_path
        ]  # type: ignore

        if modified_nodes:
            self._reload_message = Message(i18n_catalog.i18nc(
                "@info", "Would you like to reload {filename}?").format(
                    filename=os.path.basename(file_path)),
                                           title=i18n_catalog.i18nc(
                                               "@info:title",
                                               "File has been modified"))
            self._reload_message.addAction(
                "reload",
                i18n_catalog.i18nc("@action:button", "Reload"),
                icon="",
                description=i18n_catalog.i18nc(
                    "@action:description",
                    "This will trigger the modified files to reload from disk."
                ))
            self._reload_callback = functools.partial(self._reloadNodes,
                                                      modified_nodes)
            self._reload_message.actionTriggered.connect(self._reload_callback)
            self._reload_message.show()

    ##  Reloads a list of nodes after the user pressed the "Reload" button.
    #   \param nodes The list of nodes that needs to be reloaded.
    #   \param message The message that triggered the action to reload them.
    #   \param action The button that triggered the action to reload them.
    def _reloadNodes(self, nodes: List["SceneNode"], message: str,
                     action: str) -> None:
        if action != "reload":
            return
        self._reload_message.hide()
        for node in nodes:
            meshdata = node.getMeshData()
            if meshdata:
                filename = meshdata.getFileName()
                if not filename or not os.path.isfile(
                        filename):  # File doesn't exist any more.
                    continue
                job = ReadMeshJob(filename)
                self._reload_finished_callback = functools.partial(
                    self._reloadJobFinished, node)
                job.finished.connect(self._reload_finished_callback)
                job.start()

    ##  Triggered when reloading has finished.
    #
    #   This then puts the resulting mesh data in the node.
    def _reloadJobFinished(self, replaced_node: SceneNode,
                           job: ReadMeshJob) -> None:
        for node in job.getResult():
            mesh_data = node.getMeshData()
            if mesh_data:
                replaced_node.setMeshData(mesh_data)
            else:
                Logger.log("w", "Could not find a mesh in reloaded node.")
Exemple #26
0
class FileWatcher(QThread):
    fchange = pyqtSignal(str, str)

    def __init__(self, path, max_age):
        QThread.__init__(self)
        self.path = path
        self.max_age = max_age
        self.files = {}
        self.file_reg = re.compile('(.+)_\d{8}_\d{6}' + re.escape(os.path.extsep) + 'txt$', re.IGNORECASE)
        self.qtfw = QFileSystemWatcher()
        self.qtfw.directoryChanged.connect(self.directory_changed)
        self.qtfw.addPath(path)
        self.update_watched_files(path)

    def directory_changed(self, path):
        self.update_watched_files(path)

    def run(self):
        while True:
            for path, modified in self.files.items():
                new_modified = 0
                try:
                    new_modified = os.path.getsize(path)
                except Exception as e:
                    print('filewatcher-thread error:', path, str(e))
                if new_modified > modified:
                    chatname = None
                    test = self.file_reg.search(os.path.basename(path))
                    if test:
                        chatname = test.group(1)
                    self.fchange.emit(path, chatname)
                    self.files[path] = new_modified
            time.sleep(1)

    def update_watched_files(self, changed_path):
        # reeading all files from the directory
        now = time.time()
        path = self.path
        files_in_dir = set()

        for f in os.listdir(path):
            if not os.path.isdir(f):
                full_path = ''
                try:
                    add = True
                    full_path = os.path.join(path, f)
                    if self.max_age and now - os.path.getmtime(full_path) > self.max_age:
                        add = False
                    if add:
                        files_in_dir.add(full_path)
                except Exception as e:
                    print("file to filewatcher failed:", full_path, str(e))

        # are there old file, that not longer exists?
        files_to_remove = set()
        for known_file in self.files:
            if known_file not in files_in_dir:
                files_to_remove.add(known_file)

        for file_to_remove in files_to_remove:
            del self.files[file_to_remove]

        # are there new files we must watch now?
        for new_file in files_in_dir:
            if new_file not in self.files:
                self.files[new_file] = os.path.getsize(new_file)
class Gonderici(QDialog):
    
    MESAJ_DIZINI="./mesajlar/"
    def __init__(self,ebeveyn=None):
        super(Gonderici,self).__init__(ebeveyn)
        kutu = QVBoxLayout()
        self.ebeveyn = ebeveyn
        self.setLayout(kutu)
        kutu.setContentsMargins(5,5,5,5)

        self.mesaj_liste = QListWidget()
        self.mesaj_liste.setSelectionMode(QListView.ExtendedSelection)
        kutu.addWidget(self.mesaj_liste)

        self.tum_mesajlar_fonk()
        self.dosya_izleyici = QFileSystemWatcher()
        self.dosya_izleyici.addPath(self.MESAJ_DIZINI)
        self.dosya_izleyici.directoryChanged.connect(self.tum_mesajlar_fonk)

        kutu_h = QHBoxLayout()
        kutu_h.addWidget(QLabel("<b>Mesaj Tipi :</b>"))
        self.mesaj_tipi_text = QComboBox()
        self.mesaj_tipi_text.addItems(["------","bilgi","sistem","kritik",])
        kutu_h.addWidget(self.mesaj_tipi_text)
        kutu.addLayout(kutu_h)

        kutu_h = QHBoxLayout()
#        kutu_h.addWidget(QLabel("<b>Mesaj :</b>"))
        self.gonderilen_text = QTextEdit()
        self.gonderilen_text.setFixedHeight(100)
        kutu_h.addWidget(self.gonderilen_text)

        self.gonder_dugme = QPushButton("Gönder")
        self.gonder_dugme.setFixedHeight(100)
        self.gonder_dugme.clicked.connect(self.gonder_fonk)
        kutu_h.addWidget(self.gonder_dugme)
        kutu.addLayout(kutu_h)


    def gonder_fonk(self):
        tip = self.mesaj_tipi_text.currentText()
        mesaj = self.gonderilen_text.toPlainText()
        if tip == "------":
            QMessageBox.warning(self,"Uyarı","Lütfen bir mesaj tipi giriniz")
        elif mesaj == "":
            QMessageBox.warning(self,"Uyarı","Lütfen bir mesaj giriniz")
        else:
            gonderilecek = """mesaj_tipi : {}\nmesaj : {}\ntarih : {}""".format(tip,mesaj,QDateTime.currentDateTime().toString("yyyy-MM-dd_hh:mm:ss"))
            f = open("./gecici","w")
            f.write(gonderilecek)
            f.close()
            shutil.move("./gecici", self.MESAJ_DIZINI+self.dosyaHashle("./gecici"))
            QMessageBox.information(self,"Gönderildi","Mesajınız başarıyla gönderidi.")
            self.gonderilen_text.clear()
            self.tum_mesajlar_fonk()

    def ayarlar_fonk(self):
        ayarlar_pencere = ayarlarui.Ayarlar(self)
        ayarlar_pencere.show()

    def sistem_cekmecesi_tiklandi(self,value):
        if value == self.sistem_cekmecesi.DoubleClick:
            self.mesaj_oku_fonk()

    def mesaj_oku_fonk(self):
        self.sistem_cekmecesi.setIcon(QIcon("./icons/milis-bildirim.png"))
        self.show()

    def tum_mesajlar_fonk(self):
        self.mesaj_liste.clear()
        duzenli_mesajlar = self.ebeveyn.mesajlar_oku_sirala()
        mesajlar = duzenli_mesajlar.keys()
        sirali_mesajlar = list(mesajlar)
        sirali_mesajlar.sort()
        sirali_mesajlar = sirali_mesajlar[::-1]
        if len(sirali_mesajlar) != 0:
            for mesaj in sirali_mesajlar:
                mesaj_ = duzenli_mesajlar[mesaj]
                ozel_widget = listemadddesi.OzelListeMaddesi(self)
                ozel_widget.mesaj_id_ekle(mesaj_[2])
                ozel_widget.mesaj_tipi_ekle(mesaj_[0])
                ozel_widget.mesaj_ekle(mesaj_[1])
                ozel_widget.tarih_ekle(mesaj)
                ozel_widget.gonderen_ekle(mesaj_[3])
                ozel_widget.gonderen_onay_ekle(mesaj_[4])
                ozel_widget_item = QListWidgetItem(self.mesaj_liste)
                ozel_widget_item.setSizeHint(ozel_widget.sizeHint())
                self.mesaj_liste.setItemWidget(ozel_widget_item, ozel_widget)

    def dosyaHashle(self,dosya):
        BUF_SIZE = 65536  # 64k lik parca-chunklar ile 
        sha256 = hashlib.sha256()
        with open(dosya, 'rb') as f:
            while True:
                data = f.read(BUF_SIZE)
                if not data:
                    break
                sha256.update(data)
        dosya_hash=sha256.hexdigest()
        return str(dosya_hash)
Exemple #28
0
class Scene:
    def __init__(self) -> None:
        super().__init__()

        from UM.Scene.SceneNode import SceneNode
        self._root = SceneNode(name = "Root")
        self._root.setCalculateBoundingBox(False)
        self._connectSignalsRoot()
        self._active_camera = None  # type: Optional[Camera]
        self._ignore_scene_changes = False
        self._lock = threading.Lock()

        # Watching file for changes.
        self._file_watcher = QFileSystemWatcher()
        self._file_watcher.fileChanged.connect(self._onFileChanged)

        self._reload_message = None  # type: Optional[Message]

    def _connectSignalsRoot(self) -> None:
        self._root.transformationChanged.connect(self.sceneChanged)
        self._root.childrenChanged.connect(self.sceneChanged)
        self._root.meshDataChanged.connect(self.sceneChanged)

    def _disconnectSignalsRoot(self) -> None:
        self._root.transformationChanged.disconnect(self.sceneChanged)
        self._root.childrenChanged.disconnect(self.sceneChanged)
        self._root.meshDataChanged.disconnect(self.sceneChanged)

    def setIgnoreSceneChanges(self, ignore_scene_changes: bool) -> None:
        if self._ignore_scene_changes != ignore_scene_changes:
            self._ignore_scene_changes = ignore_scene_changes
            if self._ignore_scene_changes:
                self._disconnectSignalsRoot()
            else:
                self._connectSignalsRoot()

    ##  Gets the global scene lock.
    #
    #   Use this lock to prevent any read or write actions on the scene from other threads,
    #   assuming those threads also properly acquire the lock. Most notably, this
    #   prevents the rendering thread from rendering the scene while it is changing.
    def getSceneLock(self) -> threading.Lock:
        return self._lock

    ##  Get the root node of the scene.
    def getRoot(self) -> "SceneNode":
        return self._root

    ##  Change the root node of the scene
    def setRoot(self, node: "SceneNode") -> None:
        if self._root != node:
            if not self._ignore_scene_changes:
                self._disconnectSignalsRoot()
            self._root = node
            if not self._ignore_scene_changes:
                self._connectSignalsRoot()
            self.rootChanged.emit()

    rootChanged = Signal()

    ##  Get the camera that should be used for rendering.
    def getActiveCamera(self) -> Optional[Camera]:
        return self._active_camera

    def getAllCameras(self) -> List[Camera]:
        cameras = []
        for node in BreadthFirstIterator(self._root):  # type: ignore
            if isinstance(node, Camera):
                cameras.append(node)
        return cameras

    ##  Set the camera that should be used for rendering.
    #   \param name The name of the camera to use.
    def setActiveCamera(self, name: str) -> None:
        camera = self.findCamera(name)
        if camera:
            self._active_camera = camera
        else:
            Logger.log("w", "Couldn't find camera with name [%s] to activate!" % name)

    ##  Signal that is emitted whenever something in the scene changes.
    #   \param object The object that triggered the change.
    sceneChanged = Signal()

    ##  Find an object by id.
    #
    #   \param object_id The id of the object to search for, as returned by the python id() method.
    #
    #   \return The object if found, or None if not.
    def findObject(self, object_id: int) -> Optional["SceneNode"]:
        for node in BreadthFirstIterator(self._root):  # type: ignore
            if id(node) == object_id:
                return node
        return None

    def findCamera(self, name: str) -> Optional[Camera]:
        for node in BreadthFirstIterator(self._root):  # type: ignore
            if isinstance(node, Camera) and node.getName() == name:
                return node
        return None

    ##  Add a file to be watched for changes.
    #   \param file_path The path to the file that must be watched.
    def addWatchedFile(self, file_path: str) -> None:
        # The QT 5.10.0 issue, only on Windows. Cura crashes after loading a stl file from USB/sd-card/Cloud-based drive
        if not Platform.isWindows():
            self._file_watcher.addPath(file_path)

    ##  Remove a file so that it will no longer be watched for changes.
    #   \param file_path The path to the file that must no longer be watched.
    def removeWatchedFile(self, file_path: str) -> None:
        # The QT 5.10.0 issue, only on Windows. Cura crashes after loading a stl file from USB/sd-card/Cloud-based drive
        if not Platform.isWindows():
            self._file_watcher.removePath(file_path)

    ##  Triggered whenever a file is changed that we currently have loaded.
    def _onFileChanged(self, file_path: str) -> None:
        if not os.path.isfile(file_path) or os.path.getsize(file_path) == 0:  # File doesn't exist any more, or it is empty
            return

        # Multiple nodes may be loaded from the same file at different stages. Reload them all.
        from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator  # To find which nodes to reload when files have changed.
        modified_nodes = [node for node in DepthFirstIterator(self.getRoot()) if node.getMeshData() and node.getMeshData().getFileName() == file_path]  # type: ignore

        if modified_nodes:
            # Hide the message if it was already visible
            if self._reload_message is not None:
                self._reload_message.hide()

            self._reload_message = Message(i18n_catalog.i18nc("@info", "Would you like to reload {filename}?").format(filename = os.path.basename(file_path)),
                              title = i18n_catalog.i18nc("@info:title", "File has been modified"))
            self._reload_message.addAction("reload", i18n_catalog.i18nc("@action:button", "Reload"), icon = "", description = i18n_catalog.i18nc("@action:description", "This will trigger the modified files to reload from disk."))
            self._reload_callback = functools.partial(self._reloadNodes, modified_nodes)
            self._reload_message.actionTriggered.connect(self._reload_callback)
            self._reload_message.show()

    ##  Reloads a list of nodes after the user pressed the "Reload" button.
    #   \param nodes The list of nodes that needs to be reloaded.
    #   \param message The message that triggered the action to reload them.
    #   \param action The button that triggered the action to reload them.
    def _reloadNodes(self, nodes: List["SceneNode"], message: str, action: str) -> None:
        if action != "reload":
            return
        if self._reload_message is not None:
            self._reload_message.hide()
        for node in nodes:
            meshdata = node.getMeshData()
            if meshdata:
                filename = meshdata.getFileName()
                if not filename or not os.path.isfile(filename):  # File doesn't exist any more.
                    continue
                job = ReadMeshJob(filename)
                self._reload_finished_callback = functools.partial(self._reloadJobFinished, node)
                job.finished.connect(self._reload_finished_callback)
                job.start()

    ##  Triggered when reloading has finished.
    #
    #   This then puts the resulting mesh data in the node.
    def _reloadJobFinished(self, replaced_node: SceneNode, job: ReadMeshJob) -> None:
        for node in job.getResult():
            mesh_data = node.getMeshData()
            if mesh_data:
                replaced_node.setMeshData(mesh_data)
            else:
                Logger.log("w", "Could not find a mesh in reloaded node.")
Exemple #29
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 #30
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': 'Color scheme', 'type': 'list',
         'values': ['Spyder','Monokai','Zenburn'], 'value': 'Spyder'}])

    EXTENSIONS = '*.py'

    def __init__(self,parent=None):

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

        self._filename = ''

        self.setup_editor(linenumbers=True,
                          markers=True,
                          edge_line=False,
                          tab_mode=False,
                          show_blanks=True,
                          language='Python')

        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()
        self.updatePreferences()

        # autoreload support
        self._file_watcher = QFileSystemWatcher(self)
        self._watched_file = None
        self._file_watcher.fileChanged.connect(self._file_changed)

    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'])

    def new(self):

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

    def open(self):

        fname,_ = QFileDialog.getOpenFileName(self,filter=self.EXTENSIONS)
        if fname is not '':
            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 is not '':

            if self.preferences['Autoreload']:
                self._file_watcher.removePath(self.filename)

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

            if self.preferences['Autoreload']:
                self._file_watcher.addPath(self.filename)
                self.triggerRerender.emit(True)

            self.reset_modified()

        else:
            self.save_as()

    def save_as(self):

        fname,_ = QFileDialog.getSaveFileName(self,filter=self.EXTENSIONS)
        if fname is not '':
            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._file_watcher.removePath(self._watched_file)
            self._watched_file = None
        if self.preferences['Autoreload'] and self.filename and self.filename != self._watched_file:
            self._watched_file = self._filename
            self._file_watcher.addPath(self.filename)

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

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

    # callback triggered by QFileSystemWatcher
    def _file_changed(self, val):
        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)

    def saveComponenetState(self,store):

        if self.filename is not '':
            store.setValue(self.name+'/state',self.filename)

    def restoreComponenetState(self,store):

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

        if filename and filename is not '':
            self.load_from_file(filename)
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 #32
0
class BLENDWriter(MeshWriter):
    """A MeshWriter subclass that performs .blend file saving."""

    def __init__(self):
        """The constructor, which calls the super-class-contructor (MeshWriter).

        Adds a filewatcher to replace the saved file from the writer job by our own generated file.
        """

        super().__init__(add_to_recent_files = False)

        self._write_watcher = QFileSystemWatcher()
        self._write_watcher.fileChanged.connect(self._writeChanged)


    def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
        """Main entry point for writing the file.

        :param stream: Buffer containing new file name and more.
        :param nodes: All nodes on the current scene.
        :param mode: The mode we write our file in. Not important here.
        :return: Always true, because the actual writing happens internal and not in this job.
        """

        # The return value: The status either successful or unsuccessful.
        success = True

        # Checks if path to blender is correct.
        if CuraBlender.CuraBlender.verifyBlenderPath(manual=False):

            self._plugin_path = CuraBlender.CuraBlender.getPluginPath()
            self._script_path = os.path.join(self._plugin_path, 'BlenderAPI.py')

            file_list = self._createFileList(nodes)
            
            (blender_files, execute_list) = self._createExecuteList(file_list)

            command = self._buildCommand('Write', stream.name, blender_files, execute_list, temp_path = None)

            subprocess.Popen(command, shell = True)

            self._write_watcher.addPath(stream.name)
        else:
            # Failure message already gets called at other place.
            Logger.logException('e', 'Problems with path to blender!')
            success = False

        return success


    def _createFileList(self, nodes):
        """Creates a file list containing the file path of all nodes.

        :param nodes: All nodes on the current scene.
        :return: File list with all paths of real nodes.
        """

        file_list = []
        for node in nodes:
            for children in node.getAllChildren():
                # Filters nodes without real meshdata and which doesn't belong to any file.
                if isinstance(children, CuraSceneNode) and not children.callDecoration("isGroup") and children.getMeshData().getFileName():
                    file_list.append(children.getMeshData().getFileName())
        return file_list


    def _createExecuteList(self, file_list):
        """Creates a list (String) with instructions for every file in the file list.

        :param file_list: File list with paths of nodes.
        :return: A list (String) with all files with the .blend extension.
        :return: A list (String) with all files with different file extensions.
        """

        blender_files = set()
        execute_list = ''
        # Checks the file extension and builds the command based on it.
        for file_path in file_list:
            if file_path.endswith('.blend'):
                if '_curasplit_' in file_path:
                    file_path = '{}.blend'.format(file_path[:file_path.index('_curasplit_')])
                blender_files.add(file_path)
            elif file_path.endswith('.stl'):
                execute_list = execute_list + "bpy.ops.import_mesh.stl(filepath = '{}');".format(file_path)
            elif file_path.endswith('.ply'):
                execute_list = execute_list + "bpy.ops.import_mesh.ply(filepath = '{}');".format(file_path)
            elif file_path.endswith('.obj'):
                execute_list = execute_list + "bpy.ops.import_scene.obj(filepath = '{}');".format(file_path)
            elif file_path.endswith('.x3d'):
                execute_list = execute_list + "bpy.ops.import_scene.x3d(filepath = '{}');".format(file_path)
            # Ignore objects with unsupported file extension.
            else:
                Logger.logException('e', '%s\nhas unsupported file extension and was ignored!', file_path)

        blend_list = ''
        processes = []
        for file_path in blender_files:
            temp_path = '{}_curatemp_.blend'.format(file_path[:-6])
            command = self._buildCommand('Write prepare', file_path, temp_path = temp_path)
            process = subprocess.Popen(command, shell = True)
            processes.append(process)
            blend_list = '{}{}_curatemp_.blend;'.format(blend_list, file_path[:-6])

        for process in processes:
            process.wait()

        return (blend_list, execute_list)


    def _buildCommand(self, program, file_name, blender_files = None, execute_list = None, temp_path = None):
        """Builds the command used by subprocess. Calls the 'Write' program.
     
        :param program: Mode used by the BlenderAPI to determine which program to run (set of instructions).
        :param file_name: The file name we selected when saving the file.
        :param blender_files: A list (String) with all blender files.
        :param execute_list: A list (String) with instructions for all other files.
        :param temp_path: A temporary path for converting the blend file and preparing the 'Write' step.
        :return: The complete command needed by subprocess.
        """

        self._blender_path = Application.getInstance().getPreferences().getValue('cura_blender/blender_path')
        if program == 'Write prepare':
            command = '"{}" "{}" --background --python "{}" -- "{}" "{}"'.format(self._blender_path, file_name, self._script_path, temp_path, program)
        else:
            command = '"{}" --background --python "{}" -- "{}" "{}" "{}" "{}"'.format(self._blender_path, self._script_path, file_name, execute_list, blender_files, program)
        return command


    def _writeChanged(self, path):
        """On file changed connection. Deletes the original saved file and replaces it with our own generated one.

        :param path: The path of our file we try to save.
        """

        # Instead of overwriting files, blender saves the old one with .blend1 extension. This file is corrupted, so we delete it.
        if os.path.isfile(path + '1'):
            os.remove(path + '1')
Exemple #33
0
def main():
    # Command-line arguments
    parser = argparse.ArgumentParser(
        prog='qbar',
        description="An easily-configurable and good-looking status bar for Linux")
    parser.add_argument(
        "--monitor",
        "-m",
        type=str,
        default=None,
        help="Name of the monitor to display the bar on")

    # TODO: use css for this instead?
    parser.add_argument(
        "--height",
        type=int,
        default=30,
        help="Height (in px) of the status bar")
    parser.add_argument(
        "--css",
        type=str,
        default=None,
        help="Stylesheet file to override the default style")
    parser.add_argument(
        "--config",
        "-c",
        default=None,
        help="Congifuration file for qbar written in yaml.  Looks under $XDG_HOME or .config for qbar/config.yml by default.  if not found, uses a builtin default")
    parser.add_argument(
        "--position",
        "-p",
        type=str,
        default="top",
        choices=["top", "bottom"],
        help="Display position of the bar.  Can be 'top' or 'bottom'")
    parser.add_argument(
        "--log-level",
        "-l",
        default='WARNING',
        choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
        help="Change the level of information to be logged.")
    ARGS = parser.parse_args()

    # Log level for the root logger
    numeric_log_level = getattr(logging, ARGS.log_level.upper(), None)
    logging.getLogger().setLevel(numeric_log_level)

    # Init app
    app = QApplication(sys.argv)
    bar = Bar(ARGS.monitor, height=ARGS.height, position=ARGS.position)

    # Set default stylesheet and override with custom one if present
    stylesheet_files = [os.path.abspath(os.path.dirname(__file__)) + "/../configs/default.css"]
    if ARGS.css != None:
        stylesheet_files.append(ARGS.css)
    css_loader = StyleSheetLoader(stylesheet_files, lambda styles: app.setStyleSheet(styles))

    # Find config file path
    cfgfile = ARGS.config if ARGS.config != None else default_user_config_dir() + '/config.py'
    if not os.path.exists(cfgfile):
        if ARGS.config != None:
            logging.error("Specified config file does not exist: %s" % ARGS.config)
        logging.info("Using builtin default config file")
        cfgfile = os.path.abspath(os.path.dirname(__file__)) + "/../configs/default.py"

    # Load config file and set it up to reload whenever the file changes
    cfgmodule = load_config_module(cfgfile)
    bar.items = cfgmodule.items
    def reload_config(filepath):
        logging.info("Config file changed, reloading: %s" % filepath)
        try:
            importlib.reload(cfgmodule)
            bar.items = cfgmodule.items
        except SyntaxError as e:
            logging.error("SyntaxError encountered when attempting to load config file: %s" % str(e))
            bar.items = []
    watcher = QFileSystemWatcher()
    watcher.addPath(cfgfile)
    watcher.fileChanged.connect(reload_config)

    # Run!
    bar.start()
    bar.show()

    # Exit app on ctrl+c.
    # Note that a timer must be present in order for Ctrl+C to work. Otherwise
    # the python interpreter never gets a chance to run and handle the signal.
    sigtimer = QTimer()
    sigtimer.timeout.connect(lambda: None)
    sigtimer.start(500)
    def sigint_handler(signal, frame):
        print("\nReceived Ctrl+C, exiting...")
        bar.stop()
        QApplication.quit()
    signal.signal(signal.SIGINT, sigint_handler)

    ret = app.exec_()
    sys.exit(ret)
Exemple #34
0
class NFile(QObject):
    """
    SIGNALS:
    @askForSaveFileClosing(QString)
    @fileClosing(QString)
    @fileChanged()
    @willDelete(PyQt_PyObject, PyQt_PyObject)
    @willOverWrite(PyQt_PyObject, QString, QString)
    @willMove(Qt_PyQtObject, QString, QString)
    @willSave(QString, QString)
    @savedAsNewFile(PyQt_PyObject, QString, QString)
    @gotAPath(PyQt_PyObject)
    @willAttachToExistingFile(PyQt_PyObject, QString)
    """
    fileChanged = pyqtSignal()
    fileRemoved = pyqtSignal()
    fileReaded = pyqtSignal()
    willAttachToExistingFile = pyqtSignal('PyQt_PyObject', 'QString')
    gotAPath = pyqtSignal('PyQt_PyObject')
    willSave = pyqtSignal('QString', 'QString')
    willMove = pyqtSignal('PyQt_PyObject', 'QString', 'QString')
    willOverWrite = pyqtSignal('PyQt_PyObject', 'QString', 'QString')
    willCopyTo = pyqtSignal('PyQt_PyObject', 'QString', 'QString')
    willDelete = pyqtSignal('PyQt_PyObject', 'PyQt_PyObject')
    fileClosing = pyqtSignal('QString', bool)

    def __init__(self, path=None):
        """
        """
        self._file_path = path
        self.__created = False
        self.__watcher = None
        self.__mtime = None
        super(NFile, self).__init__()
        if not self._exists():
            self.__created = True

    @property
    def file_name(self):
        """"Returns filename of nfile"""
        file_name = None
        if self._file_path is None:
            file_name = translations.TR_NEW_DOCUMENT
        else:
            file_name = get_basename(self._file_path)
        return file_name

    @property
    def display_name(self):
        """Returns a pretty name to be displayed by tabs"""
        display_name = self.file_name
        if self._file_path is not None and not self.has_write_permission():
            display_name += translations.TR_READ_ONLY
        return display_name

    @property
    def is_new_file(self):
        return self.__created

    def file_ext(self):
        """"Returns extension of nfile"""
        if self._file_path is None:
            return ''
        return get_file_extension(self._file_path)

    @property
    def file_path(self):
        """"Returns file path of nfile"""
        return self._file_path

    def start_watching(self):
        """Create a file system watcher and connect its fileChanged
        SIGNAL to our _file_changed SLOT"""
        if self.__watcher is None:
            self.__watcher = QFileSystemWatcher(self)
            self.__watcher.fileChanged['const QString&'].connect(
                self._file_changed)
        if self._file_path is not None:
            self.__mtime = os.path.getmtime(self._file_path)
            self.__watcher.addPath(self._file_path)

    def _file_changed(self, path):
        if self._exists():
            current_mtime = os.path.getmtime(self._file_path)
            if current_mtime != self.__mtime:
                self.__mtime = current_mtime
                self.fileChanged.emit()
        # FIXME: for swap file
        # else:
        #     self.fileRemoved.emit()

    def has_write_permission(self):
        if not self._exists():
            return True
        return os.access(self._file_path, os.W_OK)

    def _exists(self):
        """
        Check if we have been created with a path and if such path exists
        In case there is no path, we are most likely a new file.
        """
        file_exists = False
        if self._file_path and os.path.exists(self._file_path):
            file_exists = True
        return file_exists

    def attach_to_path(self, new_path):
        if os.path.exists(new_path):
            signal_handler = SignalFlowControl()
            self.willAttachToExistingFile.emit(signal_handler, new_path)
            if signal_handler.stopped():
                    return
        self._file_path = new_path
        self.gotAPath.emit(self)
        return self._file_path

    def create(self):
        if self.__created:
            self.save("")
        self.__created = False

    def save(self, content, path=None):
        """
        Write a temporary file with .tnj extension and copy it over the
        original one.
        .nsf = Ninja Swap File
        # FIXME: Where to locate addExtension, does not fit here
        """
        new_path = False
        if path:
            self.attach_to_path(path)
            new_path = True

        save_path = self._file_path

        if not save_path:
            raise NinjaNoFileNameException("I am asked to write a "
                                           "file but no one told me where")
        swap_save_path = "%s.nsp" % save_path

        # If we have a file system watcher, remove the file path
        # from its watch list until we are done making changes.
        if self.__watcher is not None:
            self.__watcher.removePath(save_path)

        flags = QIODevice.WriteOnly | QIODevice.Truncate
        f = QFile(swap_save_path)
        if settings.use_platform_specific_eol():
            flags |= QIODevice.Text

        if not f.open(flags):
            raise NinjaIOException(f.errorString())

        stream = QTextStream(f)
        encoding = get_file_encoding(content)
        if encoding:
            stream.setCodec(encoding)

        encoded_stream = stream.codec().fromUnicode(content)
        f.write(encoded_stream)
        f.flush()
        f.close()
        # SIGNAL: Will save (temp, definitive) to warn folder to do something
        self.willSave.emit(swap_save_path, save_path)
        self.__mtime = os.path.getmtime(swap_save_path)
        shutil.move(swap_save_path, save_path)
        self.reset_state()

        # If we have a file system watcher, add the saved path back
        # to its watch list, otherwise create a watcher and start
        # watching
        if self.__watcher is not None:
            if new_path:
                # self.__watcher.removePath(self.__watcher.files()[0])
                self.__watcher.addPath(self._file_path)
            else:
                self.__watcher.addPath(save_path)
        else:
            self.start_watching()
        return self

    def reset_state(self):
        """
        #FIXE: to have a ref to changed I need to have the doc here
        """
        self.__created = False

    def read(self, path=None):
        """
        Read the file or fail
        """
        open_path = path and path or self._file_path
        self._file_path = open_path
        if not self._file_path:
            raise NinjaNoFileNameException("I am asked to read a file "
                                           "but no one told me from where")
        try:
            with open(open_path, 'r') as f:
                content = f.read()
        except (IOError, UnicodeDecodeError) as reason:
            raise NinjaIOException(reason)
        self.fileReaded.emit()
        return content

    def move(self, new_path):
        """
        Phisically move the file
        """
        if self._exists():
            signal_handler = SignalFlowControl()
            # SIGNALL: WILL MOVE TO, to warn folder to exist
            self.willMove.emit(signal_handler,
                               self._file_path,
                               new_path)
            if signal_handler.stopped():
                return
            if os.path.exists(new_path):
                signal_handler = SignalFlowControl()
                self.willOverWrite.emit(signal_handler,
                                        self._file_path,
                                        new_path)
                if signal_handler.stopped():
                    return
            if self.__watcher is not None:
                self.__watcher.removePath(self._file_path)
            shutil.move(self._file_path, new_path)
            if self.__watcher:
                self.__watcher.addPath(new_path)
        self._file_path = new_path

    def copy(self, new_path):
        """
        Copy the file to a new path
        """
        if self._exists():
            signal_handler = SignalFlowControl()
            # SIGNALL: WILL COPY TO, to warn folder to exist
            self.willCopyTo.emit(signal_handler,
                                 self._file_path,
                                 new_path)
            if signal_handler.stopped():
                return
            if os.path.exists(new_path):
                signal_handler = SignalFlowControl()
                self.willOverWrite.emit(signal_handler,
                                        self._file_path,
                                        new_path)
                if signal_handler.stopped():
                    return

            shutil.copy(self._file_path, new_path)

    def delete(self, force=False):
        """
        This deletes the object and closes the file.
        """
        # if created but exists this file migth to someone else
        self.close()
        if ((not self.__created) or force) and self._exists():
            DEBUG("Deleting our own NFile %s" % self._file_path)
            signal_handler = SignalFlowControl()
            self.willDelete.emit(signal_handler, self)
            if not signal_handler.stopped():
                if self.__watcher is not None:
                    self.__watcher.removePath(self._file_path)
                os.remove(self._file_path)

    def close(self, force_close=False):
        """
        Lets let people know we are going down so they can act upon
        As you can see close does nothing but let everyone know that we are
        not saved yet
        """
        DEBUG("About to close NFile")
        self.fileClosing.emit(self._file_path, force_close)

    def remove_watcher(self):
        if self.__watcher is not None:
            self.__watcher.removePath(self._file_path)
Exemple #35
0
class ReTextWindow(QMainWindow):
	def __init__(self, parent=None):
		QMainWindow.__init__(self, parent)
		self.resize(950, 700)
		screenRect = QDesktopWidget().screenGeometry()
		if globalSettings.windowGeometry:
			self.restoreGeometry(globalSettings.windowGeometry)
		else:
			self.move((screenRect.width() - self.width()) // 2,
			          (screenRect.height() - self.height()) // 2)
			if not screenRect.contains(self.geometry()):
				self.showMaximized()
		if sys.platform.startswith('darwin'):
			# https://github.com/retext-project/retext/issues/198
			searchPaths = QIcon.themeSearchPaths()
			searchPaths.append('/opt/local/share/icons')
			searchPaths.append('/usr/local/share/icons')
			QIcon.setThemeSearchPaths(searchPaths)
		setIconThemeFromSettings()
		if QFile.exists(getBundledIcon('retext')):
			self.setWindowIcon(QIcon(getBundledIcon('retext')))
		elif QFile.exists('/usr/share/pixmaps/retext.png'):
			self.setWindowIcon(QIcon('/usr/share/pixmaps/retext.png'))
		else:
			self.setWindowIcon(QIcon.fromTheme('retext',
				QIcon.fromTheme('accessories-text-editor')))
		self.tabWidget = QTabWidget(self)
		self.initTabWidget()
		self.setCentralWidget(self.tabWidget)
		self.tabWidget.currentChanged.connect(self.changeIndex)
		self.tabWidget.tabCloseRequested.connect(self.closeTab)
		self.toolBar = QToolBar(self.tr('File toolbar'), self)
		self.addToolBar(Qt.TopToolBarArea, self.toolBar)
		self.editBar = QToolBar(self.tr('Edit toolbar'), self)
		self.addToolBar(Qt.TopToolBarArea, self.editBar)
		self.searchBar = QToolBar(self.tr('Search toolbar'), self)
		self.addToolBar(Qt.BottomToolBarArea, self.searchBar)
		self.toolBar.setVisible(not globalSettings.hideToolBar)
		self.editBar.setVisible(not globalSettings.hideToolBar)
		self.actionNew = self.act(self.tr('New'), 'document-new',
			self.createNew, shct=QKeySequence.New)
		self.actionNew.setPriority(QAction.LowPriority)
		self.actionOpen = self.act(self.tr('Open'), 'document-open',
			self.openFile, shct=QKeySequence.Open)
		self.actionOpen.setPriority(QAction.LowPriority)
		self.actionSetEncoding = self.act(self.tr('Set encoding'),
			trig=self.showEncodingDialog)
		self.actionSetEncoding.setEnabled(False)
		self.actionReload = self.act(self.tr('Reload'), 'view-refresh',
			lambda: self.currentTab.readTextFromFile())
		self.actionReload.setEnabled(False)
		self.actionSave = self.act(self.tr('Save'), 'document-save',
			self.saveFile, shct=QKeySequence.Save)
		self.actionSave.setEnabled(False)
		self.actionSave.setPriority(QAction.LowPriority)
		self.actionSaveAs = self.act(self.tr('Save as'), 'document-save-as',
			self.saveFileAs, shct=QKeySequence.SaveAs)
		self.actionNextTab = self.act(self.tr('Next tab'), 'go-next',
			lambda: self.switchTab(1), shct=Qt.CTRL+Qt.Key_PageDown)
		self.actionPrevTab = self.act(self.tr('Previous tab'), 'go-previous',
			lambda: self.switchTab(-1), shct=Qt.CTRL+Qt.Key_PageUp)
		self.actionCloseCurrentTab = self.act(self.tr('Close tab'), 'window-close',
			lambda: self.closeTab(self.ind), shct=QKeySequence.Close)
		self.actionPrint = self.act(self.tr('Print'), 'document-print',
			self.printFile, shct=QKeySequence.Print)
		self.actionPrint.setPriority(QAction.LowPriority)
		self.actionPrintPreview = self.act(self.tr('Print preview'), 'document-print-preview',
			self.printPreview)
		self.actionViewHtml = self.act(self.tr('View HTML code'), 'text-html', self.viewHtml)
		self.actionChangeEditorFont = self.act(self.tr('Change editor font'),
			trig=self.changeEditorFont)
		self.actionChangePreviewFont = self.act(self.tr('Change preview font'),
			trig=self.changePreviewFont)
		self.actionSearch = self.act(self.tr('Find text'), 'edit-find',
			self.search, shct=QKeySequence.Find)
		self.actionGoToLine = self.act(self.tr('Go to line'),
			trig=self.goToLine, shct=Qt.CTRL+Qt.Key_G)
		self.searchBar.visibilityChanged.connect(self.searchBarVisibilityChanged)
		self.actionPreview = self.act(self.tr('Preview'), shct=Qt.CTRL+Qt.Key_E,
			trigbool=self.preview)
		if QIcon.hasThemeIcon('document-preview'):
			self.actionPreview.setIcon(QIcon.fromTheme('document-preview'))
		elif QIcon.hasThemeIcon('preview-file'):
			self.actionPreview.setIcon(QIcon.fromTheme('preview-file'))
		elif QIcon.hasThemeIcon('x-office-document'):
			self.actionPreview.setIcon(QIcon.fromTheme('x-office-document'))
		else:
			self.actionPreview.setIcon(QIcon(getBundledIcon('document-preview')))
		self.actionLivePreview = self.act(self.tr('Live preview'), shct=Qt.CTRL+Qt.Key_L,
		trigbool=self.enableLivePreview)
		menuPreview = QMenu()
		menuPreview.addAction(self.actionLivePreview)
		self.actionPreview.setMenu(menuPreview)
		self.actionInsertTable = self.act(self.tr('Insert table'),
			trig=lambda: self.insertFormatting('table'))
		self.actionTableMode = self.act(self.tr('Table editing mode'),
			shct=Qt.CTRL+Qt.Key_T,
			trigbool=lambda x: self.currentTab.editBox.enableTableMode(x))
		self.actionInsertImages = self.act(self.tr('Insert images by file path'),
			trig=lambda: self.insertImages())
		if ReTextFakeVimHandler:
			self.actionFakeVimMode = self.act(self.tr('FakeVim mode'),
				shct=Qt.CTRL+Qt.ALT+Qt.Key_V, trigbool=self.enableFakeVimMode)
			if globalSettings.useFakeVim:
				self.actionFakeVimMode.setChecked(True)
				self.enableFakeVimMode(True)
		self.actionFullScreen = self.act(self.tr('Fullscreen mode'), 'view-fullscreen',
			shct=Qt.Key_F11, trigbool=self.enableFullScreen)
		self.actionFullScreen.setChecked(self.isFullScreen())
		self.actionFullScreen.setPriority(QAction.LowPriority)
		self.actionConfig = self.act(self.tr('Preferences'), icon='preferences-system',
			trig=self.openConfigDialog)
		self.actionConfig.setMenuRole(QAction.PreferencesRole)
		self.actionSaveHtml = self.act('HTML', 'text-html', self.saveFileHtml)
		self.actionPdf = self.act('PDF', 'application-pdf', self.savePdf)
		self.actionOdf = self.act('ODT', 'x-office-document', self.saveOdf)
		self.getExportExtensionsList()
		self.actionQuit = self.act(self.tr('Quit'), 'application-exit', shct=QKeySequence.Quit)
		self.actionQuit.setMenuRole(QAction.QuitRole)
		self.actionQuit.triggered.connect(self.close)
		self.actionUndo = self.act(self.tr('Undo'), 'edit-undo',
			lambda: self.currentTab.editBox.undo(), shct=QKeySequence.Undo)
		self.actionRedo = self.act(self.tr('Redo'), 'edit-redo',
			lambda: self.currentTab.editBox.redo(), shct=QKeySequence.Redo)
		self.actionCopy = self.act(self.tr('Copy'), 'edit-copy',
			lambda: self.currentTab.editBox.copy(), shct=QKeySequence.Copy)
		self.actionCut = self.act(self.tr('Cut'), 'edit-cut',
			lambda: self.currentTab.editBox.cut(), shct=QKeySequence.Cut)
		self.actionPaste = self.act(self.tr('Paste'), 'edit-paste',
			lambda: self.currentTab.editBox.paste(), shct=QKeySequence.Paste)
		self.actionPasteImage = self.act(self.tr('Paste image'), 'edit-paste',
			lambda: self.currentTab.editBox.pasteImage(), shct=Qt.CTRL+Qt.SHIFT+Qt.Key_V)
		self.actionMoveUp = self.act(self.tr('Move line up'), 'go-up',
			lambda: self.currentTab.editBox.moveLineUp(), shct=Qt.ALT+Qt.Key_Up)
		self.actionMoveDown = self.act(self.tr('Move line down'), 'go-down',
			lambda: self.currentTab.editBox.moveLineDown(), shct=Qt.ALT+Qt.Key_Down)
		self.actionUndo.setEnabled(False)
		self.actionRedo.setEnabled(False)
		self.actionCopy.setEnabled(False)
		self.actionCut.setEnabled(False)
		qApp = QApplication.instance()
		qApp.clipboard().dataChanged.connect(self.clipboardDataChanged)
		self.clipboardDataChanged()
		if enchant is not None:
			self.actionEnableSC = self.act(self.tr('Enable'), trigbool=self.enableSpellCheck)
			self.actionSetLocale = self.act(self.tr('Set locale'), trig=self.changeLocale)
		self.actionWebKit = self.act(self.tr('Use WebKit renderer'), trigbool=self.enableWebKit)
		if ReTextWebKitPreview is None:
			globalSettings.useWebKit = False
			self.actionWebKit.setEnabled(False)
		self.actionWebKit.setChecked(globalSettings.useWebKit)
		self.actionWebEngine = self.act(self.tr('Use WebEngine (Chromium) renderer'),
			trigbool=self.enableWebEngine)
		if ReTextWebEnginePreview is None:
			globalSettings.useWebEngine = False
		self.actionWebEngine.setChecked(globalSettings.useWebEngine)
		self.actionShow = self.act(self.tr('Show directory'), 'system-file-manager', self.showInDir)
		self.actionFind = self.act(self.tr('Next'), 'go-next', self.find,
			shct=QKeySequence.FindNext)
		self.actionFindPrev = self.act(self.tr('Previous'), 'go-previous',
			lambda: self.find(back=True), shct=QKeySequence.FindPrevious)
		self.actionReplace = self.act(self.tr('Replace'), 'edit-find-replace',
			lambda: self.find(replace=True))
		self.actionReplaceAll = self.act(self.tr('Replace all'), trig=self.replaceAll)
		menuReplace = QMenu()
		menuReplace.addAction(self.actionReplaceAll)
		self.actionReplace.setMenu(menuReplace)
		self.actionCloseSearch = self.act(self.tr('Close'), 'window-close',
			lambda: self.searchBar.setVisible(False),
			shct=QKeySequence.Cancel)
		self.actionCloseSearch.setPriority(QAction.LowPriority)
		self.actionHelp = self.act(self.tr('Get help online'), 'help-contents', self.openHelp)
		self.aboutWindowTitle = self.tr('About ReText')
		self.actionAbout = self.act(self.aboutWindowTitle, 'help-about', self.aboutDialog)
		self.actionAbout.setMenuRole(QAction.AboutRole)
		self.actionAboutQt = self.act(self.tr('About Qt'))
		self.actionAboutQt.setMenuRole(QAction.AboutQtRole)
		self.actionAboutQt.triggered.connect(qApp.aboutQt)
		availableMarkups = markups.get_available_markups()
		if not availableMarkups:
			print('Warning: no markups are available!')
		if len(availableMarkups) > 1:
			self.chooseGroup = QActionGroup(self)
			markupActions = []
			for markup in availableMarkups:
				markupAction = self.act(markup.name, trigbool=self.markupFunction(markup))
				if markup.name == globalSettings.defaultMarkup:
					markupAction.setChecked(True)
				self.chooseGroup.addAction(markupAction)
				markupActions.append(markupAction)
		self.actionBold = self.act(self.tr('Bold'), shct=QKeySequence.Bold,
			trig=lambda: self.insertFormatting('bold'))
		self.actionItalic = self.act(self.tr('Italic'), shct=QKeySequence.Italic,
			trig=lambda: self.insertFormatting('italic'))
		self.actionUnderline = self.act(self.tr('Underline'), shct=QKeySequence.Underline,
			trig=lambda: self.insertFormatting('underline'))
		self.usefulTags = ('header', 'italic', 'bold', 'underline', 'numbering',
			'bullets', 'image', 'link', 'inline code', 'code block', 'blockquote',
			'table')
		self.usefulChars = ('deg', 'divide', 'euro', 'hellip', 'laquo', 'larr',
			'lsquo', 'mdash', 'middot', 'minus', 'nbsp', 'ndash', 'raquo',
			'rarr', 'rsquo', 'times')
		self.formattingBox = QComboBox(self.editBar)
		self.formattingBox.addItem(self.tr('Formatting'))
		self.formattingBox.addItems(self.usefulTags)
		self.formattingBox.activated[str].connect(self.insertFormatting)
		self.symbolBox = QComboBox(self.editBar)
		self.symbolBox.addItem(self.tr('Symbols'))
		self.symbolBox.addItems(self.usefulChars)
		self.symbolBox.activated.connect(self.insertSymbol)
		self.updateStyleSheet()
		menubar = self.menuBar()
		menuFile = menubar.addMenu(self.tr('File'))
		menuEdit = menubar.addMenu(self.tr('Edit'))
		menuHelp = menubar.addMenu(self.tr('Help'))
		menuFile.addAction(self.actionNew)
		menuFile.addAction(self.actionOpen)
		self.menuRecentFiles = menuFile.addMenu(self.tr('Open recent'))
		self.menuRecentFiles.aboutToShow.connect(self.updateRecentFiles)
		menuFile.addAction(self.actionShow)
		menuFile.addAction(self.actionSetEncoding)
		menuFile.addAction(self.actionReload)
		menuFile.addSeparator()
		menuFile.addAction(self.actionSave)
		menuFile.addAction(self.actionSaveAs)
		menuFile.addSeparator()
		menuFile.addAction(self.actionNextTab)
		menuFile.addAction(self.actionPrevTab)
		menuFile.addAction(self.actionCloseCurrentTab)
		menuFile.addSeparator()
		menuExport = menuFile.addMenu(self.tr('Export'))
		menuExport.addAction(self.actionSaveHtml)
		menuExport.addAction(self.actionOdf)
		menuExport.addAction(self.actionPdf)
		if self.extensionActions:
			menuExport.addSeparator()
			for action, mimetype in self.extensionActions:
				menuExport.addAction(action)
			menuExport.aboutToShow.connect(self.updateExtensionsVisibility)
		menuFile.addAction(self.actionPrint)
		menuFile.addAction(self.actionPrintPreview)
		menuFile.addSeparator()
		menuFile.addAction(self.actionQuit)
		menuEdit.addAction(self.actionUndo)
		menuEdit.addAction(self.actionRedo)
		menuEdit.addSeparator()
		menuEdit.addAction(self.actionCut)
		menuEdit.addAction(self.actionCopy)
		menuEdit.addAction(self.actionPaste)
		menuEdit.addAction(self.actionPasteImage)
		menuEdit.addSeparator()
		menuEdit.addAction(self.actionMoveUp)
		menuEdit.addAction(self.actionMoveDown)
		menuEdit.addSeparator()
		if enchant is not None:
			menuSC = menuEdit.addMenu(self.tr('Spell check'))
			menuSC.addAction(self.actionEnableSC)
			menuSC.addAction(self.actionSetLocale)
		menuEdit.addAction(self.actionSearch)
		menuEdit.addAction(self.actionGoToLine)
		menuEdit.addAction(self.actionChangeEditorFont)
		menuEdit.addAction(self.actionChangePreviewFont)
		menuEdit.addSeparator()
		if len(availableMarkups) > 1:
			self.menuMode = menuEdit.addMenu(self.tr('Default markup'))
			for markupAction in markupActions:
				self.menuMode.addAction(markupAction)
		menuFormat = menuEdit.addMenu(self.tr('Formatting'))
		menuFormat.addAction(self.actionBold)
		menuFormat.addAction(self.actionItalic)
		menuFormat.addAction(self.actionUnderline)
		if ReTextWebKitPreview is not None or ReTextWebEnginePreview is None:
			menuEdit.addAction(self.actionWebKit)
		else:
			menuEdit.addAction(self.actionWebEngine)
		menuEdit.addSeparator()
		menuEdit.addAction(self.actionViewHtml)
		menuEdit.addAction(self.actionPreview)
		menuEdit.addAction(self.actionInsertTable)
		menuEdit.addAction(self.actionTableMode)
		menuEdit.addAction(self.actionInsertImages)
		if ReTextFakeVimHandler:
			menuEdit.addAction(self.actionFakeVimMode)
		menuEdit.addSeparator()
		menuEdit.addAction(self.actionFullScreen)
		menuEdit.addAction(self.actionConfig)
		menuHelp.addAction(self.actionHelp)
		menuHelp.addSeparator()
		menuHelp.addAction(self.actionAbout)
		menuHelp.addAction(self.actionAboutQt)
		self.toolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
		self.toolBar.addAction(self.actionNew)
		self.toolBar.addSeparator()
		self.toolBar.addAction(self.actionOpen)
		self.toolBar.addAction(self.actionSave)
		self.toolBar.addAction(self.actionPrint)
		self.toolBar.addSeparator()
		self.toolBar.addAction(self.actionPreview)
		self.toolBar.addAction(self.actionFullScreen)
		self.editBar.addAction(self.actionUndo)
		self.editBar.addAction(self.actionRedo)
		self.editBar.addSeparator()
		self.editBar.addAction(self.actionCut)
		self.editBar.addAction(self.actionCopy)
		self.editBar.addAction(self.actionPaste)
		self.editBar.addSeparator()
		self.editBar.addWidget(self.formattingBox)
		self.editBar.addWidget(self.symbolBox)
		self.searchEdit = QLineEdit(self.searchBar)
		self.searchEdit.setPlaceholderText(self.tr('Search'))
		self.searchEdit.returnPressed.connect(self.find)
		self.replaceEdit = QLineEdit(self.searchBar)
		self.replaceEdit.setPlaceholderText(self.tr('Replace with'))
		self.replaceEdit.returnPressed.connect(self.find)
		self.csBox = QCheckBox(self.tr('Case sensitively'), self.searchBar)
		self.searchBar.addWidget(self.searchEdit)
		self.searchBar.addWidget(self.replaceEdit)
		self.searchBar.addSeparator()
		self.searchBar.addWidget(self.csBox)
		self.searchBar.addAction(self.actionFindPrev)
		self.searchBar.addAction(self.actionFind)
		self.searchBar.addAction(self.actionReplace)
		self.searchBar.addAction(self.actionCloseSearch)
		self.searchBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
		self.searchBar.setVisible(False)
		self.autoSaveEnabled = globalSettings.autoSave
		if self.autoSaveEnabled:
			timer = QTimer(self)
			timer.start(60000)
			timer.timeout.connect(self.saveAll)
		self.ind = None
		if enchant is not None:
			self.sl = globalSettings.spellCheckLocale
			try:
				enchant.Dict(self.sl or None)
			except enchant.errors.Error as e:
				warnings.warn(str(e), RuntimeWarning)
				globalSettings.spellCheck = False
			if globalSettings.spellCheck:
				self.actionEnableSC.setChecked(True)
		self.fileSystemWatcher = QFileSystemWatcher()
		self.fileSystemWatcher.fileChanged.connect(self.fileChanged)

	def restoreLastOpenedFiles(self):
		for file in readListFromSettings("lastFileList"):
			self.openFileWrapper(file)

		# Show the tab of last opened file
		lastTabIndex = globalSettings.lastTabIndex
		if lastTabIndex >= 0 and lastTabIndex < self.tabWidget.count():
			self.tabWidget.setCurrentIndex(lastTabIndex)

	def iterateTabs(self):
		for i in range(self.tabWidget.count()):
			yield self.tabWidget.widget(i)

	def updateStyleSheet(self):
		self.ss = None
		if globalSettings.styleSheet:
			sheetfile = QFile(globalSettings.styleSheet)
			sheetfile.open(QIODevice.ReadOnly)
			self.ss = QTextStream(sheetfile).readAll()
			sheetfile.close()

	def initTabWidget(self):
		def dragEnterEvent(e):
			e.acceptProposedAction()
		def dropEvent(e):
			fn = bytes(e.mimeData().data('text/plain')).decode().rstrip()
			if fn.startswith('file:'):
				fn = QUrl(fn).toLocalFile()
			self.openFileWrapper(fn)
		self.tabWidget.setTabsClosable(True)
		self.tabWidget.setAcceptDrops(True)
		self.tabWidget.setMovable(True)
		self.tabWidget.dragEnterEvent = dragEnterEvent
		self.tabWidget.dropEvent = dropEvent
		self.tabWidget.setTabBarAutoHide(globalSettings.tabBarAutoHide)

	def act(self, name, icon=None, trig=None, trigbool=None, shct=None):
		if not isinstance(shct, QKeySequence):
			shct = QKeySequence(shct)
		if icon:
			action = QAction(self.actIcon(icon), name, self)
		else:
			action = QAction(name, self)
		if trig:
			action.triggered.connect(trig)
		elif trigbool:
			action.setCheckable(True)
			action.triggered[bool].connect(trigbool)
		if shct:
			action.setShortcut(shct)
		return action

	def actIcon(self, name):
		return QIcon.fromTheme(name, QIcon(getBundledIcon(name)))

	def printError(self):
		import traceback
		print('Exception occurred while parsing document:', file=sys.stderr)
		traceback.print_exc()

	def updateTabTitle(self, ind, tab):
		changed = tab.editBox.document().isModified()
		if changed and not self.autoSaveActive(tab):
			title = tab.getBaseName() + '*'
		else:
			title = tab.getBaseName()
		self.tabWidget.setTabText(ind, title)

	def tabFileNameChanged(self, tab):
		'''
		Perform all UI state changes that need to be done when the
		filename of the current tab has changed.
		'''
		if tab == self.currentTab:
			if tab.fileName:
				self.setWindowTitle("")
				if globalSettings.windowTitleFullPath:
					self.setWindowTitle(tab.fileName + '[*]')
				self.setWindowFilePath(tab.fileName)
				self.updateTabTitle(self.ind, tab)
				self.tabWidget.setTabToolTip(self.ind, tab.fileName)
				QDir.setCurrent(QFileInfo(tab.fileName).dir().path())
			else:
				self.setWindowFilePath('')
				self.setWindowTitle(self.tr('New document') + '[*]')

			canReload = bool(tab.fileName) and not self.autoSaveActive(tab)
			self.actionSetEncoding.setEnabled(canReload)
			self.actionReload.setEnabled(canReload)

	def tabActiveMarkupChanged(self, tab):
		'''
		Perform all UI state changes that need to be done when the
		active markup class of the current tab has changed.
		'''
		if tab == self.currentTab:
			markupClass = tab.getActiveMarkupClass()
			dtMarkdown = (markupClass == markups.MarkdownMarkup)
			dtMkdOrReST = dtMarkdown or (markupClass == markups.ReStructuredTextMarkup)
			self.formattingBox.setEnabled(dtMarkdown)
			self.symbolBox.setEnabled(dtMarkdown)
			self.actionUnderline.setEnabled(dtMarkdown)
			self.actionBold.setEnabled(dtMkdOrReST)
			self.actionItalic.setEnabled(dtMkdOrReST)

	def tabModificationStateChanged(self, tab):
		'''
		Perform all UI state changes that need to be done when the
		modification state of the current tab has changed.
		'''

		if tab == self.currentTab:
			changed = tab.editBox.document().isModified()
			if self.autoSaveActive(tab):
				changed = False
			self.actionSave.setEnabled(changed)
			self.updateTabTitle(self.ind, tab)
			self.setWindowModified(changed)

	def createTab(self, fileName):
		previewStatesByName = {
			'editor': PreviewDisabled,
			'normal-preview': PreviewNormal,
			'live-preview': PreviewLive,
		}
		previewState = previewStatesByName.get(globalSettings.defaultPreviewState, PreviewDisabled)
		if previewState == PreviewNormal and not fileName:
			previewState = PreviewDisabled  # Opening empty document in preview mode makes no sense
		self.currentTab = ReTextTab(self, fileName, previewState)
		self.currentTab.fileNameChanged.connect(lambda: self.tabFileNameChanged(self.currentTab))
		self.currentTab.modificationStateChanged.connect(lambda: self.tabModificationStateChanged(self.currentTab))
		self.currentTab.activeMarkupChanged.connect(lambda: self.tabActiveMarkupChanged(self.currentTab))
		self.tabWidget.addTab(self.currentTab, self.tr("New document"))
		self.currentTab.updateBoxesVisibility()
		if previewState > 0:
			QTimer.singleShot(500, self.currentTab.triggerPreviewUpdate)

	def closeTab(self, ind):
		if self.maybeSave(ind):
			if self.tabWidget.count() == 1:
				self.createTab("")
			closedTab = self.tabWidget.widget(ind)
			if closedTab.fileName:
				self.fileSystemWatcher.removePath(closedTab.fileName)
			self.tabWidget.removeTab(ind)
			closedTab.deleteLater()

	def changeIndex(self, ind):
		'''
		This function is called when a different tab is selected.
		It changes the state of the window to mirror the current state
		of the newly selected tab. Future changes to this state will be
		done in response to signals emitted by the tab, to which the
		window was subscribed when the tab was created. The window is
		subscribed to all tabs like this, but only the active tab will
		logically generate these signals.
		Aside from the above this function also calls the handlers for
		the other changes that are implied by a tab switch: filename
		change, modification state change and active markup change.
		'''
		self.currentTab = self.tabWidget.currentWidget()
		editBox = self.currentTab.editBox
		previewState = self.currentTab.previewState
		self.actionUndo.setEnabled(editBox.document().isUndoAvailable())
		self.actionRedo.setEnabled(editBox.document().isRedoAvailable())
		self.actionCopy.setEnabled(editBox.textCursor().hasSelection())
		self.actionCut.setEnabled(editBox.textCursor().hasSelection())
		self.actionPreview.setChecked(previewState >= PreviewLive)
		self.actionLivePreview.setChecked(previewState == PreviewLive)
		self.actionTableMode.setChecked(editBox.tableModeEnabled)
		self.editBar.setEnabled(previewState < PreviewNormal)
		self.ind = ind
		editBox.setFocus(Qt.OtherFocusReason)

		self.tabFileNameChanged(self.currentTab)
		self.tabModificationStateChanged(self.currentTab)
		self.tabActiveMarkupChanged(self.currentTab)

	def changeEditorFont(self):
		font, ok = QFontDialog.getFont(globalSettings.editorFont, self)
		if ok:
			self.setEditorFont(font)

	def setEditorFont(self, font):
		globalSettings.editorFont = font
		for tab in self.iterateTabs():
			tab.editBox.updateFont()

	def changePreviewFont(self):
		font, ok = QFontDialog.getFont(globalSettings.font, self)
		if ok:
			self.setPreviewFont(font)

	def setPreviewFont(self, font):
		globalSettings.font = font
		for tab in self.iterateTabs():
			tab.triggerPreviewUpdate()

	def preview(self, viewmode):
		self.currentTab.previewState = viewmode * 2
		self.actionLivePreview.setChecked(False)
		self.editBar.setDisabled(viewmode)
		self.currentTab.updateBoxesVisibility()
		self.currentTab.triggerPreviewUpdate()

	def enableLivePreview(self, livemode):
		self.currentTab.previewState = int(livemode)
		self.actionPreview.setChecked(livemode)
		self.editBar.setEnabled(True)
		self.currentTab.updateBoxesVisibility()
		self.currentTab.triggerPreviewUpdate()

	def enableWebKit(self, enable):
		globalSettings.useWebKit = enable
		globalSettings.useWebEngine = False
		for tab in self.iterateTabs():
			tab.rebuildPreviewBox()

	def enableWebEngine(self, enable):
		globalSettings.useWebKit = False
		globalSettings.useWebEngine = enable
		for tab in self.iterateTabs():
			tab.rebuildPreviewBox()

	def enableCopy(self, copymode):
		self.actionCopy.setEnabled(copymode)
		self.actionCut.setEnabled(copymode)

	def enableFullScreen(self, yes):
		if yes:
			self.showFullScreen()
		else:
			self.showNormal()

	def openConfigDialog(self):
		dlg = ConfigDialog(self)
		dlg.setWindowTitle(self.tr('Preferences'))
		dlg.show()

	def enableFakeVimMode(self, yes):
		globalSettings.useFakeVim = yes
		if yes:
			FakeVimMode.init(self)
			for tab in self.iterateTabs():
				tab.editBox.installFakeVimHandler()
		else:
			FakeVimMode.exit(self)

	def enableSpellCheck(self, yes):
		try:
			dict = enchant.Dict(self.sl or None)
		except enchant.errors.Error as e:
			QMessageBox.warning(self, '', str(e))
			self.actionEnableSC.setChecked(False)
			yes = False
		self.setAllDictionaries(dict if yes else None)
		globalSettings.spellCheck = yes

	def setAllDictionaries(self, dictionary):
		for tab in self.iterateTabs():
			hl = tab.highlighter
			hl.dictionary = dictionary
			hl.rehighlight()

	def changeLocale(self):
		localedlg = LocaleDialog(self, defaultText=self.sl)
		if localedlg.exec() != QDialog.Accepted:
			return
		sl = localedlg.localeEdit.text()
		try:
			enchant.Dict(sl or None)
		except enchant.errors.Error as e:
			QMessageBox.warning(self, '', str(e))
		else:
			self.sl = sl or None
			self.enableSpellCheck(self.actionEnableSC.isChecked())
			if localedlg.checkBox.isChecked():
				globalSettings.spellCheckLocale = sl

	def search(self):
		self.searchBar.setVisible(True)
		self.searchEdit.setFocus(Qt.ShortcutFocusReason)

	def goToLine(self):
		line, ok = QInputDialog.getInt(self, self.tr("Go to line"), self.tr("Type the line number"))
		if ok:
			self.currentTab.goToLine(line-1)

	def searchBarVisibilityChanged(self, visible):
		if visible:
			self.searchEdit.setFocus(Qt.ShortcutFocusReason)

	def find(self, back=False, replace=False):
		flags = QTextDocument.FindFlags()
		if back:
			flags |= QTextDocument.FindBackward
		if self.csBox.isChecked():
			flags |= QTextDocument.FindCaseSensitively
		text = self.searchEdit.text()
		replaceText = self.replaceEdit.text() if replace else None
		found = self.currentTab.find(text, flags, replaceText=replaceText)
		self.setSearchEditColor(found)

	def replaceAll(self):
		text = self.searchEdit.text()
		replaceText = self.replaceEdit.text()
		found = self.currentTab.replaceAll(text, replaceText)
		self.setSearchEditColor(found)

	def setSearchEditColor(self, found):
		palette = self.searchEdit.palette()
		palette.setColor(QPalette.Active, QPalette.Base,
		                 Qt.white if found else QColor(255, 102, 102))
		self.searchEdit.setPalette(palette)

	def showInDir(self):
		if self.currentTab.fileName:
			path = QFileInfo(self.currentTab.fileName).path()
			QDesktopServices.openUrl(QUrl.fromLocalFile(path))
		else:
			QMessageBox.warning(self, '', self.tr("Please, save the file somewhere."))

	def moveToTopOfRecentFileList(self, fileName):
		if fileName:
			files = readListFromSettings("recentFileList")
			if fileName in files:
				files.remove(fileName)
			files.insert(0, fileName)
			recentCount = globalSettings.recentDocumentsCount
			if len(files) > recentCount:
				del files[recentCount:]
			writeListToSettings("recentFileList", files)

	def createNew(self, text=None):
		self.createTab("")
		self.ind = self.tabWidget.count()-1
		self.tabWidget.setCurrentIndex(self.ind)
		if text:
			self.currentTab.editBox.textCursor().insertText(text)

	def switchTab(self, shift=1):
		self.tabWidget.setCurrentIndex((self.ind + shift) % self.tabWidget.count())

	def updateRecentFiles(self):
		self.menuRecentFiles.clear()
		self.recentFilesActions = []
		filesOld = readListFromSettings("recentFileList")
		files = []
		for f in filesOld:
			if QFile.exists(f):
				files.append(f)
				self.recentFilesActions.append(self.act(f, trig=self.openFunction(f)))
		writeListToSettings("recentFileList", files)
		for action in self.recentFilesActions:
			self.menuRecentFiles.addAction(action)

	def markupFunction(self, markup):
		return lambda: self.setDefaultMarkup(markup)

	def openFunction(self, fileName):
		return lambda: self.openFileWrapper(fileName)

	def extensionFunction(self, data):
		return lambda: \
		self.runExtensionCommand(data['Exec'], data['FileFilter'], data['DefaultExtension'])

	def getExportExtensionsList(self):
		extensions = []
		for extsprefix in datadirs:
			extsdir = QDir(extsprefix+'/export-extensions/')
			if extsdir.exists():
				for fileInfo in extsdir.entryInfoList(['*.desktop', '*.ini'],
				QDir.Files | QDir.Readable):
					extensions.append(self.readExtension(fileInfo.filePath()))
		locale = QLocale.system().name()
		self.extensionActions = []
		for extension in extensions:
			try:
				if ('Name[%s]' % locale) in extension:
					name = extension['Name[%s]' % locale]
				elif ('Name[%s]' % locale.split('_')[0]) in extension:
					name = extension['Name[%s]' % locale.split('_')[0]]
				else:
					name = extension['Name']
				data = {}
				for prop in ('FileFilter', 'DefaultExtension', 'Exec'):
					if 'X-ReText-'+prop in extension:
						data[prop] = extension['X-ReText-'+prop]
					elif prop in extension:
						data[prop] = extension[prop]
					else:
						data[prop] = ''
				action = self.act(name, trig=self.extensionFunction(data))
				if 'Icon' in extension:
					action.setIcon(self.actIcon(extension['Icon']))
				mimetype = extension['MimeType'] if 'MimeType' in extension else None
			except KeyError:
				print('Failed to parse extension: Name is required', file=sys.stderr)
			else:
				self.extensionActions.append((action, mimetype))

	def updateExtensionsVisibility(self):
		markupClass = self.currentTab.getActiveMarkupClass()
		for action in self.extensionActions:
			if markupClass is None:
				action[0].setEnabled(False)
				continue
			mimetype = action[1]
			if mimetype is None:
				enabled = True
			elif markupClass == markups.MarkdownMarkup:
				enabled = (mimetype in ("text/x-retext-markdown", "text/x-markdown", "text/markdown"))
			elif markupClass == markups.ReStructuredTextMarkup:
				enabled = (mimetype in ("text/x-retext-rst", "text/x-rst"))
			else:
				enabled = False
			action[0].setEnabled(enabled)

	def readExtension(self, fileName):
		extFile = QFile(fileName)
		extFile.open(QIODevice.ReadOnly)
		extension = {}
		stream = QTextStream(extFile)
		while not stream.atEnd():
			line = stream.readLine()
			if '=' in line:
				index = line.index('=')
				extension[line[:index].rstrip()] = line[index+1:].lstrip()
		extFile.close()
		return extension

	def openFile(self):
		supportedExtensions = ['.txt']
		for markup in markups.get_all_markups():
			supportedExtensions += markup.file_extensions
		fileFilter = ' (' + str.join(' ', ['*'+ext for ext in supportedExtensions]) + ');;'
		fileNames = QFileDialog.getOpenFileNames(self,
			self.tr("Select one or several files to open"), QDir.currentPath(),
			self.tr("Supported files") + fileFilter + self.tr("All files (*)"))
		for fileName in fileNames[0]:
			self.openFileWrapper(fileName)

	@pyqtSlot(str)
	def openFileWrapper(self, fileName):
		if not fileName:
			return
		fileName = QFileInfo(fileName).canonicalFilePath()
		exists = False
		for i, tab in enumerate(self.iterateTabs()):
			if tab.fileName == fileName:
				exists = True
				ex = i
		if exists:
			self.tabWidget.setCurrentIndex(ex)
		elif QFile.exists(fileName):
			noEmptyTab = (
				(self.ind is None) or
				self.currentTab.fileName or
				self.currentTab.editBox.toPlainText() or
				self.currentTab.editBox.document().isModified()
			)
			if noEmptyTab:
				self.createTab(fileName)
				self.ind = self.tabWidget.count()-1
				self.tabWidget.setCurrentIndex(self.ind)
			if fileName:
				self.fileSystemWatcher.addPath(fileName)
			self.currentTab.readTextFromFile(fileName)
			self.moveToTopOfRecentFileList(self.currentTab.fileName)

	def showEncodingDialog(self):
		if not self.maybeSave(self.ind):
			return
		codecsSet = set(bytes(QTextCodec.codecForName(alias).name())
		                for alias in QTextCodec.availableCodecs())
		encoding, ok = QInputDialog.getItem(self, '',
			self.tr('Select file encoding from the list:'),
			[bytes(b).decode() for b in sorted(codecsSet)],
			0, False)
		if ok:
			self.currentTab.readTextFromFile(None, encoding)

	def saveFileAs(self):
		self.saveFile(dlg=True)

	def saveAll(self):
		for tab in self.iterateTabs():
			if (tab.fileName and tab.editBox.document().isModified()
				and QFileInfo(tab.fileName).isWritable()):
				tab.saveTextToFile()

	def saveFile(self, dlg=False):
		fileNameToSave = self.currentTab.fileName

		if (not fileNameToSave) or dlg:
			proposedFileName = ""
			markupClass = self.currentTab.getActiveMarkupClass()
			if (markupClass is None) or not hasattr(markupClass, 'default_extension'):
				defaultExt = self.tr("Plain text (*.txt)")
				ext = ".txt"
			else:
				defaultExt = self.tr('%s files',
					'Example of final string: Markdown files') \
					% markupClass.name + ' (' + str.join(' ',
					('*'+extension for extension in markupClass.file_extensions)) + ')'
				if markupClass == markups.MarkdownMarkup:
					ext = globalSettings.markdownDefaultFileExtension
				elif markupClass == markups.ReStructuredTextMarkup:
					ext = globalSettings.restDefaultFileExtension
				else:
					ext = markupClass.default_extension
			if fileNameToSave is not None:
				proposedFileName = fileNameToSave
			fileNameToSave = QFileDialog.getSaveFileName(self,
				self.tr("Save file"), proposedFileName, defaultExt)[0]
			if fileNameToSave:
				if not QFileInfo(fileNameToSave).suffix():
					fileNameToSave += ext
				# Make sure we don't overwrite a file opened in other tab
				for tab in self.iterateTabs():
					if tab is not self.currentTab and tab.fileName == fileNameToSave:
						QMessageBox.warning(self, "",
							self.tr("Cannot save to file which is open in another tab!"))
						return False
				self.actionSetEncoding.setDisabled(self.autoSaveActive())
		if fileNameToSave:
			if self.currentTab.saveTextToFile(fileNameToSave):
				self.moveToTopOfRecentFileList(self.currentTab.fileName)
				return True
			else:
				QMessageBox.warning(self, '',
				self.tr("Cannot save to file because it is read-only!"))
		return False

	def saveHtml(self, fileName):
		if not QFileInfo(fileName).suffix():
			fileName += ".html"
		try:
			_, htmltext, _ = self.currentTab.getDocumentForExport(webenv=True)
		except Exception:
			return self.printError()
		htmlFile = QFile(fileName)
		result = htmlFile.open(QIODevice.WriteOnly)
		if not result:
			QMessageBox.warning(self, '',
				self.tr("Cannot save to file because it is read-only!"))
			return
		html = QTextStream(htmlFile)
		if globalSettings.defaultCodec:
			html.setCodec(globalSettings.defaultCodec)
		html << htmltext
		htmlFile.close()

	def textDocument(self, title, htmltext):
		td = QTextDocument()
		td.setMetaInformation(QTextDocument.DocumentTitle, title)
		td.setHtml(htmltext)
		td.setDefaultFont(globalSettings.font)
		return td

	def saveOdf(self):
		title, htmltext, _ = self.currentTab.getDocumentForExport()
		try:
			document = self.textDocument(title, htmltext)
		except Exception:
			return self.printError()
		fileName = QFileDialog.getSaveFileName(self,
			self.tr("Export document to ODT"), self.currentTab.getBaseName() + ".odt",
			self.tr("OpenDocument text files (*.odt)"))[0]
		if not QFileInfo(fileName).suffix():
			fileName += ".odt"
		writer = QTextDocumentWriter(fileName)
		writer.setFormat(b"odf")
		writer.write(document)

	def saveFileHtml(self):
		fileName = QFileDialog.getSaveFileName(self,
			self.tr("Save file"), self.currentTab.getBaseName() + ".html",
			self.tr("HTML files (*.html *.htm)"))[0]
		if fileName:
			self.saveHtml(fileName)

	def getDocumentForPrint(self, title, htmltext, preview):
		if globalSettings.useWebKit:
			return preview
		try:
			return self.textDocument(title, htmltext)
		except Exception:
			self.printError()

	def standardPrinter(self, title):
		printer = QPrinter(QPrinter.HighResolution)
		printer.setDocName(title)
		printer.setCreator('ReText %s' % app_version)
		if globalSettings.paperSize:
			pageSize = self.getPageSizeByName(globalSettings.paperSize)
			if pageSize is not None:
				printer.setPaperSize(pageSize)
			else:
				QMessageBox.warning(self, '',
					self.tr('Unrecognized paperSize setting "%s".') %
					globalSettings.paperSize)
		return printer

	def getPageSizeByName(self, pageSizeName):
		""" Returns a validated PageSize instance corresponding to the given
		name. Returns None if the name is not a valid PageSize.
		"""
		pageSize = None

		lowerCaseNames = {pageSize.lower(): pageSize for pageSize in
		                  self.availablePageSizes()}
		if pageSizeName.lower() in lowerCaseNames:
			pageSize = getattr(QPagedPaintDevice, lowerCaseNames[pageSizeName.lower()])

		return pageSize

	def availablePageSizes(self):
		""" List available page sizes. """

		sizes = [x for x in dir(QPagedPaintDevice)
		         if type(getattr(QPagedPaintDevice, x)) == QPagedPaintDevice.PageSize]
		return sizes

	def savePdf(self):
		fileName = QFileDialog.getSaveFileName(self,
			self.tr("Export document to PDF"),
			self.currentTab.getBaseName() + ".pdf",
			self.tr("PDF files (*.pdf)"))[0]
		if fileName:
			if not QFileInfo(fileName).suffix():
				fileName += ".pdf"
			title, htmltext, preview = self.currentTab.getDocumentForExport()
			if globalSettings.useWebEngine and hasattr(preview.page(), "printToPdf"):
				pageSize = self.getPageSizeByName(globalSettings.paperSize)
				if pageSize is None:
					pageSize = QPageSize(QPageSize.A4)
				margins = QMarginsF(20, 20, 13, 20)  # left, top, right, bottom (in millimeters)
				layout = QPageLayout(pageSize, QPageLayout.Portrait, margins, QPageLayout.Millimeter)
				preview.page().printToPdf(fileName, layout)  # Available since Qt 5.7
				return
			printer = self.standardPrinter(title)
			printer.setOutputFormat(QPrinter.PdfFormat)
			printer.setOutputFileName(fileName)
			document = self.getDocumentForPrint(title, htmltext, preview)
			if document != None:
				document.print(printer)

	def printFile(self):
		title, htmltext, preview = self.currentTab.getDocumentForExport()
		printer = self.standardPrinter(title)
		dlg = QPrintDialog(printer, self)
		dlg.setWindowTitle(self.tr("Print document"))
		if (dlg.exec() == QDialog.Accepted):
			document = self.getDocumentForPrint(title, htmltext, preview)
			if document != None:
				document.print(printer)

	def printPreview(self):
		title, htmltext, preview = self.currentTab.getDocumentForExport()
		document = self.getDocumentForPrint(title, htmltext, preview)
		if document is None:
			return
		printer = self.standardPrinter(title)
		preview = QPrintPreviewDialog(printer, self)
		preview.paintRequested.connect(document.print)
		preview.exec()

	def runExtensionCommand(self, command, filefilter, defaultext):
		import shlex
		of = ('%of' in command)
		html = ('%html' in command)
		if of:
			if defaultext and not filefilter:
				filefilter = '*'+defaultext
			fileName = QFileDialog.getSaveFileName(self,
				self.tr('Export document'), '', filefilter)[0]
			if not fileName:
				return
			if defaultext and not QFileInfo(fileName).suffix():
				fileName += defaultext
		else:
			fileName = 'out' + defaultext
		basename = '.%s.retext-temp' % self.currentTab.getBaseName()
		if html:
			tmpname = basename+'.html'
			self.saveHtml(tmpname)
		else:
			tmpname = basename + self.currentTab.getActiveMarkupClass().default_extension
			self.currentTab.writeTextToFile(tmpname)
		command = command.replace('%of', shlex.quote(fileName))
		command = command.replace('%html' if html else '%if', shlex.quote(tmpname))
		try:
			Popen(str(command), shell=True).wait()
		except Exception as error:
			errorstr = str(error)
			QMessageBox.warning(self, '', self.tr('Failed to execute the command:')
			+ '\n' + errorstr)
		QFile(tmpname).remove()

	def autoSaveActive(self, tab=None):
		tab = tab if tab else self.currentTab
		return bool(self.autoSaveEnabled and tab.fileName and
			    QFileInfo(tab.fileName).isWritable())

	def clipboardDataChanged(self):
		mimeData = QApplication.instance().clipboard().mimeData()
		if mimeData is not None:
			self.actionPaste.setEnabled(mimeData.hasText())
			self.actionPasteImage.setEnabled(mimeData.hasImage())

	def insertFormatting(self, formatting):
		if formatting == 'table':
			dialog = InsertTableDialog(self)
			dialog.show()
			self.formattingBox.setCurrentIndex(0)
			return

		cursor = self.currentTab.editBox.textCursor()
		text = cursor.selectedText()
		moveCursorTo = None

		def c(cursor):
			nonlocal moveCursorTo
			moveCursorTo = cursor.position()

		def ensurenl(cursor):
			if not cursor.atBlockStart():
				cursor.insertText('\n\n')

		toinsert = {
			'header': (ensurenl, '# ', text),
			'italic': ('*', text, c, '*'),
			'bold': ('**', text, c, '**'),
			'underline': ('<u>', text, c, '</u>'),
			'numbering': (ensurenl, ' 1. ', text),
			'bullets': (ensurenl, '  * ', text),
			'image': ('![', text or self.tr('Alt text'), c, '](', self.tr('URL'), ')'),
			'link': ('[', text or self.tr('Link text'), c, '](', self.tr('URL'), ')'),
			'inline code': ('`', text, c, '`'),
			'code block': (ensurenl, '    ', text),
			'blockquote': (ensurenl, '> ', text),
		}

		if formatting not in toinsert:
			return

		cursor.beginEditBlock()
		for token in toinsert[formatting]:
			if callable(token):
				token(cursor)
			else:
				cursor.insertText(token)
		cursor.endEditBlock()

		self.formattingBox.setCurrentIndex(0)
		# Bring back the focus on the editor
		self.currentTab.editBox.setFocus(Qt.OtherFocusReason)

		if moveCursorTo:
			cursor.setPosition(moveCursorTo)
			self.currentTab.editBox.setTextCursor(cursor)

	def insertSymbol(self, num):
		if num:
			self.currentTab.editBox.insertPlainText('&'+self.usefulChars[num-1]+';')
		self.symbolBox.setCurrentIndex(0)

	def fileChanged(self, fileName):
		tab = None
		for testtab in self.iterateTabs():
			if testtab.fileName == fileName:
				tab = testtab
		if tab is None:
			self.fileSystemWatcher.removePath(fileName)
			return
		if not QFile.exists(fileName):
			self.tabWidget.setCurrentWidget(tab)
			tab.editBox.document().setModified(True)
			QMessageBox.warning(self, '', self.tr(
				'This file has been deleted by other application.\n'
				'Please make sure you save the file before exit.'))
		elif not tab.editBox.document().isModified():
			# File was not modified in ReText, reload silently
			tab.readTextFromFile()
		else:
			self.tabWidget.setCurrentWidget(tab)
			text = self.tr(
				'This document has been modified by other application.\n'
				'Do you want to reload the file (this will discard all '
				'your changes)?\n')
			if self.autoSaveEnabled:
				text += self.tr(
					'If you choose to not reload the file, auto save mode will '
					'be disabled for this session to prevent data loss.')
			messageBox = QMessageBox(QMessageBox.Warning, '', text)
			reloadButton = messageBox.addButton(self.tr('Reload'), QMessageBox.YesRole)
			messageBox.addButton(QMessageBox.Cancel)
			messageBox.exec()
			if messageBox.clickedButton() is reloadButton:
				tab.readTextFromFile()
			else:
				self.autoSaveEnabled = False
				tab.editBox.document().setModified(True)
		if fileName not in self.fileSystemWatcher.files():
			# https://github.com/retext-project/retext/issues/137
			self.fileSystemWatcher.addPath(fileName)

	def maybeSave(self, ind):
		tab = self.tabWidget.widget(ind)
		if self.autoSaveActive(tab):
			tab.saveTextToFile()
			return True
		if not tab.editBox.document().isModified():
			return True
		self.tabWidget.setCurrentIndex(ind)
		ret = QMessageBox.warning(self, '',
			self.tr("The document has been modified.\nDo you want to save your changes?"),
			QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel)
		if ret == QMessageBox.Save:
			return self.saveFile(False)
		elif ret == QMessageBox.Cancel:
			return False
		return True

	def closeEvent(self, closeevent):
		for ind in range(self.tabWidget.count()):
			if not self.maybeSave(ind):
				return closeevent.ignore()
		if globalSettings.saveWindowGeometry:
			globalSettings.windowGeometry = self.saveGeometry()
		if globalSettings.openLastFilesOnStartup:
			files = [tab.fileName for tab in self.iterateTabs()]
			writeListToSettings("lastFileList", files)
			globalSettings.lastTabIndex = self.tabWidget.currentIndex()
		closeevent.accept()

	def viewHtml(self):
		htmlDlg = HtmlDialog(self)
		try:
			_, htmltext, _ = self.currentTab.getDocumentForExport(includeStyleSheet=False)
		except Exception:
			return self.printError()
		winTitle = self.currentTab.getBaseName()
		htmlDlg.setWindowTitle(winTitle+" ("+self.tr("HTML code")+")")
		htmlDlg.textEdit.setPlainText(htmltext.rstrip())
		htmlDlg.hl.rehighlight()
		htmlDlg.show()
		htmlDlg.raise_()
		htmlDlg.activateWindow()

	def insertImages(self):
		supportedExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp']
		fileFilter = ' (%s);;' % ' '.join('*' + ext for ext in supportedExtensions)
		fileNames, _selectedFilter = QFileDialog.getOpenFileNames(self,
			self.tr("Select one or several images to open"), QDir.currentPath(),
			self.tr("Supported files") + fileFilter + self.tr("All files (*)"))

		cursor = self.currentTab.editBox.textCursor()

		imagesMarkup = '\n'.join(
			self.currentTab.editBox.getImageMarkup(fileName)
			for fileName in fileNames)
		cursor.insertText(imagesMarkup)

		self.formattingBox.setCurrentIndex(0)
		self.currentTab.editBox.setFocus(Qt.OtherFocusReason)

	def openHelp(self):
		QDesktopServices.openUrl(QUrl('https://github.com/retext-project/retext/wiki'))

	def aboutDialog(self):
		QMessageBox.about(self, self.aboutWindowTitle,
		'<p><b>' + (self.tr('ReText %s (using PyMarkups %s)') % (app_version, markups.__version__))
		+'</b></p>' + self.tr('Simple but powerful editor'
		' for Markdown and reStructuredText')
		+'</p><p>'+self.tr('Author: Dmitry Shachnev, 2011').replace('2011', '2011–2020')
		+'<br><a href="https://github.com/retext-project/retext">'+self.tr('Website')
		+'</a> | <a href="http://daringfireball.net/projects/markdown/syntax">'
		+self.tr('Markdown syntax')
		+'</a> | <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">'
		+self.tr('reStructuredText syntax')+'</a></p>')

	def setDefaultMarkup(self, markupClass):
		globalSettings.defaultMarkup = markupClass.name
		for tab in self.iterateTabs():
			if not tab.fileName:
				tab.updateActiveMarkupClass()
Exemple #36
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 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': '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()
        self.updatePreferences()

        # 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(50)
        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)

    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.toggle_wrap_mode(self.preferences['Line wrap'])

    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.removePath(self.filename)
                self._file_watch_timer.stop()

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

            if self.preferences['Autoreload']:
                self._file_watcher.addPath(self.filename)
                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._file_watcher.removePath(self._watched_file)
            self._watched_file = None
        if self.preferences[
                'Autoreload'] and self.filename and self.filename != self._watched_file:
            self._watched_file = self._filename
            self._file_watcher.addPath(self.filename)

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

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

    # callback triggered by QFileSystemWatcher
    def _file_changed(self):
        # neovim writes a file by removing it first
        # this causes QFileSystemWatcher to forget the file
        self._file_watcher.addPath(self._filename)
        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', self.filename)

        if filename and filename != '':
            try:
                self.load_from_file(filename)
            except IOError:
                self._logger.warning(f'could not open {filename}')
Exemple #38
0
class NFile(QObject):
    """
    SIGNALS:
    @askForSaveFileClosing(QString)
    @fileClosing(QString)
    @fileChanged()
    @willDelete(PyQt_PyObject, PyQt_PyObject)
    @willOverWrite(PyQt_PyObject, QString, QString)
    @willMove(Qt_PyQtObject, QString, QString)
    @willSave(QString, QString)
    @savedAsNewFile(PyQt_PyObject, QString, QString)
    @gotAPath(PyQt_PyObject)
    @willAttachToExistingFile(PyQt_PyObject, QString)
    """
    fileChanged = pyqtSignal()
    fileRemoved = pyqtSignal()
    fileReaded = pyqtSignal()
    willAttachToExistingFile = pyqtSignal('PyQt_PyObject', 'QString')
    gotAPath = pyqtSignal('PyQt_PyObject')
    willSave = pyqtSignal('QString', 'QString')
    willMove = pyqtSignal('PyQt_PyObject', 'QString', 'QString')
    willOverWrite = pyqtSignal('PyQt_PyObject', 'QString', 'QString')
    willCopyTo = pyqtSignal('PyQt_PyObject', 'QString', 'QString')
    willDelete = pyqtSignal('PyQt_PyObject', 'PyQt_PyObject')
    fileClosing = pyqtSignal('QString', bool)

    def __init__(self, path=None):
        """
        """
        self._file_path = path
        self.__created = False
        self.__watcher = None
        self.__mtime = None
        super(NFile, self).__init__()
        if not self._exists():
            self.__created = True

    @property
    def file_name(self):
        """"Returns filename of nfile"""
        file_name = None
        if self._file_path is None:
            file_name = translations.TR_NEW_DOCUMENT
        else:
            file_name = get_basename(self._file_path)
        return file_name

    @property
    def display_name(self):
        """Returns a pretty name to be displayed by tabs"""
        display_name = self.file_name
        if self._file_path is not None and not self.has_write_permission():
            display_name += translations.TR_READ_ONLY
        return display_name

    @property
    def is_new_file(self):
        return self.__created

    def file_ext(self):
        """"Returns extension of nfile"""
        if self._file_path is None:
            return ''
        return get_file_extension(self._file_path)

    @property
    def file_path(self):
        """"Returns file path of nfile"""
        return self._file_path

    def start_watching(self):
        """Create a file system watcher and connect its fileChanged
        SIGNAL to our _file_changed SLOT"""
        if self.__watcher is None:
            self.__watcher = QFileSystemWatcher(self)
            self.__watcher.fileChanged['const QString&'].connect(
                self._file_changed)
        if self._file_path is not None:
            self.__mtime = os.path.getmtime(self._file_path)
            self.__watcher.addPath(self._file_path)

    def _file_changed(self, path):
        if self._exists():
            current_mtime = os.path.getmtime(self._file_path)
            if current_mtime != self.__mtime:
                self.__mtime = current_mtime
                self.fileChanged.emit()
        else:
            self.fileRemoved.emit()

    def has_write_permission(self):
        if not self._exists():
            return True
        return os.access(self._file_path, os.W_OK)

    def _exists(self):
        """
        Check if we have been created with a path and if such path exists
        In case there is no path, we are most likely a new file.
        """
        file_exists = False
        if self._file_path and os.path.exists(self._file_path):
            file_exists = True
        return file_exists

    def attach_to_path(self, new_path):
        if os.path.exists(new_path):
            signal_handler = SignalFlowControl()
            self.willAttachToExistingFile.emit(signal_handler, new_path)
            if signal_handler.stopped():
                return
        self._file_path = new_path
        self.gotAPath.emit(self)
        return self._file_path

    def create(self):
        if self.__created:
            self.save("")
        self.__created = False

    def save(self, content, path=None):
        """
        Write a temporary file with .tnj extension and copy it over the
        original one.
        .nsf = Ninja Swap File
        # FIXME: Where to locate addExtension, does not fit here
        """
        new_path = False
        if path:
            self.attach_to_path(path)
            new_path = True

        save_path = self._file_path

        if not save_path:
            raise NinjaNoFileNameException("I am asked to write a "
                                           "file but no one told me where")
        swap_save_path = "%s.nsp" % save_path

        # If we have a file system watcher, remove the file path
        # from its watch list until we are done making changes.
        if self.__watcher is not None:
            self.__watcher.removePath(save_path)

        flags = QIODevice.WriteOnly | QIODevice.Truncate
        f = QFile(swap_save_path)
        if settings.use_platform_specific_eol():
            flags |= QIODevice.Text

        if not f.open(flags):
            raise NinjaIOException(f.errorString())

        stream = QTextStream(f)
        encoding = get_file_encoding(content)
        if encoding:
            stream.setCodec(encoding)

        encoded_stream = stream.codec().fromUnicode(content)
        f.write(encoded_stream)
        f.flush()
        f.close()
        # SIGNAL: Will save (temp, definitive) to warn folder to do something
        self.willSave.emit(swap_save_path, save_path)
        self.__mtime = os.path.getmtime(swap_save_path)
        shutil.move(swap_save_path, save_path)
        self.reset_state()

        # If we have a file system watcher, add the saved path back
        # to its watch list, otherwise create a watcher and start
        # watching
        if self.__watcher is not None:
            if new_path:
                # FIXME: what?
                # self.__watcher.removePath(self.__watcher.files()[0])
                self.__watcher.addPath(self._file_path)
            else:
                self.__watcher.addPath(save_path)
        else:
            self.start_watching()
        return self

    def reset_state(self):
        """
        #FIXE: to have a ref to changed I need to have the doc here
        """
        self.__created = False

    def read(self, path=None):
        """
        Read the file or fail
        """
        open_path = path and path or self._file_path
        self._file_path = open_path
        if not self._file_path:
            raise NinjaNoFileNameException("I am asked to read a file "
                                           "but no one told me from where")
        try:
            with open(open_path, 'r') as f:
                content = f.read()
        except IOError as reason:
            raise NinjaIOException(reason)
        self.fileReaded.emit()
        return content

    def move(self, new_path):
        """
        Phisically move the file
        """
        if self._exists():
            signal_handler = SignalFlowControl()
            # SIGNALL: WILL MOVE TO, to warn folder to exist
            self.willMove.emit(signal_handler, self._file_path, new_path)
            if signal_handler.stopped():
                return
            if os.path.exists(new_path):
                signal_handler = SignalFlowControl()
                self.willOverWrite.emit(signal_handler, self._file_path,
                                        new_path)
                if signal_handler.stopped():
                    return
            if self.__watcher is not None:
                self.__watcher.removePath(self._file_path)
            shutil.move(self._file_path, new_path)
            if self.__watcher:
                self.__watcher.addPath(new_path)
        self._file_path = new_path
        return

    def copy(self, new_path):
        """
        Copy the file to a new path
        """
        if self._exists():
            signal_handler = SignalFlowControl()
            # SIGNALL: WILL COPY TO, to warn folder to exist
            self.willCopyTo.emit(signal_handler, self._file_path, new_path)
            if signal_handler.stopped():
                return
            if os.path.exists(new_path):
                signal_handler = SignalFlowControl()
                self.willOverWrite.emit(signal_handler, self._file_path,
                                        new_path)
                if signal_handler.stopped():
                    return

            shutil.copy(self._file_path, new_path)

    def delete(self, force=False):
        """
        This deletes the object and closes the file.
        """
        # if created but exists this file migth to someone else
        self.close()
        if ((not self.__created) or force) and self._exists():
            DEBUG("Deleting our own NFile %s" % self._file_path)
            signal_handler = SignalFlowControl()
            self.willDelete.emit(signal_handler, self)
            if not signal_handler.stopped():
                if self.__watcher is not None:
                    self.__watcher.removePath(self._file_path)
                os.remove(self._file_path)

    def close(self, force_close=False):
        """
        Lets let people know we are going down so they can act upon
        As you can see close does nothing but let everyone know that we are
        not saved yet
        """
        DEBUG("About to close NFile")
        self.fileClosing.emit(self._file_path, force_close)

    def remove_watcher(self):
        if self.__watcher is not None:
            self.__watcher.removePath(self._file_path)
Exemple #39
0
class ReTextWindow(QMainWindow):
	def __init__(self, parent=None):
		QMainWindow.__init__(self, parent)
		self.resize(950, 700)
		screenRect = QDesktopWidget().screenGeometry()
		if globalSettings.windowGeometry:
			self.restoreGeometry(globalSettings.windowGeometry)
		else:
			self.move((screenRect.width()-self.width())/2, (screenRect.height()-self.height())/2)
		if not screenRect.contains(self.geometry()):
			self.showMaximized()
		if globalSettings.iconTheme:
			QIcon.setThemeName(globalSettings.iconTheme)
		if QIcon.themeName() in ('hicolor', ''):
			if not QFile.exists(icon_path + 'document-new.png'):
				QIcon.setThemeName(get_icon_theme())
		if QFile.exists(icon_path+'retext.png'):
			self.setWindowIcon(QIcon(icon_path+'retext.png'))
		elif QFile.exists('/usr/share/pixmaps/retext.png'):
			self.setWindowIcon(QIcon('/usr/share/pixmaps/retext.png'))
		else:
			self.setWindowIcon(QIcon.fromTheme('retext',
				QIcon.fromTheme('accessories-text-editor')))
		self.tabWidget = QTabWidget(self)
		self.initTabWidget()
		self.setCentralWidget(self.tabWidget)
		self.tabWidget.currentChanged.connect(self.changeIndex)
		self.tabWidget.tabCloseRequested.connect(self.closeTab)
		toolBar = QToolBar(self.tr('File toolbar'), self)
		self.addToolBar(Qt.TopToolBarArea, toolBar)
		self.editBar = QToolBar(self.tr('Edit toolbar'), self)
		self.addToolBar(Qt.TopToolBarArea, self.editBar)
		self.searchBar = QToolBar(self.tr('Search toolbar'), self)
		self.addToolBar(Qt.BottomToolBarArea, self.searchBar)
		toolBar.setVisible(not globalSettings.hideToolBar)
		self.editBar.setVisible(not globalSettings.hideToolBar)
		self.actionNew = self.act(self.tr('New'), 'document-new',
			self.createNew, shct=QKeySequence.New)
		self.actionNew.setPriority(QAction.LowPriority)
		self.actionOpen = self.act(self.tr('Open'), 'document-open',
			self.openFile, shct=QKeySequence.Open)
		self.actionOpen.setPriority(QAction.LowPriority)
		self.actionSetEncoding = self.act(self.tr('Set encoding'),
			trig=self.showEncodingDialog)
		self.actionSetEncoding.setEnabled(False)
		self.actionReload = self.act(self.tr('Reload'), 'view-refresh',
			lambda: self.currentTab.readTextFromFile())
		self.actionReload.setEnabled(False)
		self.actionSave = self.act(self.tr('Save'), 'document-save',
			self.saveFile, shct=QKeySequence.Save)
		self.actionSave.setEnabled(False)
		self.actionSave.setPriority(QAction.LowPriority)
		self.actionSaveAs = self.act(self.tr('Save as'), 'document-save-as',
			self.saveFileAs, shct=QKeySequence.SaveAs)
		self.actionNextTab = self.act(self.tr('Next tab'), 'go-next',
			lambda: self.switchTab(1), shct=Qt.CTRL+Qt.Key_PageDown)
		self.actionPrevTab = self.act(self.tr('Previous tab'), 'go-previous',
			lambda: self.switchTab(-1), shct=Qt.CTRL+Qt.Key_PageUp)
		self.actionPrint = self.act(self.tr('Print'), 'document-print',
			self.printFile, shct=QKeySequence.Print)
		self.actionPrint.setPriority(QAction.LowPriority)
		self.actionPrintPreview = self.act(self.tr('Print preview'), 'document-print-preview',
			self.printPreview)
		self.actionViewHtml = self.act(self.tr('View HTML code'), 'text-html', self.viewHtml)
		self.actionChangeEditorFont = self.act(self.tr('Change editor font'),
			trig=self.changeEditorFont)
		self.actionChangePreviewFont = self.act(self.tr('Change preview font'),
			trig=self.changePreviewFont)
		self.actionSearch = self.act(self.tr('Find text'), 'edit-find', shct=QKeySequence.Find)
		self.actionSearch.setCheckable(True)
		self.actionSearch.triggered[bool].connect(self.searchBar.setVisible)
		self.searchBar.visibilityChanged.connect(self.searchBarVisibilityChanged)
		self.actionPreview = self.act(self.tr('Preview'), shct=Qt.CTRL+Qt.Key_E,
			trigbool=self.preview)
		if QIcon.hasThemeIcon('document-preview'):
			self.actionPreview.setIcon(QIcon.fromTheme('document-preview'))
		elif QIcon.hasThemeIcon('preview-file'):
			self.actionPreview.setIcon(QIcon.fromTheme('preview-file'))
		elif QIcon.hasThemeIcon('x-office-document'):
			self.actionPreview.setIcon(QIcon.fromTheme('x-office-document'))
		else:
			self.actionPreview.setIcon(QIcon(icon_path+'document-preview.png'))
		self.actionLivePreview = self.act(self.tr('Live preview'), shct=Qt.CTRL+Qt.Key_L,
		trigbool=self.enableLivePreview)
		menuPreview = QMenu()
		menuPreview.addAction(self.actionLivePreview)
		self.actionPreview.setMenu(menuPreview)
		self.actionTableMode = self.act(self.tr('Table mode'), shct=Qt.CTRL+Qt.Key_T,
			trigbool=lambda x: self.currentTab.editBox.enableTableMode(x))
		if ReTextFakeVimHandler:
			self.actionFakeVimMode = self.act(self.tr('FakeVim mode'),
				shct=Qt.CTRL+Qt.ALT+Qt.Key_V, trigbool=self.enableFakeVimMode)
			if globalSettings.useFakeVim:
				self.actionFakeVimMode.setChecked(True)
				self.enableFakeVimMode(True)
		self.actionFullScreen = self.act(self.tr('Fullscreen mode'), 'view-fullscreen',
			shct=Qt.Key_F11, trigbool=self.enableFullScreen)
		self.actionFullScreen.setPriority(QAction.LowPriority)
		self.actionConfig = self.act(self.tr('Preferences'), icon='preferences-system',
			trig=self.openConfigDialog)
		self.actionConfig.setMenuRole(QAction.PreferencesRole)
		self.actionSaveHtml = self.act('HTML', 'text-html', self.saveFileHtml)
		self.actionPdf = self.act('PDF', 'application-pdf', self.savePdf)
		self.actionOdf = self.act('ODT', 'x-office-document', self.saveOdf)
		self.getExportExtensionsList()
		self.actionQuit = self.act(self.tr('Quit'), 'application-exit', shct=QKeySequence.Quit)
		self.actionQuit.setMenuRole(QAction.QuitRole)
		self.actionQuit.triggered.connect(self.close)
		self.actionUndo = self.act(self.tr('Undo'), 'edit-undo',
			lambda: self.currentTab.editBox.undo(), shct=QKeySequence.Undo)
		self.actionRedo = self.act(self.tr('Redo'), 'edit-redo',
			lambda: self.currentTab.editBox.redo(), shct=QKeySequence.Redo)
		self.actionCopy = self.act(self.tr('Copy'), 'edit-copy',
			lambda: self.currentTab.editBox.copy(), shct=QKeySequence.Copy)
		self.actionCut = self.act(self.tr('Cut'), 'edit-cut',
			lambda: self.currentTab.editBox.cut(), shct=QKeySequence.Cut)
		self.actionPaste = self.act(self.tr('Paste'), 'edit-paste',
			lambda: self.currentTab.editBox.paste(), shct=QKeySequence.Paste)
		self.actionUndo.setEnabled(False)
		self.actionRedo.setEnabled(False)
		self.actionCopy.setEnabled(False)
		self.actionCut.setEnabled(False)
		qApp = QApplication.instance()
		qApp.clipboard().dataChanged.connect(self.clipboardDataChanged)
		self.clipboardDataChanged()
		if enchant_available:
			self.actionEnableSC = self.act(self.tr('Enable'), trigbool=self.enableSpellCheck)
			self.actionSetLocale = self.act(self.tr('Set locale'), trig=self.changeLocale)
		self.actionWebKit = self.act(self.tr('Use WebKit renderer'), trigbool=self.enableWebKit)
		self.actionWebKit.setChecked(globalSettings.useWebKit)
		self.actionShow = self.act(self.tr('Show directory'), 'system-file-manager', self.showInDir)
		self.actionFind = self.act(self.tr('Next'), 'go-next', self.find,
			shct=QKeySequence.FindNext)
		self.actionFindPrev = self.act(self.tr('Previous'), 'go-previous',
			lambda: self.find(back=True), shct=QKeySequence.FindPrevious)
		self.actionCloseSearch = self.act(self.tr('Close'), 'window-close',
			lambda: self.searchBar.setVisible(False))
		self.actionCloseSearch.setPriority(QAction.LowPriority)
		self.actionHelp = self.act(self.tr('Get help online'), 'help-contents', self.openHelp)
		self.aboutWindowTitle = self.tr('About ReText')
		self.actionAbout = self.act(self.aboutWindowTitle, 'help-about', self.aboutDialog)
		self.actionAbout.setMenuRole(QAction.AboutRole)
		self.actionAboutQt = self.act(self.tr('About Qt'))
		self.actionAboutQt.setMenuRole(QAction.AboutQtRole)
		self.actionAboutQt.triggered.connect(qApp.aboutQt)
		availableMarkups = markups.get_available_markups()
		if not availableMarkups:
			print('Warning: no markups are available!')
		self.defaultMarkup = availableMarkups[0] if availableMarkups else None
		if globalSettings.defaultMarkup:
			mc = markups.find_markup_class_by_name(globalSettings.defaultMarkup)
			if mc and mc.available():
				self.defaultMarkup = mc
		if len(availableMarkups) > 1:
			self.chooseGroup = QActionGroup(self)
			markupActions = []
			for markup in availableMarkups:
				markupAction = self.act(markup.name, trigbool=self.markupFunction(markup))
				if markup == self.defaultMarkup:
					markupAction.setChecked(True)
				self.chooseGroup.addAction(markupAction)
				markupActions.append(markupAction)
		self.actionBold = self.act(self.tr('Bold'), shct=QKeySequence.Bold,
			trig=lambda: self.insertFormatting('bold'))
		self.actionItalic = self.act(self.tr('Italic'), shct=QKeySequence.Italic,
			trig=lambda: self.insertFormatting('italic'))
		self.actionUnderline = self.act(self.tr('Underline'), shct=QKeySequence.Underline,
			trig=lambda: self.insertFormatting('underline'))
		self.usefulTags = ('header', 'italic', 'bold', 'underline', 'numbering',
			'bullets', 'image', 'link', 'inline code', 'code block', 'blockquote')
		self.usefulChars = ('deg', 'divide', 'dollar', 'hellip', 'laquo', 'larr',
			'lsquo', 'mdash', 'middot', 'minus', 'nbsp', 'ndash', 'raquo',
			'rarr', 'rsquo', 'times')
		self.formattingBox = QComboBox(self.editBar)
		self.formattingBox.addItem(self.tr('Formatting'))
		self.formattingBox.addItems(self.usefulTags)
		self.formattingBox.activated[str].connect(self.insertFormatting)
		self.symbolBox = QComboBox(self.editBar)
		self.symbolBox.addItem(self.tr('Symbols'))
		self.symbolBox.addItems(self.usefulChars)
		self.symbolBox.activated.connect(self.insertSymbol)
		self.updateStyleSheet()
		menubar = self.menuBar()
		menuFile = menubar.addMenu(self.tr('File'))
		menuEdit = menubar.addMenu(self.tr('Edit'))
		menuHelp = menubar.addMenu(self.tr('Help'))
		menuFile.addAction(self.actionNew)
		menuFile.addAction(self.actionOpen)
		self.menuRecentFiles = menuFile.addMenu(self.tr('Open recent'))
		self.menuRecentFiles.aboutToShow.connect(self.updateRecentFiles)
		menuFile.addAction(self.actionShow)
		menuFile.addAction(self.actionSetEncoding)
		menuFile.addAction(self.actionReload)
		menuFile.addSeparator()
		menuFile.addAction(self.actionSave)
		menuFile.addAction(self.actionSaveAs)
		menuFile.addSeparator()
		menuFile.addAction(self.actionNextTab)
		menuFile.addAction(self.actionPrevTab)
		menuFile.addSeparator()
		menuExport = menuFile.addMenu(self.tr('Export'))
		menuExport.addAction(self.actionSaveHtml)
		menuExport.addAction(self.actionOdf)
		menuExport.addAction(self.actionPdf)
		if self.extensionActions:
			menuExport.addSeparator()
			for action, mimetype in self.extensionActions:
				menuExport.addAction(action)
			menuExport.aboutToShow.connect(self.updateExtensionsVisibility)
		menuFile.addAction(self.actionPrint)
		menuFile.addAction(self.actionPrintPreview)
		menuFile.addSeparator()
		menuFile.addAction(self.actionQuit)
		menuEdit.addAction(self.actionUndo)
		menuEdit.addAction(self.actionRedo)
		menuEdit.addSeparator()
		menuEdit.addAction(self.actionCut)
		menuEdit.addAction(self.actionCopy)
		menuEdit.addAction(self.actionPaste)
		menuEdit.addSeparator()
		if enchant_available:
			menuSC = menuEdit.addMenu(self.tr('Spell check'))
			menuSC.addAction(self.actionEnableSC)
			menuSC.addAction(self.actionSetLocale)
		menuEdit.addAction(self.actionSearch)
		menuEdit.addAction(self.actionChangeEditorFont)
		menuEdit.addAction(self.actionChangePreviewFont)
		menuEdit.addSeparator()
		if len(availableMarkups) > 1:
			self.menuMode = menuEdit.addMenu(self.tr('Default markup'))
			for markupAction in markupActions:
				self.menuMode.addAction(markupAction)
		menuFormat = menuEdit.addMenu(self.tr('Formatting'))
		menuFormat.addAction(self.actionBold)
		menuFormat.addAction(self.actionItalic)
		menuFormat.addAction(self.actionUnderline)
		menuEdit.addAction(self.actionWebKit)
		menuEdit.addSeparator()
		menuEdit.addAction(self.actionViewHtml)
		menuEdit.addAction(self.actionPreview)
		menuEdit.addAction(self.actionTableMode)
		if ReTextFakeVimHandler:
			menuEdit.addAction(self.actionFakeVimMode)
		menuEdit.addSeparator()
		menuEdit.addAction(self.actionFullScreen)
		menuEdit.addAction(self.actionConfig)
		menuHelp.addAction(self.actionHelp)
		menuHelp.addSeparator()
		menuHelp.addAction(self.actionAbout)
		menuHelp.addAction(self.actionAboutQt)
		toolBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
		toolBar.addAction(self.actionNew)
		toolBar.addSeparator()
		toolBar.addAction(self.actionOpen)
		toolBar.addAction(self.actionSave)
		toolBar.addAction(self.actionPrint)
		toolBar.addSeparator()
		toolBar.addAction(self.actionPreview)
		toolBar.addAction(self.actionFullScreen)
		self.editBar.addAction(self.actionUndo)
		self.editBar.addAction(self.actionRedo)
		self.editBar.addSeparator()
		self.editBar.addAction(self.actionCut)
		self.editBar.addAction(self.actionCopy)
		self.editBar.addAction(self.actionPaste)
		self.editBar.addSeparator()
		self.editBar.addWidget(self.formattingBox)
		self.editBar.addWidget(self.symbolBox)
		self.searchEdit = QLineEdit(self.searchBar)
		self.searchEdit.setPlaceholderText(self.tr('Search'))
		self.searchEdit.returnPressed.connect(self.find)
		self.csBox = QCheckBox(self.tr('Case sensitively'), self.searchBar)
		self.searchBar.addWidget(self.searchEdit)
		self.searchBar.addSeparator()
		self.searchBar.addWidget(self.csBox)
		self.searchBar.addAction(self.actionFindPrev)
		self.searchBar.addAction(self.actionFind)
		self.searchBar.addAction(self.actionCloseSearch)
		self.searchBar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
		self.searchBar.setVisible(False)
		self.autoSaveEnabled = globalSettings.autoSave
		if self.autoSaveEnabled:
			timer = QTimer(self)
			timer.start(60000)
			timer.timeout.connect(self.saveAll)
		self.ind = None
		if enchant_available:
			self.sl = globalSettings.spellCheckLocale
			if self.sl:
				try:
					enchant.Dict(self.sl)
				except Exception as e:
					print(e, file=sys.stderr)
					self.sl = None
			if globalSettings.spellCheck:
				self.actionEnableSC.setChecked(True)
		self.fileSystemWatcher = QFileSystemWatcher()
		self.fileSystemWatcher.fileChanged.connect(self.fileChanged)

	def iterateTabs(self):
		for i in range(self.tabWidget.count()):
			yield self.tabWidget.widget(i).tab

	def updateStyleSheet(self):
		if globalSettings.styleSheet:
			sheetfile = QFile(globalSettings.styleSheet)
			sheetfile.open(QIODevice.ReadOnly)
			self.ss = QTextStream(sheetfile).readAll()
			sheetfile.close()
		else:
			palette = QApplication.palette()
			self.ss = 'html { color: %s; }\n' % palette.color(QPalette.WindowText).name()
			self.ss += 'td, th { border: 1px solid #c3c3c3; padding: 0 3px 0 3px; }\n'
			self.ss += 'table { border-collapse: collapse; }\n'

	def initTabWidget(self):
		def dragEnterEvent(e):
			e.acceptProposedAction()
		def dropEvent(e):
			fn = bytes(e.mimeData().data('text/plain')).decode().rstrip()
			if fn.startswith('file:'):
				fn = QUrl(fn).toLocalFile()
			self.openFileWrapper(fn)
		self.tabWidget.setTabsClosable(True)
		self.tabWidget.setAcceptDrops(True)
		self.tabWidget.setMovable(True)
		self.tabWidget.dragEnterEvent = dragEnterEvent
		self.tabWidget.dropEvent = dropEvent

	def act(self, name, icon=None, trig=None, trigbool=None, shct=None):
		if not isinstance(shct, QKeySequence):
			shct = QKeySequence(shct)
		if icon:
			action = QAction(self.actIcon(icon), name, self)
		else:
			action = QAction(name, self)
		if trig:
			action.triggered.connect(trig)
		elif trigbool:
			action.setCheckable(True)
			action.triggered[bool].connect(trigbool)
		if shct:
			action.setShortcut(shct)
		return action

	def actIcon(self, name):
		return QIcon.fromTheme(name, QIcon(icon_path+name+'.png'))

	def printError(self):
		import traceback
		print('Exception occured while parsing document:', file=sys.stderr)
		traceback.print_exc()

	def createTab(self, fileName):
		self.currentTab = ReTextTab(self, fileName,
			previewState=int(globalSettings.livePreviewByDefault))
		self.tabWidget.addTab(self.currentTab.getSplitter(), self.tr("New document"))

	def closeTab(self, ind):
		if self.maybeSave(ind):
			if self.tabWidget.count() == 1:
				self.createTab("")
			currentWidget = self.tabWidget.widget(ind)
			if currentWidget.tab.fileName:
				self.fileSystemWatcher.removePath(currentWidget.tab.fileName)
			del currentWidget.tab
			self.tabWidget.removeTab(ind)

	def docTypeChanged(self):
		markupClass = self.currentTab.getMarkupClass()
		if type(self.currentTab.markup) != markupClass:
			self.currentTab.setMarkupClass(markupClass)
			self.currentTab.updatePreviewBox()
		dtMarkdown = (markupClass == markups.MarkdownMarkup)
		dtMkdOrReST = dtMarkdown or (markupClass == markups.ReStructuredTextMarkup)
		self.formattingBox.setEnabled(dtMarkdown)
		self.symbolBox.setEnabled(dtMarkdown)
		self.actionUnderline.setEnabled(dtMarkdown)
		self.actionBold.setEnabled(dtMkdOrReST)
		self.actionItalic.setEnabled(dtMkdOrReST)
		canReload = bool(self.currentTab.fileName) and not self.autoSaveActive()
		self.actionSetEncoding.setEnabled(canReload)
		self.actionReload.setEnabled(canReload)

	def changeIndex(self, ind):
		self.currentTab = self.tabWidget.currentWidget().tab
		editBox = self.currentTab.editBox
		previewState = self.currentTab.previewState
		self.actionUndo.setEnabled(editBox.document().isUndoAvailable())
		self.actionRedo.setEnabled(editBox.document().isRedoAvailable())
		self.actionCopy.setEnabled(editBox.textCursor().hasSelection())
		self.actionCut.setEnabled(editBox.textCursor().hasSelection())
		self.actionPreview.setChecked(previewState >= PreviewLive)
		self.actionLivePreview.setChecked(previewState == PreviewLive)
		self.actionTableMode.setChecked(editBox.tableModeEnabled)
		self.editBar.setEnabled(previewState < PreviewNormal)
		self.ind = ind
		if self.currentTab.fileName:
			self.setCurrentFile()
		else:
			self.setWindowTitle(self.tr('New document') + '[*]')
			self.docTypeChanged()
		self.modificationChanged(editBox.document().isModified())
		editBox.setFocus(Qt.OtherFocusReason)

	def changeEditorFont(self):
		font, ok = QFontDialog.getFont(globalSettings.editorFont, self)
		if ok:
			globalSettings.editorFont = font
			for tab in self.iterateTabs():
				tab.editBox.updateFont()

	def changePreviewFont(self):
		font, ok = QFontDialog.getFont(globalSettings.font, self)
		if ok:
			globalSettings.font = font
			for tab in self.iterateTabs():
				tab.updatePreviewBox()

	def preview(self, viewmode):
		self.currentTab.previewState = viewmode * 2
		self.actionLivePreview.setChecked(False)
		self.editBar.setDisabled(viewmode)
		self.currentTab.updateBoxesVisibility()
		if viewmode:
			self.currentTab.updatePreviewBox()

	def enableLivePreview(self, livemode):
		self.currentTab.previewState = int(livemode)
		self.actionPreview.setChecked(livemode)
		self.editBar.setEnabled(True)
		self.currentTab.updateBoxesVisibility()
		if livemode:
			self.currentTab.updatePreviewBox()

	def enableWebKit(self, enable):
		globalSettings.useWebKit = enable
		for i in range(self.tabWidget.count()):
			splitter = self.tabWidget.widget(i)
			tab = splitter.tab
			tab.previewBox.disconnectExternalSignals()
			tab.previewBox.setParent(None)
			tab.previewBox.deleteLater()
			tab.previewBox = tab.createPreviewBox(tab.editBox)
			tab.previewBox.setMinimumWidth(125)
			splitter.addWidget(tab.previewBox)
			splitter.setSizes((50, 50))
			tab.updatePreviewBox()
			tab.updateBoxesVisibility()

	def enableCopy(self, copymode):
		self.actionCopy.setEnabled(copymode)
		self.actionCut.setEnabled(copymode)

	def enableFullScreen(self, yes):
		if yes:
			self.showFullScreen()
		else:
			self.showNormal()

	def openConfigDialog(self):
		dlg = ConfigDialog(self)
		dlg.setWindowTitle(self.tr('Preferences'))
		dlg.show()

	def enableFakeVimMode(self, yes):
		globalSettings.useFakeVim = yes
		if yes:
			FakeVimMode.init(self)
			for tab in self.iterateTabs():
				tab.installFakeVimHandler()
		else:
			FakeVimMode.exit(self)

	def enableSpellCheck(self, yes):
		if yes:
			self.setAllDictionaries(enchant.Dict(self.sl or None))
		else:
			self.setAllDictionaries(None)
		globalSettings.spellCheck = yes

	def setAllDictionaries(self, dictionary):
		for tab in self.iterateTabs():
			hl = tab.highlighter
			hl.dictionary = dictionary
			hl.rehighlight()

	def changeLocale(self):
		if self.sl:
			localedlg = LocaleDialog(self, defaultText=self.sl)
		else:
			localedlg = LocaleDialog(self)
		if localedlg.exec() != QDialog.Accepted:
			return
		sl = localedlg.localeEdit.text()
		setdefault = localedlg.checkBox.isChecked()
		if sl:
			try:
				sl = str(sl)
				enchant.Dict(sl)
			except Exception as e:
				QMessageBox.warning(self, '', str(e))
			else:
				self.sl = sl
				self.enableSpellCheck(self.actionEnableSC.isChecked())
		else:
			self.sl = None
			self.enableSpellCheck(self.actionEnableSC.isChecked())
		if setdefault:
			globalSettings.spellCheckLocale = sl

	def searchBarVisibilityChanged(self, visible):
		self.actionSearch.setChecked(visible)
		if visible:
			self.searchEdit.setFocus(Qt.ShortcutFocusReason)

	def find(self, back=False):
		flags = QTextDocument.FindFlags()
		if back:
			flags |= QTextDocument.FindBackward
		if self.csBox.isChecked():
			flags |= QTextDocument.FindCaseSensitively
		text = self.searchEdit.text()
		editBox = self.currentTab.editBox
		cursor = editBox.textCursor()
		newCursor = editBox.document().find(text, cursor, flags)
		if not newCursor.isNull():
			editBox.setTextCursor(newCursor)
			return self.setSearchEditColor(True)
		cursor.movePosition(QTextCursor.End if back else QTextCursor.Start)
		newCursor = editBox.document().find(text, cursor, flags)
		if not newCursor.isNull():
			editBox.setTextCursor(newCursor)
			return self.setSearchEditColor(True)
		self.setSearchEditColor(False)

	def setSearchEditColor(self, found):
		palette = self.searchEdit.palette()
		palette.setColor(QPalette.Active, QPalette.Base,
		                 Qt.white if found else QColor(255, 102, 102))
		self.searchEdit.setPalette(palette)

	def showInDir(self):
		if self.currentTab.fileName:
			path = QFileInfo(self.currentTab.fileName).path()
			QDesktopServices.openUrl(QUrl.fromLocalFile(path))
		else:
			QMessageBox.warning(self, '', self.tr("Please, save the file somewhere."))

	def setCurrentFile(self):
		self.setWindowTitle("")
		self.tabWidget.setTabText(self.ind, self.currentTab.getDocumentTitle(baseName=True))
		self.tabWidget.setTabToolTip(self.ind, self.currentTab.fileName or '')
		self.setWindowFilePath(self.currentTab.fileName)
		files = readListFromSettings("recentFileList")
		while self.currentTab.fileName in files:
			files.remove(self.currentTab.fileName)
		files.insert(0, self.currentTab.fileName)
		if len(files) > 10:
			del files[10:]
		writeListToSettings("recentFileList", files)
		QDir.setCurrent(QFileInfo(self.currentTab.fileName).dir().path())
		self.docTypeChanged()

	def createNew(self, text=None):
		self.createTab("")
		self.ind = self.tabWidget.count()-1
		self.tabWidget.setCurrentIndex(self.ind)
		if text:
			self.currentTab.editBox.textCursor().insertText(text)

	def switchTab(self, shift=1):
		self.tabWidget.setCurrentIndex((self.ind + shift) % self.tabWidget.count())

	def updateRecentFiles(self):
		self.menuRecentFiles.clear()
		self.recentFilesActions = []
		filesOld = readListFromSettings("recentFileList")
		files = []
		for f in filesOld:
			if QFile.exists(f):
				files.append(f)
				self.recentFilesActions.append(self.act(f, trig=self.openFunction(f)))
		writeListToSettings("recentFileList", files)
		for action in self.recentFilesActions:
			self.menuRecentFiles.addAction(action)

	def markupFunction(self, markup):
		return lambda: self.setDefaultMarkup(markup)

	def openFunction(self, fileName):
		return lambda: self.openFileWrapper(fileName)

	def extensionFunction(self, data):
		return lambda: \
		self.runExtensionCommand(data['Exec'], data['FileFilter'], data['DefaultExtension'])

	def getExportExtensionsList(self):
		extensions = []
		for extsprefix in datadirs:
			extsdir = QDir(extsprefix+'/export-extensions/')
			if extsdir.exists():
				for fileInfo in extsdir.entryInfoList(['*.desktop', '*.ini'],
				QDir.Files | QDir.Readable):
					extensions.append(self.readExtension(fileInfo.filePath()))
		locale = QLocale.system().name()
		self.extensionActions = []
		for extension in extensions:
			try:
				if ('Name[%s]' % locale) in extension:
					name = extension['Name[%s]' % locale]
				elif ('Name[%s]' % locale.split('_')[0]) in extension:
					name = extension['Name[%s]' % locale.split('_')[0]]
				else:
					name = extension['Name']
				data = {}
				for prop in ('FileFilter', 'DefaultExtension', 'Exec'):
					if 'X-ReText-'+prop in extension:
						data[prop] = extension['X-ReText-'+prop]
					elif prop in extension:
						data[prop] = extension[prop]
					else:
						data[prop] = ''
				action = self.act(name, trig=self.extensionFunction(data))
				if 'Icon' in extension:
					action.setIcon(self.actIcon(extension['Icon']))
				mimetype = extension['MimeType'] if 'MimeType' in extension else None
			except KeyError:
				print('Failed to parse extension: Name is required', file=sys.stderr)
			else:
				self.extensionActions.append((action, mimetype))

	def updateExtensionsVisibility(self):
		markupClass = self.currentTab.getMarkupClass()
		for action in self.extensionActions:
			if markupClass is None:
				action[0].setEnabled(False)
				continue
			mimetype = action[1]
			if mimetype == None:
				enabled = True
			elif markupClass == markups.MarkdownMarkup:
				enabled = (mimetype in ("text/x-retext-markdown", "text/x-markdown"))
			elif markupClass == markups.ReStructuredTextMarkup:
				enabled = (mimetype in ("text/x-retext-rst", "text/x-rst"))
			else:
				enabled = False
			action[0].setEnabled(enabled)

	def readExtension(self, fileName):
		extFile = QFile(fileName)
		extFile.open(QIODevice.ReadOnly)
		extension = {}
		stream = QTextStream(extFile)
		while not stream.atEnd():
			line = stream.readLine()
			if '=' in line:
				index = line.index('=')
				extension[line[:index].rstrip()] = line[index+1:].lstrip()
		extFile.close()
		return extension

	def openFile(self):
		supportedExtensions = ['.txt']
		for markup in markups.get_all_markups():
			supportedExtensions += markup.file_extensions
		fileFilter = ' (' + str.join(' ', ['*'+ext for ext in supportedExtensions]) + ');;'
		fileNames = QFileDialog.getOpenFileNames(self,
			self.tr("Select one or several files to open"), "",
			self.tr("Supported files") + fileFilter + self.tr("All files (*)"))
		for fileName in fileNames[0]:
			self.openFileWrapper(fileName)

	def openFileWrapper(self, fileName):
		if not fileName:
			return
		fileName = QFileInfo(fileName).canonicalFilePath()
		exists = False
		for i, tab in enumerate(self.iterateTabs()):
			if tab.fileName == fileName:
				exists = True
				ex = i
		if exists:
			self.tabWidget.setCurrentIndex(ex)
		elif QFile.exists(fileName):
			noEmptyTab = (
				(self.ind is None) or
				self.currentTab.fileName or
				self.currentTab.editBox.toPlainText() or
				self.currentTab.editBox.document().isModified()
			)
			if noEmptyTab:
				self.createTab(fileName)
				self.ind = self.tabWidget.count()-1
				self.tabWidget.setCurrentIndex(self.ind)
			if fileName:
				self.fileSystemWatcher.addPath(fileName)
			self.currentTab.fileName = fileName
			self.currentTab.readTextFromFile()
			editBox = self.currentTab.editBox
			self.setCurrentFile()
			self.setWindowModified(editBox.document().isModified())

	def showEncodingDialog(self):
		if not self.maybeSave(self.ind):
			return
		encoding, ok = QInputDialog.getItem(self, '',
			self.tr('Select file encoding from the list:'),
			[bytes(b).decode() for b in QTextCodec.availableCodecs()],
			0, False)
		if ok:
			self.currentTab.readTextFromFile(encoding)

	def saveFile(self):
		self.saveFileMain(dlg=False)

	def saveFileAs(self):
		self.saveFileMain(dlg=True)

	def saveAll(self):
		for tab in self.iterateTabs():
			if tab.fileName and QFileInfo(tab.fileName).isWritable():
				tab.saveTextToFile()
				tab.editBox.document().setModified(False)

	def saveFileMain(self, dlg):
		if (not self.currentTab.fileName) or dlg:
			markupClass = self.currentTab.getMarkupClass()
			if (markupClass is None) or not hasattr(markupClass, 'default_extension'):
				defaultExt = self.tr("Plain text (*.txt)")
				ext = ".txt"
			else:
				defaultExt = self.tr('%s files',
					'Example of final string: Markdown files') \
					% markupClass.name + ' (' + str.join(' ',
					('*'+extension for extension in markupClass.file_extensions)) + ')'
				if markupClass == markups.MarkdownMarkup:
					ext = globalSettings.markdownDefaultFileExtension
				elif markupClass == markups.ReStructuredTextMarkup:
					ext = globalSettings.restDefaultFileExtension
				else:
					ext = markupClass.default_extension
			newFileName = QFileDialog.getSaveFileName(self,
				self.tr("Save file"), "", defaultExt)[0]
			if newFileName:
				if not QFileInfo(newFileName).suffix():
					newFileName += ext
				if self.currentTab.fileName:
					self.fileSystemWatcher.removePath(self.currentTab.fileName)
				self.currentTab.fileName = newFileName
				self.actionSetEncoding.setDisabled(self.autoSaveActive())
		if self.currentTab.fileName:
			if self.currentTab.saveTextToFile():
				self.setCurrentFile()
				self.currentTab.editBox.document().setModified(False)
				self.setWindowModified(False)
				return True
			else:
				QMessageBox.warning(self, '',
				self.tr("Cannot save to file because it is read-only!"))
		return False

	def saveHtml(self, fileName):
		if not QFileInfo(fileName).suffix():
			fileName += ".html"
		try:
			htmltext = self.currentTab.getHtml(includeStyleSheet=False,
				webenv=True)
		except Exception:
			return self.printError()
		htmlFile = QFile(fileName)
		htmlFile.open(QIODevice.WriteOnly)
		html = QTextStream(htmlFile)
		if globalSettings.defaultCodec:
			html.setCodec(globalSettings.defaultCodec)
		html << htmltext
		htmlFile.close()

	def textDocument(self):
		td = QTextDocument()
		td.setMetaInformation(QTextDocument.DocumentTitle,
		                      self.currentTab.getDocumentTitle())
		if self.ss:
			td.setDefaultStyleSheet(self.ss)
		td.setHtml(self.currentTab.getHtml())
		td.setDefaultFont(globalSettings.font)
		return td

	def saveOdf(self):
		try:
			document = self.textDocument()
		except Exception:
			return self.printError()
		fileName = QFileDialog.getSaveFileName(self,
			self.tr("Export document to ODT"), "",
			self.tr("OpenDocument text files (*.odt)"))[0]
		if not QFileInfo(fileName).suffix():
			fileName += ".odt"
		writer = QTextDocumentWriter(fileName)
		writer.setFormat(b"odf")
		writer.write(document)

	def saveFileHtml(self):
		fileName = QFileDialog.getSaveFileName(self,
			self.tr("Save file"), "",
			self.tr("HTML files (*.html *.htm)"))[0]
		if fileName:
			self.saveHtml(fileName)

	def getDocumentForPrint(self):
		if globalSettings.useWebKit:
			return self.currentTab.previewBox
		try:
			return self.textDocument()
		except Exception:
			self.printError()

	def standardPrinter(self):
		printer = QPrinter(QPrinter.HighResolution)
		printer.setDocName(self.currentTab.getDocumentTitle())
		printer.setCreator('ReText %s' % app_version)
		return printer

	def savePdf(self):
		self.currentTab.updatePreviewBox()
		fileName = QFileDialog.getSaveFileName(self,
			self.tr("Export document to PDF"),
			"", self.tr("PDF files (*.pdf)"))[0]
		if fileName:
			if not QFileInfo(fileName).suffix():
				fileName += ".pdf"
			printer = self.standardPrinter()
			printer.setOutputFormat(QPrinter.PdfFormat)
			printer.setOutputFileName(fileName)
			document = self.getDocumentForPrint()
			if document != None:
				document.print(printer)

	def printFile(self):
		self.currentTab.updatePreviewBox()
		printer = self.standardPrinter()
		dlg = QPrintDialog(printer, self)
		dlg.setWindowTitle(self.tr("Print document"))
		if (dlg.exec() == QDialog.Accepted):
			document = self.getDocumentForPrint()
			if document != None:
				document.print(printer)

	def printPreview(self):
		document = self.getDocumentForPrint()
		if document == None:
			return
		printer = self.standardPrinter()
		preview = QPrintPreviewDialog(printer, self)
		preview.paintRequested.connect(document.print)
		preview.exec()

	def runExtensionCommand(self, command, filefilter, defaultext):
		of = ('%of' in command)
		html = ('%html' in command)
		if of:
			if defaultext and not filefilter:
				filefilter = '*'+defaultext
			fileName = QFileDialog.getSaveFileName(self,
				self.tr('Export document'), '', filefilter)[0]
			if not fileName:
				return
			if defaultext and not QFileInfo(fileName).suffix():
				fileName += defaultext
		basename = '.%s.retext-temp' % self.currentTab.getDocumentTitle(baseName=True)
		if html:
			tmpname = basename+'.html'
			self.saveHtml(tmpname)
		else:
			tmpname = basename + self.currentTab.getMarkupClass().default_extension
			self.currentTab.saveTextToFile(fileName=tmpname, addToWatcher=False)
		command = command.replace('%of', '"out'+defaultext+'"')
		command = command.replace('%html' if html else '%if', '"'+tmpname+'"')
		try:
			Popen(str(command), shell=True).wait()
		except Exception as error:
			errorstr = str(error)
			QMessageBox.warning(self, '', self.tr('Failed to execute the command:')
			+ '\n' + errorstr)
		QFile(tmpname).remove()
		if of:
			QFile('out'+defaultext).rename(fileName)

	def autoSaveActive(self, ind=None):
		tab = self.currentTab if ind is None else self.tabWidget.widget(ind).tab
		return (self.autoSaveEnabled and tab.fileName and
			QFileInfo(tab.fileName).isWritable())

	def modificationChanged(self, changed):
		if self.autoSaveActive():
			changed = False
		self.actionSave.setEnabled(changed)
		self.setWindowModified(changed)

	def clipboardDataChanged(self):
		mimeData = QApplication.instance().clipboard().mimeData()
		if mimeData is not None:
			self.actionPaste.setEnabled(mimeData.hasText())

	def insertFormatting(self, formatting):
		cursor = self.currentTab.editBox.textCursor()
		text = cursor.selectedText()
		moveCursorTo = None

		def c(cursor):
			nonlocal moveCursorTo
			moveCursorTo = cursor.position()

		def ensurenl(cursor):
			if not cursor.atBlockStart():
				cursor.insertText('\n\n')

		toinsert = {
			'header': (ensurenl, '# ', text),
			'italic': ('*', text, c, '*'),
			'bold': ('**', text, c, '**'),
			'underline': ('<u>', text, c, '</u>'),
			'numbering': (ensurenl, ' 1. ', text),
			'bullets': (ensurenl, '  * ', text),
			'image': ('![', text or self.tr('Alt text'), c, '](', self.tr('URL'), ')'),
			'link': ('[', text or self.tr('Link text'), c, '](', self.tr('URL'), ')'),
			'inline code': ('`', text, c, '`'),
			'code block': (ensurenl, '    ', text),
			'blockquote': (ensurenl, '> ', text),
		}

		if formatting not in toinsert:
			return

		cursor.beginEditBlock()
		for token in toinsert[formatting]:
			if callable(token):
				token(cursor)
			else:
				cursor.insertText(token)
		cursor.endEditBlock()

		self.formattingBox.setCurrentIndex(0)
		# Bring back the focus on the editor
		self.currentTab.editBox.setFocus(Qt.OtherFocusReason)

		if moveCursorTo:
			cursor.setPosition(moveCursorTo)
			self.currentTab.editBox.setTextCursor(cursor)

	def insertSymbol(self, num):
		if num:
			self.currentTab.editBox.insertPlainText('&'+self.usefulChars[num-1]+';')
		self.symbolBox.setCurrentIndex(0)

	def fileChanged(self, fileName):
		ind = None
		for testind, tab in enumerate(self.iterateTabs()):
			if tab.fileName == fileName:
				ind = testind
		if ind is None:
			self.fileSystemWatcher.removePath(fileName)
		self.tabWidget.setCurrentIndex(ind)
		if not QFile.exists(fileName):
			self.currentTab.editBox.document().setModified(True)
			QMessageBox.warning(self, '', self.tr(
				'This file has been deleted by other application.\n'
				'Please make sure you save the file before exit.'))
		elif not self.currentTab.editBox.document().isModified():
			# File was not modified in ReText, reload silently
			self.currentTab.readTextFromFile()
			self.currentTab.updatePreviewBox()
		else:
			text = self.tr(
				'This document has been modified by other application.\n'
				'Do you want to reload the file (this will discard all '
				'your changes)?\n')
			if self.autoSaveEnabled:
				text += self.tr(
					'If you choose to not reload the file, auto save mode will '
					'be disabled for this session to prevent data loss.')
			messageBox = QMessageBox(QMessageBox.Warning, '', text)
			reloadButton = messageBox.addButton(self.tr('Reload'), QMessageBox.YesRole)
			messageBox.addButton(QMessageBox.Cancel)
			messageBox.exec()
			if messageBox.clickedButton() is reloadButton:
				self.currentTab.readTextFromFile()
				self.currentTab.updatePreviewBox()
			else:
				self.autoSaveEnabled = False
				self.currentTab.editBox.document().setModified(True)
		if fileName not in self.fileSystemWatcher.files():
			# https://github.com/retext-project/retext/issues/137
			self.fileSystemWatcher.addPath(fileName)

	def maybeSave(self, ind):
		tab = self.tabWidget.widget(ind).tab
		if self.autoSaveActive(ind):
			tab.saveTextToFile()
			return True
		if not tab.editBox.document().isModified():
			return True
		self.tabWidget.setCurrentIndex(ind)
		ret = QMessageBox.warning(self, '',
			self.tr("The document has been modified.\nDo you want to save your changes?"),
			QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel)
		if ret == QMessageBox.Save:
			return self.saveFileMain(False)
		elif ret == QMessageBox.Cancel:
			return False
		return True

	def closeEvent(self, closeevent):
		for ind in range(self.tabWidget.count()):
			if not self.maybeSave(ind):
				return closeevent.ignore()
		if globalSettings.saveWindowGeometry and not self.isMaximized():
			globalSettings.windowGeometry = self.saveGeometry()
		closeevent.accept()

	def viewHtml(self):
		htmlDlg = HtmlDialog(self)
		try:
			htmltext = self.currentTab.getHtml(includeStyleSheet=False)
		except Exception:
			return self.printError()
		winTitle = self.currentTab.getDocumentTitle(baseName=True)
		htmlDlg.setWindowTitle(winTitle+" ("+self.tr("HTML code")+")")
		htmlDlg.textEdit.setPlainText(htmltext.rstrip())
		htmlDlg.hl.rehighlight()
		htmlDlg.show()
		htmlDlg.raise_()
		htmlDlg.activateWindow()

	def openHelp(self):
		QDesktopServices.openUrl(QUrl('https://github.com/retext-project/retext/wiki'))

	def aboutDialog(self):
		QMessageBox.about(self, self.aboutWindowTitle,
		'<p><b>' + (self.tr('ReText %s (using PyMarkups %s)') % (app_version, markups.__version__))
		+'</b></p>' + self.tr('Simple but powerful editor'
		' for Markdown and reStructuredText')
		+'</p><p>'+self.tr('Author: Dmitry Shachnev, 2011').replace('2011', '2011–2016')
		+'<br><a href="https://github.com/retext-project/retext">'+self.tr('Website')
		+'</a> | <a href="http://daringfireball.net/projects/markdown/syntax">'
		+self.tr('Markdown syntax')
		+'</a> | <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">'
		+self.tr('reStructuredText syntax')+'</a></p>')

	def setDefaultMarkup(self, markupClass):
		self.defaultMarkup = markupClass
		defaultName = markups.get_available_markups()[0].name
		writeToSettings('defaultMarkup', markupClass.name, defaultName)
		for tab in self.iterateTabs():
			if not tab.fileName:
				tab.setMarkupClass(markupClass)
				tab.updatePreviewBox()
		self.docTypeChanged()
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 #41
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")
Exemple #42
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 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 #44
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