thenaterhood/gscreenshot

View on GitHub
src/gscreenshot/frontend/gtk/__init__.py

Summary

Maintainability
B
4 hrs
Test Coverage
#pylint: disable=unused-argument
#pylint: disable=wrong-import-order
#pylint: disable=wrong-import-position
#pylint: disable=ungrouped-imports
#pylint: disable=too-many-statements
'''
Classes for the GTK gscreenshot frontend
'''
import gettext
import io
import sys
import threading
import typing
from time import sleep
from pkg_resources import resource_string, resource_filename
import pygtkcompat
from gscreenshot import Gscreenshot, GscreenshotClipboardException
from gscreenshot.frontend.gtk.dialogs import OpenWithDialog, WarningDialog
from gscreenshot.frontend.gtk.dialogs import FileSaveDialog, FileOpenDialog
from gscreenshot.frontend.gtk.view import View
from gscreenshot.screenshooter.exceptions import NoSupportedScreenshooterError
from gscreenshot.screenshot.effects.crop import CropEffect

pygtkcompat.enable()
pygtkcompat.enable_gtk(version='3.0')
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import GObject
from gi.repository import GLib

i18n = gettext.gettext


class Presenter(object):
    '''Presenter class for the GTK frontend'''

    __slots__ = ('_delay', '_app', '_hide',
            '_view', '_keymappings', '_capture_cursor',
            '_cursor_selection', '_overwrite_mode')

    _delay: int
    _app: Gscreenshot
    _hide: bool
    _view: View
    _keymappings: dict
    _capture_cursor: bool
    _overwrite_mode: bool
    _cursor_selection: str

    def __init__(self, application: Gscreenshot, view: View):
        self._app = application
        self._view = view
        self._delay = 0
        self._hide = True
        self._capture_cursor = False
        self._show_preview()
        self._keymappings = {}
        self._overwrite_mode = True

        cursors = self._app.get_available_cursors()
        cursors[i18n("custom")] = None

        self._cursor_selection = list(cursors.keys())[0]

        self._view.update_available_cursors(
                cursors
                )

        self._view.show_cursor_options(self._capture_cursor)

    def _begin_take_screenshot(self, app_method, **args):
        app_method(delay=self._delay,
            capture_cursor=self._capture_cursor,
            cursor_name=self._cursor_selection,
            overwrite=self._overwrite_mode,
            **args)

        # Re-enable UI on the UI thread.
        GLib.idle_add(self._end_take_screenshot)

    def _end_take_screenshot(self):
        self._show_preview()
        screenshot_collection = self._app.get_screenshot_collection()
        self._view.update_gallery_controls(screenshot_collection)

        self._view.unhide()
        self._view.set_ready()

    def set_keymappings(self, keymappings: dict):
        '''Set the keymappings'''
        self._keymappings = keymappings

    def window_state_event_handler(self, widget, event, *_):
        '''Handle window state events'''
        self._view.handle_state_event(widget, event)

    def take_screenshot(self, app_method: typing.Callable, **args):
        '''Take a screenshot using the passed app method'''
        self._view.set_busy()

        if self._hide:
            self._view.hide()

        # Do work in background thread.
        # Taken from here: https://wiki.gnome.org/Projects/PyGObject/Threading
        _thread = threading.Thread(target=self._begin_take_screenshot(app_method, **args))
        _thread.daemon = True
        _thread.start()

    def handle_keypress(self, widget, event, *args):
        """
        This method handles individual keypresses. These are
        handled separately from accelerators (which include
        modifiers).
        """
        if event.keyval in self._keymappings:
            # The called function should return True to prevent
            # further handling of the keypress
            return self._keymappings[event.keyval](widget)

        return False

    def handle_preview_click_event(self, widget, event, *args):
        '''
        Handle a click on the screenshot preview widget
        '''
        # 3 is right click
        if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:
            self._view.show_actions_menu()

    def hide_window_toggled(self, widget):
        '''Toggle the window to hidden'''
        self._hide = widget.get_active()

    def capture_cursor_toggled(self, widget):
        '''Toggle capturing cursor'''
        self._capture_cursor = widget.get_active()
        self._view.show_cursor_options(self._capture_cursor)

    def overwrite_mode_toggled(self, widget):
        '''Toggle overwrite or multishot mode'''
        self._overwrite_mode = widget.get_active()

    def delay_value_changed(self, widget):
        '''Handle a change with the screenshot delay input'''
        self._delay = widget.get_value()

    def selected_cursor_changed(self, widget):
        '''Handle a change to the selected cursor'''
        try:
            cursor_selection = widget.get_model()[widget.get_active()][2]
        except IndexError:
            return

        if cursor_selection is None:
            return

        if cursor_selection == "custom":

            file_filter:Gtk.FileFilter = Gtk.FileFilter()
            supported_formats = self._app.get_supported_formats()
            _ = [file_filter.add_mime_type(
                f"image/{format}") for format in supported_formats
                if format not in ["pdf"]
            ]

            chooser = FileOpenDialog(
                file_filter=file_filter
            )
            chosen = None
            cancelled = False
            while not (chosen or cancelled):
                chosen = self._view.run_dialog(chooser)
                if chosen is None:
                    cancelled = True

            if chosen:
                try:
                    cursor_selection = self._app.register_stamp_image(chosen)
                #pylint: disable=broad-except
                except Exception:
                    warning = WarningDialog(f"Unable to open {chosen}")
                    self._view.run_dialog(warning)
                    cancelled = True

            if cancelled or cursor_selection is None:
                cursor_selection = self._cursor_selection

            cursors = self._app.get_available_cursors()
            cursors[i18n("custom")] = None
            self._view.update_available_cursors(
                cursors,
                cursor_selection
            )

        self._cursor_selection = cursor_selection

    def on_button_all_clicked(self, *_):
        '''Take a screenshot of the full screen'''
        self.take_screenshot(
            self._app.screenshot_full_display
            )

    def on_button_window_clicked(self, *args):
        '''Take a screenshot of a window'''
        self._button_select_area_or_window_clicked(args)

    def on_button_selectarea_clicked(self, *args):
        '''Take a screenshot of an area'''
        self._button_select_area_or_window_clicked(args)

    def _button_select_area_or_window_clicked(self, *_):
        '''Take a screenshot of an area or window'''
        self.take_screenshot(
            self._app.screenshot_selected
            )

    def on_preview_drag(self, _widget, _drag_context, data, _info, _time):
        '''
        Handle dragging and dropping the image preview
        '''
        fname = self._app.save_and_return_path()

        if fname is None:
            return

        data.set_uris([f"file://{fname}"])

    def on_use_last_region_clicked(self, *_):
        '''
        Take a screenshot with the same region as the
        screenshot under the cursor, if applicable
        '''
        last_screenshot = self._app.get_screenshot_collection().cursor_current()
        region = None

        if last_screenshot is not None:
            effects = last_screenshot.get_effects()
            crop_effect = next((i for i in effects if isinstance(i, CropEffect)), None)
            if crop_effect and "region" in crop_effect.meta:
                region = crop_effect.meta["region"]

        self.take_screenshot(
            self._app.screenshot_selected,
            region=region
        )

    def on_preview_prev_clicked(self, *_):
        '''Handle a click of the "previous" button on the preview'''
        screenshot_collection = self._app.get_screenshot_collection()
        screenshot_collection.cursor_prev()
        self._show_preview()
        self._view.update_gallery_controls(screenshot_collection)
        return True

    def on_preview_next_clicked(self, *_):
        '''Handle a click of the "next" button on the preview'''
        screenshot_collection = self._app.get_screenshot_collection()
        screenshot_collection.cursor_next()
        self._show_preview()
        self._view.update_gallery_controls(screenshot_collection)
        return True

    def effect_checkbox_handler(self, widget, effect):
        '''
        Handles toggling effects on and off
        '''
        screenshot = self._app.get_screenshot_collection().cursor_current()
        if screenshot is None:
            return

        if widget.get_active():
            effect.enable()
        else:
            effect.disable()

        self._show_preview()

    def on_button_saveas_clicked(self, *_):
        '''Handle the saveas button'''
        saved = False
        cancelled = False

        save_dialog = FileSaveDialog(
                self._app.get_time_filename(),
                self._app.get_last_save_directory(),
                self._view.get_window()
                )

        while not (saved or cancelled):
            fname = self._view.run_dialog(save_dialog)
            if fname is not None:
                saved = self._app.save_last_image(fname)
            else:
                cancelled = True

        if saved:
            self._view.update_gallery_controls(self._app.get_screenshot_collection())
            self._view.flash_status_icon("document-save")

    def on_button_save_all_clicked(self, *_):
        '''Handle the "save all" button'''
        saved = False
        cancelled = False
        save_dialog = FileSaveDialog(
            self._app.get_time_foldername(),
            self._app.get_last_save_directory(),
            self._view.get_window(),
            choose_directory=True
        )

        while not (saved or cancelled):
            fname = self._view.run_dialog(save_dialog)
            if fname is not None:
                self._view.set_busy()
                saved = self._app.save_screenshot_collection(fname)
                self._view.set_ready()
            else:
                cancelled = True

        if saved:
            self._view.update_gallery_controls(self._app.get_screenshot_collection())
            self._view.flash_status_icon("document-save")

    def on_button_openwith_clicked(self, *_):
        '''Handle the "open with" button'''
        self._view.flash_status_icon(Gtk.STOCK_EXECUTE)
        fname = self._app.save_and_return_path()

        if fname is None:
            return

        appchooser = OpenWithDialog()

        self._view.run_dialog(appchooser)

        appinfo = appchooser.appinfo

        if appinfo is not None:
            if appinfo.launch_uris(["file://"+fname], None):

                screenshots = self._app.get_screenshot_collection()
                current = screenshots.cursor_current()
                if current is not None:
                    screenshots.remove(current)

                current = screenshots.cursor_current()
                if current is not None:
                    self._view.update_gallery_controls(screenshots)
                    self._show_preview()

                    return

                self.quit(None)

    def on_button_copy_clicked(self, *_):
        """
        Copy the current screenshot to the clipboard
        """
        img = self._app.get_last_image()

        if img is None:
            return False

        pixbuf = self._image_to_pixbuf(img)

        if not self._view.copy_to_clipboard(pixbuf):
            try:
                self._app.copy_last_screenshot_to_clipboard()
            except GscreenshotClipboardException as error:
                warning_dialog = WarningDialog(
                    i18n(
                        "Your clipboard doesn't support persistence and {0} isn't available."
                    ).format(error),
                    self._view.get_window())
                self._view.run_dialog(warning_dialog)
                return False

        self._view.flash_status_icon("edit-copy")
        return True

    def on_button_copy_and_close_clicked(self, *_):
        """
        Copy the current screenshot to the clipboard and
        close gscreenshot
        """
        if self.on_button_copy_clicked():
            screenshots = self._app.get_screenshot_collection()
            current = screenshots.cursor_current()
            if current is not None:
                screenshots.remove(current)

            current = screenshots.cursor_current()
            if current is not None:
                self._view.update_gallery_controls(screenshots)
                self._show_preview()

                return

            self.quit(None)

    def on_button_open_clicked(self, *_):
        '''Handle the open button'''
        success = self._app.open_last_screenshot()
        if not success:
            dialog = WarningDialog(
                i18n("Please install xdg-open to open files."),
                self._view.get_window())
            self._view.run_dialog(dialog)
        else:
            self._view.flash_status_icon("document-open")
            screenshots = self._app.get_screenshot_collection()
            current = screenshots.cursor_current()
            if current is not None:
                screenshots.remove(current)

            current = screenshots.cursor_current()
            if current is not None:
                self._view.update_gallery_controls(screenshots)
                self._show_preview()

                return
            self.quit(None)

    def on_button_about_clicked(self, *_):
        '''Handle the about button'''
        about = Gtk.AboutDialog(transient_for=self._view.get_window())

        authors = self._app.get_program_authors()
        about.set_authors(authors)

        description = i18n(self._app.get_program_description())
        description += "\n" + i18n("Using {0} screenshot backend").format(
            self._app.get_screenshooter_name()
        )

        capabilities_formatted = []
        for capability, provider in self._app.get_capabilities().items():
            capabilities_formatted.append(f"{i18n(capability)} ({provider})")
        description += "\n" + i18n("Available features: {0}").format(
            "\n ".join(capabilities_formatted)
        )

        about.set_comments(i18n(description))

        website = self._app.get_program_website()
        about.set_website(website)
        about.set_website_label(website)

        name = self._app.get_program_name()
        about.set_program_name(name)
        about.set_title(i18n("About"))

        license_text = self._app.get_program_license_text()
        about.set_license(license_text)

        version = self._app.get_program_version()
        about.set_version(version)

        about.set_logo(
                Gtk.gdk.pixbuf_new_from_file(
                    resource_filename(
                        'gscreenshot.resources.pixmaps', 'gscreenshot.png'
                        )
                    )
                )

        self._view.run_dialog(about)

    def on_fullscreen_toggle(self, *_):
        '''Handle the window getting toggled to fullscreen'''
        self._view.toggle_fullscreen()

    def on_button_quit_clicked(self, widget=None):
        '''Handle the quit button'''
        self.quit(widget)

    def on_window_main_destroy(self, widget=None):
        '''Handle the titlebar close button'''
        self.quit(widget)

    def on_window_resize(self, *_):
        '''Handle window resizes'''
        self._view.resize()
        self._show_preview()

    def quit(self, *_):
        '''Exit the app'''
        self._app.quit()

    def _image_to_pixbuf(self, image):
        for img_format in [("pnm", "ppm"), ("png", "png"), ("jpeg", "jpeg")]:
            try:
                loader = Gtk.gdk.PixbufLoader(img_format[0])
                descriptor = io.BytesIO()
                image = image.convert("RGB")
                image.save(descriptor, img_format[1])
                contents = descriptor.getvalue()
                descriptor.close()

                loader.write(contents)
            except GLib.GError:
                loader.close()
                continue

        pixbuf = loader.get_pixbuf()
        try:
            loader.close()
        except GLib.GError:
            pass
        return pixbuf

    def _show_preview(self):
        height, width = self._view.get_preview_dimensions()

        preview_img = self._app.get_thumbnail(width, height, with_border=True)

        self._view.update_preview(self._image_to_pixbuf(preview_img))


