"provide methods called from file menu"

import datetime
import fcntl
import glob
import logging
import os
import re
import sys
import tempfile

import config
import gi
from comboboxtext import ComboBoxText
from const import ASTERISK, EMPTY, EMPTY_LIST
from dialog.save import Save as SaveDialog
from helpers import collate_metadata, expand_metadata_pattern
from i18n import _
from print_operation import PrintOperation

gi.require_version("Gtk", "3.0")
from gi.repository import Gio, GLib, Gtk  # pylint: disable=wrong-import-position

logger = logging.getLogger(__name__)


def add_filter(file_chooser, name, file_extensions):
    "Create a file filter to show only supported file types in FileChooser dialog"
    ffilter = Gtk.FileFilter()
    for extension in file_extensions:
        pattern = []

        # Create case insensitive pattern
        for char in extension:
            pattern.append("[" + char.upper() + char.lower() + "]")

        ffilter.add_pattern("*." + EMPTY.join(pattern))

    types = None
    for ext in file_extensions:
        if types is not None:
            types += f", *.{ext}"

        else:
            types = f"*.{ext}"

    ffilter.set_name(f"{name} ({types})")
    file_chooser.add_filter(ffilter)
    ffilter = Gtk.FileFilter()
    ffilter.add_pattern("*")
    ffilter.set_name("All files")
    file_chooser.add_filter(ffilter)


def file_exists(chooser, filename):
    "Check if a file exists and prompt the user for confirmation if it does."

    if os.path.isfile(filename):
        # File exists; get the file chooser to ask the user to confirm.
        chooser.set_filename(filename)

        # Give the name change a chance to take effect
        GLib.idle_add(lambda: chooser.response(Gtk.ResponseType.OK))
        return True

    return False


def launch_default_for_file(filename):
    "Launch default viewer for file"
    uri = GLib.filename_to_uri(os.path.abspath(filename), None)
    logger.info("Opening %s via default launcher", uri)
    context = Gio.AppLaunchContext()
    try:
        Gio.AppInfo.launch_default_for_uri(uri, context)
    except Gio.Error as e:
        logger.error("Unable to launch viewer: %s", e)


