示例#1
0
文件: gui.py 项目: hallowslab/STPDF
class MainWindow(QMainWindow):
    def __init__(self, parent):
        super(MainWindow, self).__init__()
        # App required parameters
        self.parent = parent
        self.stop_thread = False
        self.converter_thread = None
        self.title = 'STPDF'
        self.icon = QIcon('stpdf.ico')
        self.user_themes = {"default": "default"}
        self.settings = None
        self.user_values = None
        self.time_stopper = None
        self.converter = None
        self.retries = 0
        self.files_location = ""
        self.files_destination = ""
        self.app_lang = "en"
        self.has_loaded_once = False
        self.log_levels = ["debug", "info", "warning", "error", "critical"]
        self.is_running = False
        self.settings_window = None
        self.about_window = None
        self.app_pallete = None
        self.has_tesseract = self.check_tesseract()
        self.available_langs = ["pt", "en", "es"]
        # Console logger
        # logger is initialized in load_settings
        self.logger = None
        self.init_ui()

    # Building the menubar and actions
    def build_menu(self):
        # Menus ---------------------
        # Main menu bar ---------------------
        main_menu = self.menuBar()
        # Save
        menu_save = QAction(_("Save"), self)
        menu_save.setShortcut("Ctrl+S")
        menu_save.setStatusTip(_("Save your settings to a file"))
        menu_save.triggered.connect(self.do_save)

        # TODO: Store this in values and set them accordingly
        # Processing options
        self.menu_copy = QAction(_("Save images"), self, checkable=True)
        self.menu_copy.setStatusTip(_("Copies files to destination"))
        self.menu_copy.triggered.connect(lambda: self.on_menu_action("d_copy"))
        self.menu_pdf = QAction(_("Make pdf"), self, checkable=True)
        self.menu_pdf.setStatusTip(_("Makes PDF(s) with the images found"))
        self.menu_pdf.triggered.connect(
            lambda: self.on_menu_action("make_pdf"))

        # Exit
        menu_exit = QAction(_("Exit"), self)
        menu_exit.setShortcut("Ctrl+Q")
        menu_exit.setStatusTip(_("Leave The App"))
        menu_exit.triggered.connect(qApp.quit)

        # Help link
        self.menu_help = QAction(_("Help"), self)
        self.menu_help.setStatusTip(_("Open the documentation in a browser"))
        self.menu_help.triggered.connect(lambda: QDesktopServices.openUrl(
            QUrl("https://github.com/hallowf/STPDF")))

        # About link
        self.menu_about = QAction(_("About STPDF"), self)
        self.menu_about.setStatusTip(_("Open info page"))
        self.menu_about.triggered.connect(lambda: self.on_menu_action("about"))
        # SettingsWindow
        self.menu_settings = QAction(_("Settings"), self)
        self.menu_settings.setShortcut("Ctrl+O")
        self.menu_settings.setStatusTip(_("Open settings window"))
        self.menu_settings.triggered.connect(
            lambda: self.on_menu_action("settings"))

        # load the values since the menu is the last thing build
        app_menu = main_menu.addMenu(_("App"))
        app_menu.addAction(menu_save)
        app_menu.addSeparator()
        app_menu.addAction(self.menu_copy)
        app_menu.addAction(self.menu_pdf)
        app_menu.addSeparator()
        app_menu.addAction(menu_exit)
        options_menu = main_menu.addMenu(_("Options"))
        options_menu.addAction(self.menu_help)
        options_menu.addAction(self.menu_about)
        options_menu.addAction(self.menu_settings)

    # Initializes the user interface after loading settings , values and building the menu
    def init_ui(self):
        self.load_settings()
        # Main window
        window = QWidget(self)
        self.setCentralWidget(window)
        self.status_bar = self.statusBar()
        self.setWindowTitle(self.title)
        self.setWindowIcon(self.icon)
        # Buttons / sliders / checkboxes --------------
        self.source_button = QPushButton(_("Search"))
        self.source_button.clicked.connect(lambda: self.open_dir_dlg("source"))
        self.source_button.setStatusTip(_("Location of the files"))
        self.dest_button = QPushButton(_("Search"))
        self.dest_button.clicked.connect(lambda: self.open_dir_dlg("dest"))
        self.dest_button.setStatusTip(_("Destination of files created"))
        self.do_split = QCheckBox()
        self.do_split.setStatusTip(_("Split into multiple PDF files"))
        self.deskew_check = QCheckBox()
        if not self.has_tesseract:
            self.deskew_check.setEnabled(False)
            m = _("The app failed to find tesseract on your system")
            self.deskew_check.setStatusTip(m)
        else:
            self.deskew_check.setStatusTip(_("Removes rotation"))
        self.split_slider = TipSlider(Qt.Horizontal, parent=self)
        self.split_slider.setFocusPolicy(Qt.StrongFocus)
        self.split_slider.setTickPosition(QSlider.TicksBothSides)
        self.run_button = QPushButton(_("Run"))
        self.run_button.setStatusTip(_("Run the program"))
        self.run_button.clicked.connect(self.do_run)
        self.stop_button = QPushButton(_("Stop"))
        self.stop_button.setStatusTip(_("Stops the program"))
        self.stop_button.clicked.connect(self.do_stop)
        self.show_vals = QPushButton(_("Show values"))
        self.show_vals.clicked.connect(self.show_values)
        self.show_vals.setStatusTip(_("Show your current input values"))
        self.clean_button = QPushButton(_("Clean log"))
        self.clean_button.clicked.connect(self.clean_gui_logger)
        self.clean_button.setStatusTip(_("Cleans the current log"))
        # TODO: This options must be changeable in the settings
        self.split_slider.setTickInterval(10)
        self.split_slider.setSingleStep(1)
        # GUI logger
        self.gui_logger = QTextEdit(self)
        self.gui_logger.resize(150, 300)
        self.gui_logger.setReadOnly(True)
        # Layouts ---------------------
        # Main vertical box
        v_box = QVBoxLayout()
        # Center horizontal box
        ch_box = QHBoxLayout()
        # Left grid
        lh_grid = QGridLayout()
        lh_grid.setSpacing(15)
        lh_grid.addWidget(QLabel(_("Source:")), 1, 0)
        lh_grid.addWidget(self.source_button, 1, 1)
        lh_grid.addWidget(QLabel(_("Destination:")), 2, 0)
        lh_grid.addWidget(self.dest_button, 2, 1)
        lh_grid.addWidget(QLabel(_("Deskew:")), 3, 0)
        lh_grid.addWidget(self.deskew_check, 3, 1)
        lh_grid.addWidget(QLabel(_("Split scans:")), 4, 0)
        lh_grid.addWidget(self.do_split, 4, 1)
        # lh_grid.addWidget(self.split_slider_label,4,1)
        lh_grid.addWidget(QLabel(_("Split at:")), 5, 0)
        lh_grid.addWidget(self.split_slider, 5, 1)
        lh_grid.addWidget(self.show_vals, 6, 0)
        lh_grid.addWidget(self.clean_button, 6, 1)
        lh_grid.addWidget(self.run_button, 7, 0)
        lh_grid.addWidget(self.stop_button, 7, 1)
        # Right Box
        rh_box = QHBoxLayout()
        rh_box.addWidget(self.gui_logger)
        # set layouts and Geometry
        ch_box.addLayout(lh_grid, 1)
        ch_box.addLayout(rh_box, 2)
        v_box.addLayout(ch_box, 3)
        window.setLayout(v_box)
        # x: screen x pos, y: screen y pos, width, height
        self.setGeometry(600, 600, 700, 300)
        self.setFixedSize(600, 300)
        self.build_menu()
        self.load_values()
        self.load_theme(self.settings["app_theme"])
        self.show()

    # directory: "source" or "dest"
    def open_dir_dlg(self, directory):
        if directory == "source":
            selected_dir = QFileDialog.getExistingDirectory(
                self, "Source", ".", QFileDialog.ShowDirsOnly)
            self.files_location = selected_dir
            self.user_values["source"] = self.files_location
        elif directory == "dest":
            selected_dir = QFileDialog.getExistingDirectory(
                self, "Destination", ".", QFileDialog.ShowDirsOnly)
            self.files_destination = selected_dir
            self.user_values["dest"] = self.files_destination
        else:
            self.logger.error("%s %s" %
                              (_("Invalid directory parameter"), directory))

    # Main function that loads the theme provided in settings
    def load_theme(self, theme="default"):
        has_themes = self.load_custom_themes()
        if theme == "default":
            self.logger.debug(_("Theme is default"))
        elif theme == "STPDF-dark" or theme == "STPDF-cmder":
            if has_themes:
                dmsg = "%s: %s" % (_("Changing theme to"), theme)
                self.logger.debug(dmsg)
                theme = os.path.abspath("themes/%s.ini" % theme)
                self.theme_config = configparser.ConfigParser()
                self.theme_config.read(theme)
                self.set_theme()
            else:
                self.logger.error(_("Missing themes folder"))
        else:
            theme_keys = list(self.user_themes.keys())
            if len(self.user_themes) >= 4 and theme in theme_keys:
                theme = self.user_themes[theme]
                self.theme_config = configparser.ConfigParser()
                self.theme_config.read(theme)
                self.set_theme()
            else:
                self.logger.warning("%s: %s" % (_("Invalid theme"), theme))

    # Iterates trough the themes folder and maps all themes
    def load_custom_themes(self):
        if os.path.isdir("themes"):
            for root, __, files in os.walk("themes"):
                for file in files:
                    if file.endswith(".ini"):
                        source_path = os.path.join(root, file)
                        theme = file.replace(".ini", "")
                        if theme == "default":
                            self.logger.error(
                                _("Found a default theme in themes folder, but the default theme is already defined"
                                  ))
                        else:
                            self.user_themes[theme] = source_path
            return True
        else:
            return False

    # Switches colors based on the values provided by the theme
    def set_theme(self):
        self.logger.debug(_("Trying to set theme"))
        allowed = ["lighter", "darker"]
        if False not in self.verify_theme():
            self.logger.debug(_("Successfully verified theme"))
            tv = self.theme_values
            tve = self.theme_values_extras
            qplt = QtGui.QPalette()
            color = QtGui.QColor(*tv["window"])
            if tve["window"] in allowed:
                color = getattr(color, tve["window"])()
            qplt.setColor(QtGui.QPalette.Window, color)
            color = QtGui.QColor(*tv["window_text"])
            if tve["window_text"] in allowed:
                color = getattr(color, tve["window_text"])()
            qplt.setColor(QtGui.QPalette.WindowText, color)
            color = QtGui.QColor(*tv["base"])
            if tve["base"] in allowed:
                color = getattr(color, tve["base"])()
            qplt.setColor(QtGui.QPalette.Base, color)
            color = QtGui.QColor(*tv["background"])
            if tve["background"] in allowed:
                color = getattr(color, tve["background"])()
            qplt.setColor(QtGui.QPalette.Background, color)
            color = QtGui.QColor(*tv["foreground"])
            if tve["foreground"] in allowed:
                color = getattr(color, tve["foreground"])()
            qplt.setColor(QtGui.QPalette.Foreground, color)
            color = QtGui.QColor(*tv["alternate_base"])
            if tve["alternate_base"] in allowed:
                color = getattr(color, tve["alternate_base"])()
            qplt.setColor(QtGui.QPalette.AlternateBase, color)
            color = QtGui.QColor(*tv["tooltip_base"])
            if tve["tooltip_base"] in allowed:
                color = getattr(color, tve["tooltip_base"])()
            qplt.setColor(QtGui.QPalette.ToolTipBase, color)
            color = QtGui.QColor(*tv["tooltip_text"])
            if tve["tooltip_text"] in allowed:
                color = getattr(color, tve["tooltip_text"])()
            qplt.setColor(QtGui.QPalette.ToolTipText, color)
            color = QtGui.QColor(*tv["text"])
            if tve["text"] in allowed:
                color = getattr(color, tve["text"])()
            qplt.setColor(QtGui.QPalette.Text, color)
            color = QtGui.QColor(*tv["button"])
            if tve["button"] in allowed:
                color = getattr(color, tve["button"])()
            qplt.setColor(QtGui.QPalette.Button, color)
            color = QtGui.QColor(*tv["button_text"])
            if tve["button_text"] in allowed:
                color = getattr(color, tve["button_text"])()
            qplt.setColor(QtGui.QPalette.ButtonText, color)
            color = QtGui.QColor(*tv["bright_text"])
            if tve["bright_text"] in allowed:
                color = getattr(color, tve["bright_text"])()
            qplt.setColor(QtGui.QPalette.BrightText, color)
            color = QtGui.QColor(*tv["highlight"])
            if tve["highlight"] in allowed:
                color = getattr(color, tve["highlight"])()
            qplt.setColor(QtGui.QPalette.Highlight, color)
            color = QtGui.QColor(*tv["highlighted_text"])
            if tve["highlighted_text"] in allowed:
                color = getattr(color, tve["highlighted_text"])()
            qplt.setColor(QtGui.QPalette.HighlightedText, color)
            self.app_pallete = qplt
            self.parent.setPalette(qplt)
            self.logger.debug(_("Successfully set theme"))
        else:
            self.logger.error(_("Failed to verify theme"))

    # Verifies the theme values - called by set_theme
    def verify_theme(self):
        self.theme_values = {}
        self.theme_values_extras = {}
        known_values = [
            "window", "window_text", "base", "background", "foreground",
            "alternate_base", "tooltip_base", "tooltip_text", "text", "button",
            "button_text", "bright_text", "highlight", "highlighted_text"
        ]
        tc = self.theme_config["THEME"]
        ce = self.theme_config["COLOR_EXTRAS"]
        try:
            for value in tc:
                rgb = tc[value].split(",")
                try:
                    rgb = [int(i) for i in rgb]
                except ValueError:
                    self.logger.error(_("Invalid RGB values in app theme"))
                    yield False
                if len(rgb) != 3:
                    m = _("Invalid theme RGB value")
                    self.logger.error("%s: %s = %s" % (m, value, tc[value]))
                    yield False
                if value in known_values:
                    self.theme_values[value] = rgb
                    extra = ce[value]
                    self.theme_values_extras[value] = extra
        except Exception as e:
            en = e.__class__.__name__
            if en == "KeyError":
                msg = _(
                    "Your selected theme probably has invalid or missing values"
                )
                self.logger.error(msg)
            else:
                self.logger.critical(e)
                yield False
        yield True

    # For opening new windows on menu actions
    def on_menu_action(self, action):
        if action == "settings":
            if self.settings_window is None:
                self.settings_window = SettingsWindow(self)
            else:
                msg = _("Settings are already open")
                self.logger.error(msg)
                self.gui_logger.append(msg)
        elif action == "about":
            if self.about_window is None:
                self.about_window = AboutWindow(self)
            else:
                msg = _("About window already open")
                self.logger.error(msg)
                self.gui_logger.append(msg)
        elif action == "d_copy" or action == "make_pdf":
            map_actions = {
                "d_copy": self.menu_copy.isChecked(),
                "make_pdf": self.menu_pdf.isCheckable()
            }
            self.user_values[action] = map_actions[action]
        else:
            msg = "%s: %s" % (_("Invalid action"), action)
            self.logger.error(msg)
            self.gui_logger.append(msg)

    # Sets up the console logger
    def set_up_logger(self):
        l_level = self.settings["log_level"]
        n_level = getattr(logging, l_level.upper(), 20)
        if n_level == 20 and l_level.upper() != "INFO":
            sys.stdout.write("%s: %s\n" % (_("Invalid log level"), l_level))
        # Console logger
        formatter = logging.Formatter("%(name)s - %(levelname)s: %(message)s")
        self.logger = logging.getLogger("STPDF")
        self.logger.setLevel(n_level)
        ch = logging.StreamHandler()
        ch.setLevel(n_level)
        ch.setFormatter(formatter)
        self.logger.addHandler(ch)
        msg = "%s: %s" % (_("Console logger is set with log level"), l_level)
        self.logger.info(msg)

    # Loads user settings from settings.json
    def load_settings(self):
        if not os.path.isfile("settings.json"):
            self.settings = {
                "keep_vals": False,
                "log_level": "info",
                "app_theme": "default",
                "lang": "en"
            }
            self.set_up_logger()
            json.dump(self.settings, open("settings.json", "w"))
        else:
            self.settings = json.load(open("settings.json", "r"))
            self.set_up_logger()
            try:
                lang = self.settings["lang"]
                if lang in self.available_langs and lang != "en":
                    modl = "%s_gui" % lang
                    # for some reason this seems necessary
                    # when running the app as an executable
                    # after building, ofc this returns the system lang,
                    # so when trying to set another language than your
                    # system's or the default it fails to find the module
                    current_locale, _ = locale.getdefaultlocale()
                    cl = current_locale.split("_")
                    if lang != cl[0]:
                        # however this stupid hack seems to work
                        current_locale = "%s_%s" % (lang, lang.upper())
                    self.app_lang = lang
                    lang = gettext.translation(modl, "locale/",
                                               [current_locale])
                    lang.install()
            except KeyError:
                self.logger.warning(_("Invalid settings found, removing file"))
                os.remove("settings.json")
                if self.retries >= 5:
                    self.logger.error(
                        "Failed to load settings after %s retries" %
                        (self.retries))
                    self.retries = 0
                else:
                    self.load_settings()

    # Loads user values from valuesjson
    def load_values(self):
        self.user_values = {
            "source": "",
            "dest": "",
            "deskew": False,
            "split": False,
            "split_at": 0,
            "d_copy": False,
            "make_pdf": True
        }
        try:
            if self.settings["keep_vals"] or not os.path.isfile("values.json"):
                self.logger.debug(_("Loading user values"))
                if not os.path.isfile("values.json"):
                    m = _("Values file does not exist, creating one now")
                    self.logger.debug(m)
                    json.dump(self.user_values, open("values.json", "w"))
                else:
                    self.user_values = json.load(open("values.json", "r"))
                    if self.user_values:
                        msg = "%s: %s" % (_("Loaded user values"),
                                          self.user_values)
                        self.logger.debug(msg)
                        self.files_location = self.user_values["source"]
                        self.files_destination = self.user_values["dest"]
                        self.deskew_check.setChecked(
                            self.user_values["deskew"])
                        self.do_split.setChecked(self.user_values["split"])
                        self.split_slider.setValue(
                            self.user_values["split_at"])
                        self.menu_copy.setChecked(self.user_values["d_copy"])
                        self.menu_pdf.setChecked(self.user_values["make_pdf"])
                    else:
                        self.logger.error("Failed to load user values: %s" %
                                          self.user_values)
                        raise KeyError
        except KeyError:
            self.logger.warning(_("Invalid values found, removing file"))
            os.remove("values.json")
            if self.retries >= 5:
                self.logger.error("Failed to load values after %s retries" %
                                  (self.retries))
                self.retries = 0
            else:
                self.load_values()

    # clears gui TextEdit that serves as visual logeer
    def clean_gui_logger(self):
        self.gui_logger.setText("")

    # Shows all settings and values in gui_logger
    def show_values(self):
        s = "  %s: %s\n" % (_("Files location"), (self.files_location or ""))
        d = "  %s: %s\n" % (_("Files destination"),
                            (self.files_destination or ""))
        di = "  %s: %s\n" % (_("Deskew"), (self.deskew_check.isChecked()))
        ds = "  %s: %s\n" % (_("Split"), (self.do_split.isChecked()))
        sa = "  %s: %s\n" % (_("Split at"), (self.split_slider.value()))
        dc = "  %s: %s\n" % (_("Save images"), self.menu_copy.isChecked())
        mp = "  %s: %s\n" % (_("Make pdf"), self.menu_pdf.isChecked())
        values = "%s:\n%s%s%s%s%s%s%s" % (_("Values are"), s, d, di, ds, sa,
                                          dc, mp)
        self.gui_logger.append(values)
        self.logger.debug(values)
        # kv = "  Keep values: %s\n" % self.menu_keep.isChecked()
        kv = "  %s: %s\n" % (_("Keep values"), self.settings["keep_vals"])
        t = "  %s: %s\n" % (_("App theme"), self.settings["app_theme"])
        la = "  %s: %s\n" % (_("App language"), self.settings["lang"])
        ll = "  %s: %s\n" % (_("Console log Level"),
                             self.settings["log_level"])
        settings = "%s:\n%s%s%s%s" % (_("Settings are"), kv, t, la, ll)
        self.gui_logger.append(settings)
        self.logger.debug(settings)

    # Checks if it has required input to run
    def verify_required(self):
        fl = self.files_location
        fd = self.files_destination
        ds = self.do_split.isChecked()
        sa = self.split_slider.value()
        if not fl or not fd:
            fl = fl or "None"
            fd = fd or "None"
            return _("Location or destination undefined") + \
                "\n\t%s %s\n\t%s %s" % (_("Source:"),
                                        fl,
                                        _("Destination:"),
                                        fd)
        elif not os.path.isdir(self.files_location):
            return _("Missing files location")
        elif len(os.listdir(self.files_location)) <= 1:
            return _("Location is empty, no files to copy")
        elif ds and sa < 3:
            return _("Minimum split is 3")
        else:
            return True

    # Save settings and values if keep_vals is checked
    def do_save(self):
        msg = _("Saving")
        self.logger.info(msg)
        self.gui_logger.append(msg)
        uv = self.user_values
        s = self.files_location
        d = self.files_destination
        dsk = self.deskew_check.isChecked()
        splt = self.do_split.isChecked()
        splt_a = self.split_slider.value()
        dc = self.menu_copy.isChecked()
        mp = self.menu_pdf.isChecked()
        if uv is not None:
            for val in uv:
                if val == "source" and uv[val] != s:
                    uv[val] = s
                elif val == "dest" and uv[val] != d:
                    uv[val] = d
                elif val == "deskew" and uv[val] != dsk:
                    uv[val] = dsk
                elif val == "split" and uv[val] != splt:
                    uv[val] = splt
                elif val == "split_at" and uv[val] != splt_a:
                    uv[val] = splt_a
                elif val == "d_copy" and uv[val] != dc:
                    uv[val] = dc
                elif val == "make_pdf" and uv[val] != mp:
                    uv[val] = mp
            if self.settings["keep_vals"]:
                self.logger.debug("User values: %s" % uv)
                json.dump(uv, open("values.json", "w"))
            self.logger.debug("Settings: %s" % self.settings)
            json.dump(self.settings, open("settings.json", "w"))
        else:
            self.logger.error(_("Failed to obtain user values"))
            dm1 = _("User values:") + "%s" % self.user_values
            self.logger.debug(dm1)

    # Checks for tesseract in the system
    def check_tesseract(self):
        img = Image.new('RGB', (60, 30), color='red')
        try:
            image_to_string(img)
            return True
        except TesseractNotFoundError:
            return False

    # A function that runs converter in a new thread
    # and appends the yielded string to the loggers
    def do_run(self):
        cvt_args = {}
        cvt_args["fl"] = self.files_location
        cvt_args["fd"] = self.files_destination
        cvt_args["di"] = self.deskew_check.isChecked()
        cvt_args["ds"] = self.do_split.isChecked()
        cvt_args["sa"] = self.split_slider.value()
        cvt_args["pd"] = self.menu_pdf.isChecked()
        cvt_args["dc"] = self.menu_copy.isChecked()
        cvt_args["lang"] = self.app_lang

        def run_in_process(cvt_args):
            print("in process %r" % cvt_args)
            fl = cvt_args["fl"]
            fd = cvt_args["fd"]
            di = cvt_args["di"]
            ds = cvt_args["ds"]
            sa = cvt_args["sa"]
            pd = cvt_args["pd"]
            dc = cvt_args["dc"]
            la = cvt_args["lang"]
            converter = STPDFCore(fl,
                                  fd,
                                  split=(ds, sa),
                                  deskew=di,
                                  lang=la,
                                  make_pdf=pd,
                                  copy_files=dc)
            for line in converter.run_converter():
                print(line)

        Process(target=run_in_process, args=(cvt_args, )).start()

    def handle_thread_exception(self, msg):
        self.gui_logger.append(msg)
        self.logger.error(msg)
        self.do_stop()

    def log_progress(self, p_text):
        self.gui_logger.append(p_text)

    def do_stop(self):
        msg = _("Stop requested")
        self.logger.debug(msg)
        self.gui_logger.append(msg)
        if self.converter.is_running:
            self.logger.debug("Converter running, trying to stop it")
            self.converter.do_stop()
            try:
                self.converter.wait()
            except Exception as e:
                pass
        else:
            msg = "Unable to find converter thread"
            self.gui_logger.append(msg)
            self.logger.error(msg)