def main():
    '''The main function for the GTK frontend'''

    try:
        application = Gscreenshot()
    except NoSupportedScreenshooterError as gscreenshot_error:
        warning = WarningDialog(
            i18n("No supported screenshot backend is available."),
            None
            )

        if gscreenshot_error.required is not None:
            warning = WarningDialog(
                    i18n("Please install one of the following to use gscreenshot:")
                    + ", ".join(gscreenshot_error.required),
                    None
                )

        warning.run()
        sys.exit(1)

    builder = Gtk.Builder()
    builder.set_translation_domain('gscreenshot')
    builder.add_from_string(resource_string(
        'gscreenshot.resources.gui.glade', 'main.glade').decode('UTF-8'))

    window = builder.get_object('window_main')

    capabilities = application.get_capabilities()
    view = View(window, builder, capabilities)

    presenter = Presenter(
            application,
            view
            )

    # Lucky 13 to give a tiny bit more time for the desktop environment
    # to settle down and hide the window before we take our initial
    # screenshot.
    sleep(0.13)
    presenter.on_button_all_clicked()

    accel = Gtk.AccelGroup()
    accel.connect(Gdk.keyval_from_name('S'), Gdk.ModifierType.CONTROL_MASK,
            0, presenter.on_button_saveas_clicked)
    accel.connect(Gdk.keyval_from_name('C'), Gdk.ModifierType.CONTROL_MASK,
            0, presenter.on_button_copy_clicked)
    accel.connect(Gdk.keyval_from_name('O'), Gdk.ModifierType.CONTROL_MASK,
            0, presenter.on_button_open_clicked)
    accel.connect(Gdk.keyval_from_name('O'),
            Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK,
            0,
            presenter.on_button_openwith_clicked)
    accel.connect(Gdk.keyval_from_name('C'),
            Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK,
            0,
            presenter.on_button_copy_and_close_clicked)
    # These are set up in glade, so adding them here is redundant.
    # We'll keep the code for reference.
    #window.add_accel_group(accel)

    window.connect("key-press-event", presenter.handle_keypress)

    keymappings = {
        Gtk.gdk.keyval_to_lower(Gtk.gdk.keyval_from_name('Escape')):
            presenter.on_button_quit_clicked,
        Gtk.gdk.keyval_to_lower(Gtk.gdk.keyval_from_name('F11')):
            presenter.on_fullscreen_toggle,
        Gtk.gdk.keyval_to_lower(Gtk.gdk.keyval_from_name('Right')):
            presenter.on_preview_next_clicked,
        Gtk.gdk.keyval_to_lower(Gtk.gdk.keyval_from_name('Left')):
            presenter.on_preview_prev_clicked,
        # Handled in Glade - just here for reference
        #Gtk.gdk.keyval_to_lower(Gtk.gdk.keyval_from_name('Insert')):
        #    presenter.overwrite_mode_toggled
    }
    presenter.set_keymappings(keymappings)

    view.connect_signals(presenter)
    view.run()

    GObject.threads_init() # Start background threads.
    Gtk.main()

if __name__ == "__main__":
    main()