class FileMenuMixins:
    "provide methods called from file menu"

    def _chdir_cwd(self):
        """Change directory to self.settings['cwd'],
        falling back to HOME if it doesn't exist"""
        try:
            os.chdir(self.settings["cwd"])
        except FileNotFoundError:
            self.settings["cwd"] = os.path.expanduser("~")
            os.chdir(self.settings["cwd"])

    def new_(self, _action, _param):
        "Deletes all scans after warning"
        if not self._pages_saved(
            _("Some pages have not been saved.\nDo you really want to clear all pages?")
        ):
            return

        # Starting a new file with an empty document is a no-op: don't send a
        # request or create an undo step
        if not self.slist.data:
            return

        # Reset the view immediately; the page list is cleared via the thread
        # so that the deletion is an ordinary undoable step (issue #74)
        self.view.set_pixbuf(None)
        self.t_canvas.clear_text()
        self.a_canvas.clear_text()
        self._current_page = None

        self.slist.delete_all_pages()

    def open_dialog(self, _action, _param):
        "Throw up file selector and open selected file"
        # cd back to cwd to get filename
        self._chdir_cwd()
        file_chooser = Gtk.FileChooserDialog(
            title=_("Open image"),
            parent=self,
            action=Gtk.FileChooserAction.OPEN,
        )
        file_chooser.add_buttons(
            Gtk.STOCK_CANCEL,
            Gtk.ResponseType.CANCEL,
            Gtk.STOCK_OPEN,
            Gtk.ResponseType.OK,
        )
        file_chooser.set_select_multiple(True)
        file_chooser.set_default_response(Gtk.ResponseType.OK)
        file_chooser.set_current_folder(self.settings["cwd"])
        add_filter(
            file_chooser,
            _("Image files"),
            [
                "jpg",
                "png",
                "pnm",
                "ppm",
                "pbm",
                "gif",
                "tif",
                "tiff",
                "pdf",
                "djvu",
                "ps",
                "sdb",
            ],
        )
        if file_chooser.run() == Gtk.ResponseType.OK:
            # cd back to tempdir to import
            os.chdir(self.session.name)

            filenames = file_chooser.get_filenames()
            file_chooser.destroy()

            # Update cwd
            self.settings["cwd"] = os.path.dirname(filenames[0])
            self._import_files(filenames)
        else:
            file_chooser.destroy()

        # cd back to tempdir
        os.chdir(self.session.name)

    def _select_pagerange_callback(self, info):
        dialog = Gtk.Dialog(
            title=_("Pages to extract"),
            transient_for=self,
            modal=True,
            destroy_with_parent=True,
        )
        dialog.add_buttons(
            Gtk.STOCK_OK,
            Gtk.ResponseType.OK,
            Gtk.STOCK_CANCEL,
            Gtk.ResponseType.CANCEL,
        )
        vbox = dialog.get_content_area()
        hbox = Gtk.Box()
        vbox.pack_start(hbox, True, True, 0)
        label = Gtk.Label(label=_("First page to extract"))
        hbox.pack_start(label, False, False, 0)
        spinbuttonf = Gtk.SpinButton.new_with_range(1, info["pages"], 1)
        hbox.pack_end(spinbuttonf, False, False, 0)
        hbox = Gtk.Box()
        vbox.pack_start(hbox, True, True, 0)
        label = Gtk.Label(label=_("Last page to extract"))
        hbox.pack_start(label, False, False, 0)
        spinbuttonl = Gtk.SpinButton.new_with_range(1, info["pages"], 1)
        spinbuttonl.set_value(info["pages"])
        hbox.pack_end(spinbuttonl, False, False, 0)
        dialog.show_all()
        response = dialog.run()
        dialog.destroy()
        if response == Gtk.ResponseType.OK:
            return int(spinbuttonf.get_value()), int(spinbuttonl.get_value())
        return None, None

    def _import_files_password_callback(self, filename):
        "Ask for password for encrypted PDF"
        text = _("Enter user password for PDF %s") % (filename)
        dialog = Gtk.MessageDialog(
            parent=self,
            destroy_with_parent=True,
            modal=True,
            message_type=Gtk.MessageType.QUESTION,
            buttons=Gtk.ButtonsType.OK_CANCEL,
            title=text,
            text=text,
        )
        vbox = dialog.get_content_area()
        entry = Gtk.Entry()
        entry.set_visibility(False)
        entry.set_invisible_char(ASTERISK)
        vbox.pack_end(entry, False, False, 0)
        dialog.show_all()
        response = dialog.run()
        text = entry.get_text()
        dialog.destroy()
        if response == Gtk.ResponseType.OK and text != EMPTY:
            return text
        return None

    def _import_files_finished_callback(self, response):
        "import_files finished callback"
        self.post_process_progress.finish(response)
        # Resume full-resolution loading and show the final imported page.
        self._suppress_full_display = False
        indices = self.slist.get_selected_indices()
        if indices:
            self._display_image(self.slist.data[indices[0]][2])

    def _import_files_metadata_callback(self, metadata):
        "Update the metadata from the imported file"
        logger.debug("import_files_metadata_callback(%s)", metadata)
        for dialog in (self._windowi, self._windowe):
            if dialog is not None:
                dialog.update_from_import_metadata(metadata)
        config.update_config_from_imported_metadata(self.settings, metadata)

    def _import_files(self, filenames, all_pages=False):
        "Import given files"
        # FIXME: import_files() now returns an array of pids.
        # During a bulk import only show thumbnails; the final page is loaded
        # full-res once the import finishes (see _import_files_finished_callback).
        self._suppress_full_display = True
        options = {
            "paths": filenames,
            "password_callback": self._import_files_password_callback,
            "queued_callback": self.post_process_progress.queued,
            "started_callback": self.post_process_progress.update,
            "running_callback": self.post_process_progress.update,
            "finished_callback": self._import_files_finished_callback,
            "metadata_callback": self._import_files_metadata_callback,
            "error_callback": self._error_callback,
        }
        if all_pages:
            options["pagerange_callback"] = lambda info: (1, info["pages"])
        else:
            options["pagerange_callback"] = self._select_pagerange_callback

        self.slist.import_files(**options)

    def _open_session_action(self, _action, _param):
        "open session"
        file_chooser = Gtk.FileChooserDialog(
            title=_("Open crashed session"),
            parent=self,
            action=Gtk.FileChooserAction.SELECT_FOLDER,
        )
        file_chooser.add_buttons(
            Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK
        )
        file_chooser.set_default_response(Gtk.ResponseType.OK)
        file_chooser.set_current_folder(self.settings["cwd"])
        if file_chooser.run() == Gtk.ResponseType.OK:
            filename = file_chooser.get_filenames()
            self._open_session(filename[0])

        file_chooser.destroy()

    def _open_session(self, db):
        "open session"
        logger.info("Restoring session in %s", self.session)
        self.slist.open_session(
            db=db, delete=False, error_callback=self._error_callback
        )

    def save_dialog(self, _action, _param):
        "Display page selector and on save a fileselector."
        if self._windowi is not None:
            self._windowi.present()
            return

        image_types = [
            "pdf",
            "gif",
            "jpg",
            "png",
            "pnm",
            "ps",
            "tif",
            "txt",
            "hocr",
            "sdb",
        ]
        if self._dependencies["pdfunite"]:
            image_types.extend(["prependpdf", "appendpdf"])

        if self._dependencies["djvu"]:
            image_types.append("djvu")
        ps_backends = []
        for backend in ["libtiff", "pdf2ps", "pdftops"]:
            if self._dependencies[backend]:
                ps_backends.append(backend)

        self._windowi = SaveDialog(
            transient_for=self,
            title=_("Save"),
            hide_on_delete=True,
            page_range=self.settings["Page range"],
            include_time=self.settings["use_time"],
            meta_datetime=datetime.datetime.now() + self.settings["datetime offset"],
            select_datetime=bool(self.settings["datetime offset"]),
            meta_title=self.settings["title"],
            meta_title_suggestions=self.settings["title-suggestions"],
            meta_author=self.settings["author"],
            meta_author_suggestions=self.settings["author-suggestions"],
            meta_subject=self.settings["subject"],
            meta_subject_suggestions=self.settings["subject-suggestions"],
            meta_keywords=self.settings["keywords"],
            meta_keywords_suggestions=self.settings["keywords-suggestions"],
            image_types=image_types,
            image_type=self.settings["image type"],
            ps_backends=ps_backends,
            jpeg_quality=self.settings["quality"],
            downsample_dpi=self.settings["downsample dpi"],
            downsample=self.settings["downsample"],
            pdf_compression=self.settings["pdf compression"],
            can_encrypt_pdf="qpdf" in self._dependencies,
            tiff_compression=self.settings["tiff compression"],
        )

        # Frame for page range
        self._windowi.add_page_range()
        self._windowi.add_image_type()

        # Post-save hook
        pshbutton = Gtk.CheckButton(label=_("Post-save hook"))
        pshbutton.set_tooltip_text(
            _(
                "Run command on saved file. The available commands are those "
                "user-defined tools that do not specify %o"
            )
        )
        vbox = self._windowi.get_content_area()
        vbox.pack_start(pshbutton, False, True, 0)
        self._update_post_save_hooks()
        # Wrap combobox in a scrolled window to avoid the dialog becoming
        # excessively wide for users with long tool names.
        sw = Gtk.ScrolledWindow()
        sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.NEVER)
        sw.set_propagate_natural_width(False)
        sw.add(self._windowi.comboboxpsh)
        vbox.pack_start(sw, False, True, 0)
        pshbutton.connect(
            "toggled",
            lambda _action: self._windowi.comboboxpsh.set_sensitive(
                pshbutton.get_active()
            ),
        )
        pshbutton.set_active(self.settings["post_save_hook"])
        self._windowi.comboboxpsh.set_sensitive(pshbutton.get_active())
        kbutton = Gtk.CheckButton(label=_("Close dialog on save"))
        kbutton.set_tooltip_text(_("Close dialog on save"))
        kbutton.set_active(self.settings["close_dialog_on_save"])
        vbox.pack_start(kbutton, False, True, 0)

        self._windowi.add_actions(
            [
                (
                    "gtk-save",
                    lambda: self._save_button_clicked_callback(kbutton, pshbutton),
                ),
                ("gtk-cancel", self._windowi.hide),
            ]
        )
        self._windowi.show_all()
        self._windowi.resize(1, 1)

    def _save_button_clicked_callback(self, kbutton, pshbutton):
        "Save selected pages"

        # Compile list of pages
        self.settings["Page range"] = self._windowi.page_range
        uuids = self._list_of_page_uuids()

        # dig out the image type, compression and quality
        self.settings["image type"] = self._windowi.image_type
        self.settings["close_dialog_on_save"] = kbutton.get_active()
        self.settings["post_save_hook"] = pshbutton.get_active()
        if (
            self.settings["post_save_hook"]
            and self._windowi.comboboxpsh.get_active() > EMPTY_LIST
        ):
            self.settings["current_psh"] = self._windowi.comboboxpsh.get_active_text()

        if re.search(r"pdf", self.settings["image type"]):
            if self.settings["image type"] == "pdf":  # not for pre/append or email
                self._windowi.update_config_dict(self.settings)

            # dig out the compression
            self.settings["downsample"] = self._windowi.downsample
            self.settings["downsample dpi"] = self._windowi.downsample_dpi
            self.settings["pdf compression"] = self._windowi.pdf_compression
            self.settings["quality"] = self._windowi.jpeg_quality
            self._save_file_chooser(uuids)
        elif self.settings["image type"] == "djvu":
            self._windowi.update_config_dict(self.settings)
            self._save_file_chooser(uuids)
        elif self.settings["image type"] == "tif":
            self.settings["tiff compression"] = self._windowi.tiff_compression
            self.settings["quality"] = self._windowi.jpeg_quality
            self._save_file_chooser(uuids)
        elif self.settings["image type"] == "txt":
            self._save_file_chooser(uuids)
        elif self.settings["image type"] == "hocr":
            self._save_file_chooser(uuids)
        elif self.settings["image type"] == "ps":
            self.settings["ps_backend"] = self._windowi.ps_backend
            logger.info("Selected '%s' as ps backend", self.settings["ps_backend"])
            self._save_file_chooser(uuids)
        elif self.settings["image type"] == "sdb":
            self._save_file_chooser(uuids)
        else:
            if self.settings["image type"] == "jpg":
                self.settings["quality"] = self._windowi.jpeg_quality
            self._save_image(uuids)

    def _save_file_chooser(self, uuids):

        # cd back to cwd to save
        self._chdir_cwd()

        title = _("PDF filename")  # pdf, append, prepend
        filter_desc = _("PDF files")
        filter_list = ["pdf"]
        if self.settings["image type"] == "djvu":
            title = _("DjVu filename")
            filter_desc = _("DjVu files")
            filter_list = [self.settings["image type"]]
        elif self.settings["image type"] == "tif":
            title = _("TIFF filename")
            filter_desc = _("Image files")
            filter_list = [self.settings["image type"]]
        elif self.settings["image type"] == "txt":
            title = _("Text filename")
            filter_desc = _("Text files")
            filter_list = [self.settings["image type"]]
        elif self.settings["image type"] == "hocr":
            title = _("hOCR filename")
            filter_desc = _("hOCR files")
            filter_list = [self.settings["image type"]]
        elif self.settings["image type"] == "ps":
            title = _("PS filename")
            filter_desc = _("Postscript files")
            filter_list = [self.settings["image type"]]
        elif self.settings["image type"] == "sdb":
            title = _("scantpaper session filename")
            filter_desc = _("scantpaper session files")
            filter_list = [self.settings["image type"]]
        file_chooser = Gtk.FileChooserDialog(
            title=title,
            parent=self._windowi,
            action=Gtk.FileChooserAction.SAVE,
        )
        file_chooser.add_buttons(
            Gtk.STOCK_CANCEL,
            Gtk.ResponseType.CANCEL,
            Gtk.STOCK_OK,
            Gtk.ResponseType.OK,
        )
        if self.settings["image type"] in ["pdf", "djvu"]:
            filename = expand_metadata_pattern(
                template=self.settings["default filename"],
                convert_whitespace=self.settings["convert whitespace to underscores"],
                author=self.settings["author"],
                title=self.settings["title"],
                docdate=self._windowi.meta_datetime,
                today_and_now=datetime.datetime.now(),
                extension=self.settings["image type"],
                subject=self.settings["subject"],
                keywords=self.settings["keywords"],
            )
            file_chooser.set_current_name(filename)
            file_chooser.set_do_overwrite_confirmation(True)
        add_filter(file_chooser, filter_desc, filter_list)
        file_chooser.set_current_folder(self.settings["cwd"])
        file_chooser.set_default_response(Gtk.ResponseType.OK)
        file_chooser.connect(
            "response",
            self._file_chooser_response_callback,
            [self.settings["image type"], uuids],
        )
        file_chooser.show()

        # cd back to tempdir
        os.chdir(self.session.name)

    def _list_of_page_uuids(self):
        "Compile list of pages"
        pagelist = self.slist.get_page_index(
            self.settings["Page range"], self._error_callback
        )
        if not pagelist:
            return []
        return [self.slist.data[i][2] for i in pagelist]

    def _file_chooser_response_callback(self, dialog, response, data):
        "Callback for file chooser dialog"
        filetype, uuids = data
        suffix = filetype
        if re.search(r"pdf", suffix, re.IGNORECASE):
            suffix = "pdf"
        if response == Gtk.ResponseType.OK:
            filename = dialog.get_filename()
            logger.debug("FileChooserDialog returned %s", filename)
            if not re.search(rf"[.]{suffix}$", filename, re.IGNORECASE):
                filename = f"{filename}.{filetype}"
                if file_exists(dialog, filename):
                    return

            if not self._file_writable(dialog, filename):
                return

            # Update cwd
            self.settings["cwd"] = os.path.dirname(filename)
            if re.search(r"pdf", filetype, re.IGNORECASE):
                self._save_pdf(filename, uuids, filetype)

            elif filetype == "ps":
                if self.settings["ps_backend"] == "libtiff":
                    tif = tempfile.TemporaryFile(dir=self.session, suffix=".tif")
                    self._save_tif(tif.filename(), uuids, filename)
                else:
                    self._save_pdf(filename, uuids, "ps")

            elif filetype == "session":
                self.slist.save_session(filename)

            elif filetype in ["djvu", "tif", "txt", "hocr"]:
                method = getattr(self, f"_save_{filetype}")
                method(filename, uuids)

            if self._windowi is not None and self.settings["close_dialog_on_save"]:
                self._windowi.hide()

        dialog.destroy()

    def _file_writable(self, chooser, filename):
        "Check if a file or its directory is writable and show an error dialog if not."

        if not os.access(
            os.path.dirname(filename), os.W_OK
        ):  # FIXME: replace with try/except
            text = _("Directory %s is read-only") % (os.path.dirname(filename))
            self._show_message_dialog(
                parent=chooser,
                message_type="error",
                buttons=Gtk.ButtonsType.CLOSE,
                text=text,
            )
            return False

        if os.path.isfile(filename) and not os.access(
            filename, os.W_OK
        ):  # FIXME: replace with try/except
            text = _("File %s is read-only") % (filename)
            self._show_message_dialog(
                parent=chooser,
                message_type="error",
                buttons=Gtk.ButtonsType.CLOSE,
                text=text,
            )
            return False

        return True

    def _save_pdf(self, filename, list_of_page_uuids, option):
        "Save selected pages as PDF under given name."

        # Compile options
        options = {
            "compression": self.settings["pdf compression"],
            "downsample": self.settings["downsample"],
            "downsample dpi": self.settings["downsample dpi"],
            "quality": self.settings["quality"],
            "user-password": self._windowi.pdf_user_password,
            "set_timestamp": self.settings["set_timestamp"],
            "convert whitespace to underscores": self.settings[
                "convert whitespace to underscores"
            ],
        }
        if option == "prependpdf":
            options["prepend"] = filename

        elif option == "appendpdf":
            options["append"] = filename

        elif option == "ps":
            options["ps"] = filename
            options["pstool"] = self.settings["ps_backend"]

        if self.settings["post_save_hook"]:
            options["post_save_hook"] = self.settings["current_psh"]

        # Create the PDF
        logger.debug("Started saving %s", filename)

        def save_pdf_finished_callback(response):
            self.post_process_progress.finish(response)
            self.slist.thread.send("set_saved", list_of_page_uuids)
            if (
                "view files toggle" in self.settings
                and self.settings["view files toggle"]
            ):
                if "ps" in options:
                    launch_default_for_file(options["ps"])
                else:
                    launch_default_for_file(filename)

            logger.debug("Finished saving %s", filename)

        self.slist.save_pdf(
            path=filename,
            list_of_pages=list_of_page_uuids,
            metadata=collate_metadata(self.settings, datetime.datetime.now()),
            options=options,
            queued_callback=self.post_process_progress.queued,
            started_callback=self.post_process_progress.update,
            running_callback=self.post_process_progress.update,
            data_callback=self.post_process_progress.update,
            finished_callback=save_pdf_finished_callback,
            error_callback=self._error_callback,
        )

    def _save_djvu(self, filename, uuids):
        "Save a list of pages as a DjVu file."

        # cd back to tempdir
        os.chdir(self.session.name)

        # Create the DjVu
        logger.debug("Started saving %s", filename)
        options = {
            "set_timestamp": self.settings["set_timestamp"],
            "convert whitespace to underscores": self.settings[
                "convert whitespace to underscores"
            ],
        }
        if self.settings["post_save_hook"]:
            options["post_save_hook"] = self.settings["current_psh"]

        def save_djvu_finished_callback(response):
            filename = response.request.args[0]["path"]
            self.post_process_progress.finish(response)
            self.slist.thread.send("set_saved", uuids)
            if (
                "view files toggle" in self.settings
                and self.settings["view files toggle"]
            ):
                launch_default_for_file(filename)
            logger.debug("Finished saving %s", filename)

        self.slist.save_djvu(
            path=filename,
            list_of_pages=uuids,
            options=options,
            metadata=collate_metadata(self.settings, datetime.datetime.now()),
            queued_callback=self.post_process_progress.queued,
            started_callback=self.post_process_progress.update,
            running_callback=self.post_process_progress.update,
            finished_callback=save_djvu_finished_callback,
            error_callback=self._error_callback,
        )

    def _save_tif(self, filename, uuids, ps=None):
        "Save a list of pages as a TIFF file with specified options"
        options = {
            "compression": self.settings["tiff compression"],
            "quality": self.settings["quality"],
            "ps": ps,
        }
        if self.settings["post_save_hook"]:
            options["post_save_hook"] = self.settings["current_psh"]

        def save_tiff_finished_callback(response):
            filename = response.request.args[0]["path"]
            self.post_process_progress.finish(response)
            self.slist.thread.send("set_saved", uuids)
            file = ps if ps is not None else filename
            if (
                "view files toggle" in self.settings
                and self.settings["view files toggle"]
            ):
                launch_default_for_file(file)

            logger.debug("Finished saving %s", file)

        self.slist.save_tiff(
            path=filename,
            list_of_pages=uuids,
            options=options,
            queued_callback=self.post_process_progress.queued,
            started_callback=self.post_process_progress.update,
            running_callback=self.post_process_progress.update,
            finished_callback=save_tiff_finished_callback,
            error_callback=self._error_callback,
        )

    def _save_txt(self, filename, uuids):
        "Save OCR text"
        options = {}
        if self.settings["post_save_hook"]:
            options["post_save_hook"] = self.settings["current_psh"]

        def save_text_finished_callback(response):
            self.post_process_progress.finish(response)
            self.slist.thread.send("set_saved", uuids)
            if (
                "view files toggle" in self.settings
                and self.settings["view files toggle"]
            ):
                launch_default_for_file(filename)

            logger.debug("Finished saving %s", filename)

        self.slist.save_text(
            path=filename,
            list_of_pages=uuids,
            options=options,
            queued_callback=self.post_process_progress.queued,
            started_callback=self.post_process_progress.update,
            running_callback=self.post_process_progress.update,
            finished_callback=save_text_finished_callback,
            error_callback=self._error_callback,
        )

    def _save_hocr(self, filename, uuids):
        "Save HOCR (HTML OCR) data to a file"
        options = {}
        if self.settings["post_save_hook"]:
            options["post_save_hook"] = self.settings["current_psh"]

        def save_hocr_finished_callback(response):
            self.slist.thread.send("set_saved", uuids)
            self.post_process_progress.finish(response)
            if (
                "view files toggle" in self.settings
                and self.settings["view files toggle"]
            ):
                launch_default_for_file(filename)

            logger.debug("Finished saving %s", filename)

        self.slist.save_hocr(
            path=filename,
            list_of_pages=uuids,
            options=options,
            queued_callback=self.post_process_progress.queued,
            started_callback=self.post_process_progress.update,
            running_callback=self.post_process_progress.update,
            finished_callback=save_hocr_finished_callback,
            error_callback=self._error_callback,
        )

    def _save_image(self, uuids):
        "Save selected pages as image under given name."

        # cd back to cwd to save
        self._chdir_cwd()

        # Set up file selector
        file_chooser = Gtk.FileChooserDialog(
            title=_("Image filename"),
            parent=self._windowi,
            action=Gtk.FileChooserAction.SAVE,
        )
        file_chooser.add_buttons(
            Gtk.STOCK_CANCEL,
            Gtk.ResponseType.CANCEL,
            Gtk.STOCK_SAVE,
            Gtk.ResponseType.OK,
        )
        file_chooser.set_default_response(Gtk.ResponseType.OK)
        file_chooser.set_current_folder(self.settings["cwd"])
        add_filter(
            file_chooser,
            _("Image files"),
            ["jpg", "png", "pnm", "gif", "tif", "tiff", "pdf", "djvu", "ps"],
        )
        file_chooser.set_do_overwrite_confirmation(True)
        if file_chooser.run() == Gtk.ResponseType.OK:
            filename = file_chooser.get_filename()

            # Update cwd
            self.settings["cwd"] = os.path.dirname(filename)

            # cd back to tempdir
            os.chdir(self.session.name)
            if len(uuids) > 1:
                w = len(uuids)
                for i in range(1, len(uuids) + 1):
                    current_filename = (
                        f"{filename}_%0{w}d.{self.settings['image type']}" % (i)
                    )
                    if os.path.isfile(current_filename):
                        text = _("This operation would overwrite %s") % (
                            current_filename
                        )
                        self._show_message_dialog(
                            parent=file_chooser,
                            message_type="error",
                            buttons=Gtk.ButtonsType.CLOSE,
                            text=text,
                        )
                        file_chooser.destroy()
                        return

                filename = f"${filename}_%0${w}d.{self.settings['image type']}"

            else:
                if not re.search(
                    rf"[.]{self.settings['image type']}$",
                    filename,
                    re.IGNORECASE | re.MULTILINE | re.DOTALL | re.VERBOSE,
                ):
                    filename = f"{filename}.{self.settings['image type']}"
                    if file_exists(file_chooser, filename):
                        return

                if not self._file_writable(file_chooser, filename):
                    return

            # Create the image
            logger.debug("Started saving %s", filename)

            def save_image_finished_callback(response):
                filename = response.request.args[0]["path"]
                self.post_process_progress.finish(response)
                self.slist.thread.send("set_saved", uuids)
                if (
                    "view files toggle" in self.settings
                    and self.settings["view files toggle"]
                ):
                    w = len(uuids)
                    if w > 1:
                        for i in range(1, w + 1):
                            launch_default_for_file(filename % (i))
                    else:
                        launch_default_for_file(filename)

                logger.debug("Finished saving %s", filename)

            self.slist.save_image(
                path=filename,
                list_of_pages=uuids,
                queued_callback=self.post_process_progress.queued,
                started_callback=self.post_process_progress.update,
                running_callback=self.post_process_progress.update,
                finished_callback=save_image_finished_callback,
                error_callback=self._error_callback,
            )
            if self._windowi is not None:
                self._windowi.hide()

        file_chooser.destroy()

    def _update_post_save_hooks(self):
        "Updates the post-save hooks"
        if self._windowi is not None:
            if hasattr(self._windowi, "comboboxpsh"):
                # empty combobox
                for _i in range(1, self._windowi.comboboxpsh.get_num_rows() + 1):
                    self._windowi.comboboxpsh.remove(0)

            else:
                # create it
                self._windowi.comboboxpsh = ComboBoxText()

            # fill it again
            for tool in self.settings["user_defined_tools"]:
                if not re.search(r"%o", tool, re.MULTILINE | re.DOTALL | re.VERBOSE):
                    self._windowi.comboboxpsh.append_text(tool)

            self._windowi.comboboxpsh.set_active_by_text(self.settings["current_psh"])

    def print_dialog(self, _action, _param):
        "print"
        self._chdir_cwd()
        print_op = PrintOperation(settings=self.print_settings, slist=self.slist)
        res = print_op.run(Gtk.PrintOperationAction.PRINT_DIALOG, self)
        if res == Gtk.PrintOperationResult.APPLY:
            self.print_settings = print_op.get_print_settings()
        os.chdir(self.session.name)

    def quit_app(self, _action, _param):
        "Handle the quit action for the application."
        if self._can_quit():
            self.get_application().quit()

    def _can_quit(self):
        "Remove temporary files, note window state, save settings and quit."
        if not self._pages_saved(
            _("Some pages have not been saved.\nDo you really want to quit?")
        ):
            return False

        # Make sure that we are back in the start directory,
        # otherwise we can't delete the temp dir.
        self._chdir_cwd()

        # Remove temporary files and session db
        for file in glob.glob(self.session.name + "/*") + glob.glob(
            self.session.name + ".sdb*"
        ):
            os.remove(file)
        os.rmdir(self.session.name)
        # Write window state to settings
        self.settings["window_width"], self.settings["window_height"] = self.get_size()
        self.settings["window_x"], self.settings["window_y"] = self.get_position()
        self.settings["thumb panel"] = self._hpaned.get_position()
        if self._windows:
            (
                self.settings["scan_window_width"],
                self.settings["scan_window_height"],
            ) = self._windows.get_size()
            logger.info("Killing Sane thread(s)")
            self._windows.thread.quit()

        # Write config file
        config.write_config(self._configfile, self.settings)
        logger.info("Killing document thread(s)")
        self.slist.thread.quit()
        logger.debug("Quitting")

        # remove lock
        fcntl.lockf(self._lockfd, fcntl.LOCK_UN)

        return True

    def _restart(self):
        "Restart the application"
        self._can_quit()
        os.execv(sys.executable, ["python"] + sys.argv)

    def _pages_saved(self, message):
        "Check that all pages have been saved"
        if not self.slist.thread.pages_saved():
            response = self._ask_question(
                parent=self,
                type="question",
                buttons=Gtk.ButtonsType.OK_CANCEL,
                text=message,
                store_response=True,
                stored_responses=[Gtk.ResponseType.OK],
            )
            if response != Gtk.ResponseType.OK:
                return False
        return True
