Пример #1
0
    def __init__(self):
        super().__init__()

        self.__events = Events()
        self.__threadpool = GlobalThreadPool.threadpool
        self.__extractor = None
        self.__condition = threading.Condition()
Пример #2
0
    def __init__(self, window: MainWindow):
        super().__init__()

        self.__events = Events()
        self.__events.add_event(EventType.FILE_EXTRACTED, self._extracted_file)
        self.__events.add_event(EventType.FILE_LISTED, self._listed_contents)

        self.__image_handler = ImageHandler()
        self.__image_files = ImageFiles()

        #: Indicates if files/archives are currently loaded/loading.
        self.__file_loaded = False
        self.__file_loading = False
        #: False if current file is not an archive, or unrecognized format.
        self.__is_archive = False

        #: Either path to the current archive, or first file in image list.
        #: This is B{not} the path to the currently open page.
        self.__current_file = None
        #: Reference to L{MainWindow}.
        self.__window = window
        #: Path to opened archive file, or directory containing current images.
        self.__base_path = None
        #: Archive extractor.
        self.__extractor = Extractor()
        #: Provides a list of available files/archives in the open directory.
        self.__file_provider_chooser = GetFileProvider()
        self.__file_provider = None

        self.__start_page = 1

        self.__open_first_page = None

        self.update_opening_behavior()
Пример #3
0
    def __init__(self, window: MainWindow):
        super().__init__()

        self.__window = window

        self.__events = Events()

        self.__file_handler = FileHandler(None)
        self.__image_handler = ImageHandler()

        self.__bookmark_path = ConfigFiles.BOOKMARK.value
        self.__bookmark_state_dirty = False

        #: List of bookmarks
        self.__bookmarks = self.load_bookmarks_file()
        #: Modification date of bookmarks file
        self.__bookmarks_size = self.get_bookmarks_file_size()
Пример #4
0
    def __init__(self, window: MainWindow):
        super().__init__()

        self.__window = window

        events = Events()
        events.add_event(EventType.BOOKMARK_ADD, self._create_bookmark_menuitems)
        events.add_event(EventType.BOOKMARK_REMOVE, self._create_bookmark_menuitems)

        item = Gtk.MenuItem()
        item.set_label('Add Bookmark')
        item.connect('activate', self._add_current_to_bookmarks)
        self.append(item)

        item = Gtk.MenuItem()
        item.set_label('Edit Bookmarks')
        item.connect('activate', self._edit_bookmarks)
        self.append(item)

        separator = Gtk.SeparatorMenuItem()
        self.append(separator)

        # Re-create the bookmarks menu if one was added/removed
        self._create_bookmark_menuitems()

        self.show_all()
Пример #5
0
    def __init__(self):
        super().__init__()

        self.__events = Events()
        self.__events.add_event(EventType.FILE_AVAILABLE, self.file_available)

        self.__image_files = ImageFiles()

        #: Caching thread
        self.__threadpool = GlobalThreadPool.threadpool
        self.__lock = Lock()
        self.__cache_lock = {}
        #: Current page
        self.__current_image = None
        #: Set of images reading for decoding (i.e. already extracted)
        self.__available_images = set()
        #: List of pixbufs we want to cache
        self.__wanted_pixbufs = []
        #: Pixbuf map from page > Pixbuf
        self.__raw_pixbufs = {}

        self.__thumbnailer = Thumbnailer()
Пример #6
0
    def __init__(self, window: MainWindow):
        super().__init__(title='Properties')

        self.set_transient_for(window)
        self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                         Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT)

        self.__window = window

        events = Events()
        events.add_event(EventType.FILE_OPENED, self._on_book_change)
        events.add_event(EventType.FILE_CLOSED, self._on_book_change)
        events.add_event(EventType.PAGE_AVAILABLE, self._on_page_available)
        events.add_event(EventType.PAGE_CHANGED, self._on_page_change)

        self.__file_handler = FileHandler(None)
        self.__image_handler = ImageHandler()

        self.resize(870, 560)
        self.set_border_width(2)
        self.set_resizable(True)
        self.set_default_response(Gtk.ResponseType.CLOSE)

        self.__notebook = Gtk.Notebook()
        self.vbox.pack_start(self.__notebook, True, True, 0)

        self.__notebook.set_border_width(2)

        self.__archive_page = PropertiesPage()
        self.__image_page = PropertiesPage()

        self.__notebook.append_page(self.__archive_page,
                                    Gtk.Label(label='Archive'))
        self.__notebook.append_page(self.__image_page,
                                    Gtk.Label(label='Image'))

        self._update_archive_page()

        self.show_all()
Пример #7
0
    def __init__(self, open_path: list = None):
        super().__init__(type=Gtk.WindowType.TOPLEVEL)

        # Load configuration.
        self.__preference_manager = PreferenceManager()
        self.__preference_manager.load_config_file()

        self.__events = Events()
        self.__events.add_event(EventType.FILE_OPENED, self._on_file_opened)
        self.__events.add_event(EventType.FILE_CLOSED, self._on_file_closed)
        self.__events.add_event(EventType.PAGE_AVAILABLE, self._page_available)
        self.__events.add_event(EventType.PAGE_CHANGED, self._page_changed)

        # Remember last scroll destination.
        self.__last_scroll_destination = Scroll.START.value

        self.__dummy_layout = FiniteLayout([(1, 1)], (1, 1), [1, 1], 0, 0)
        self.__layout = self.__dummy_layout
        self.__waiting_for_redraw = False
        self.__page_orientation = self._page_orientation()

        self.__file_handler = FileHandler(self)
        self.__filesystem_actions = FileSystemActions(self)
        self.__image_handler = ImageHandler()
        self.__bookmark_backend = BookmarkBackend(self)

        self.__thumbnailsidebar = ThumbnailSidebar(self)
        self.__thumbnailsidebar.hide()

        self.__statusbar = Statusbar()

        self.__zoom = ZoomModel()
        self.__zoom.set_fit_mode(config['ZOOM_MODE'])
        self.__zoom.set_scale_up(config['STRETCH'])
        self.__zoom.reset_user_zoom()

        self.__menubar = Menubar(self)

        self.__event_handler = EventHandler(self)

        self.__keybindings_map = KeyBindingsMap(self).BINDINGS
        self.__keybindings = KeybindingManager(self)

        # Hook up keyboard shortcuts
        self.__event_handler.event_handler_init()
        self.__event_handler.register_key_events()

        self.__cursor_handler = CursorHandler(self)
        self.__lens = MagnifyingLens(self)

        self.__main_layout = Gtk.Layout()
        self.__main_scrolled_window = Gtk.ScrolledWindow()
        self.__main_scrolled_window.add(self.__main_layout)
        self.__main_scrolled_window.set_hexpand(True)
        self.__main_scrolled_window.set_vexpand(True)
        self.__vadjust = self.__main_scrolled_window.get_vadjustment()
        self.__hadjust = self.__main_scrolled_window.get_hadjustment()

        grid = Gtk.Grid()
        grid.attach(self.__menubar, 0, 0, 2, 1)
        grid.attach(self.__thumbnailsidebar, 0, 1, 1, 1)
        grid.attach_next_to(self.__main_scrolled_window, self.__thumbnailsidebar, Gtk.PositionType.RIGHT, 1, 1)
        grid.attach(self.__statusbar, 0, 2, 2, 1)
        self.add(grid)

        # XXX limited to at most 2 pages
        self.__images = [Gtk.Image(), Gtk.Image()]
        for img in self.__images:
            self.__main_layout.put(img, 0, 0)

        # Each widget "eats" part of the main layout visible area.
        self.__toggle_axis = {
            self.__thumbnailsidebar: ZoomAxis.WIDTH.value,
            self.__statusbar: ZoomAxis.HEIGHT.value,
            self.__menubar: ZoomAxis.HEIGHT.value,
        }

        self.__main_layout.set_events(Gdk.EventMask.BUTTON1_MOTION_MASK |
                                      Gdk.EventMask.BUTTON2_MOTION_MASK |
                                      Gdk.EventMask.BUTTON_PRESS_MASK |
                                      Gdk.EventMask.BUTTON_RELEASE_MASK |
                                      Gdk.EventMask.POINTER_MOTION_MASK)

        self.__main_layout.drag_dest_set(Gtk.DestDefaults.ALL,
                                         [Gtk.TargetEntry.new('text/uri-list', 0, 0)],
                                         Gdk.DragAction.COPY |
                                         Gdk.DragAction.MOVE)

        self.connect('delete_event', self.terminate_program)
        self.connect('key_press_event', self.__event_handler.key_press_event)
        self.connect('configure_event', self.__event_handler.resize_event)
        self.connect('window-state-event', self.__event_handler.window_state_event)

        self.__main_layout.connect('button_release_event', self.__event_handler.mouse_release_event)
        self.__main_layout.connect('scroll_event', self.__event_handler.scroll_wheel_event)
        self.__main_layout.connect('button_press_event', self.__event_handler.mouse_press_event)
        self.__main_layout.connect('motion_notify_event', self.__event_handler.mouse_move_event)
        self.__main_layout.connect('drag_data_received', self.__event_handler.drag_n_drop_event)
        self.__main_layout.connect('motion-notify-event', self.__lens.motion_event)
        self.__main_layout.connect('motion-notify-event', self.__cursor_handler.refresh)

        self.set_title(Mcomix.APP_NAME.value)
        self.restore_window_geometry()

        if config['DEFAULT_FULLSCREEN']:
            self.change_fullscreen()

        self.show_all()

        self.__file_handler.open_file_init(open_path)
Пример #8
0
class MainWindow(Gtk.ApplicationWindow):
    """
    The main window, is created at start and terminates the program when closed
    """

    def __init__(self, open_path: list = None):
        super().__init__(type=Gtk.WindowType.TOPLEVEL)

        # Load configuration.
        self.__preference_manager = PreferenceManager()
        self.__preference_manager.load_config_file()

        self.__events = Events()
        self.__events.add_event(EventType.FILE_OPENED, self._on_file_opened)
        self.__events.add_event(EventType.FILE_CLOSED, self._on_file_closed)
        self.__events.add_event(EventType.PAGE_AVAILABLE, self._page_available)
        self.__events.add_event(EventType.PAGE_CHANGED, self._page_changed)

        # Remember last scroll destination.
        self.__last_scroll_destination = Scroll.START.value

        self.__dummy_layout = FiniteLayout([(1, 1)], (1, 1), [1, 1], 0, 0)
        self.__layout = self.__dummy_layout
        self.__waiting_for_redraw = False
        self.__page_orientation = self._page_orientation()

        self.__file_handler = FileHandler(self)
        self.__filesystem_actions = FileSystemActions(self)
        self.__image_handler = ImageHandler()
        self.__bookmark_backend = BookmarkBackend(self)

        self.__thumbnailsidebar = ThumbnailSidebar(self)
        self.__thumbnailsidebar.hide()

        self.__statusbar = Statusbar()

        self.__zoom = ZoomModel()
        self.__zoom.set_fit_mode(config['ZOOM_MODE'])
        self.__zoom.set_scale_up(config['STRETCH'])
        self.__zoom.reset_user_zoom()

        self.__menubar = Menubar(self)

        self.__event_handler = EventHandler(self)

        self.__keybindings_map = KeyBindingsMap(self).BINDINGS
        self.__keybindings = KeybindingManager(self)

        # Hook up keyboard shortcuts
        self.__event_handler.event_handler_init()
        self.__event_handler.register_key_events()

        self.__cursor_handler = CursorHandler(self)
        self.__lens = MagnifyingLens(self)

        self.__main_layout = Gtk.Layout()
        self.__main_scrolled_window = Gtk.ScrolledWindow()
        self.__main_scrolled_window.add(self.__main_layout)
        self.__main_scrolled_window.set_hexpand(True)
        self.__main_scrolled_window.set_vexpand(True)
        self.__vadjust = self.__main_scrolled_window.get_vadjustment()
        self.__hadjust = self.__main_scrolled_window.get_hadjustment()

        grid = Gtk.Grid()
        grid.attach(self.__menubar, 0, 0, 2, 1)
        grid.attach(self.__thumbnailsidebar, 0, 1, 1, 1)
        grid.attach_next_to(self.__main_scrolled_window, self.__thumbnailsidebar, Gtk.PositionType.RIGHT, 1, 1)
        grid.attach(self.__statusbar, 0, 2, 2, 1)
        self.add(grid)

        # XXX limited to at most 2 pages
        self.__images = [Gtk.Image(), Gtk.Image()]
        for img in self.__images:
            self.__main_layout.put(img, 0, 0)

        # Each widget "eats" part of the main layout visible area.
        self.__toggle_axis = {
            self.__thumbnailsidebar: ZoomAxis.WIDTH.value,
            self.__statusbar: ZoomAxis.HEIGHT.value,
            self.__menubar: ZoomAxis.HEIGHT.value,
        }

        self.__main_layout.set_events(Gdk.EventMask.BUTTON1_MOTION_MASK |
                                      Gdk.EventMask.BUTTON2_MOTION_MASK |
                                      Gdk.EventMask.BUTTON_PRESS_MASK |
                                      Gdk.EventMask.BUTTON_RELEASE_MASK |
                                      Gdk.EventMask.POINTER_MOTION_MASK)

        self.__main_layout.drag_dest_set(Gtk.DestDefaults.ALL,
                                         [Gtk.TargetEntry.new('text/uri-list', 0, 0)],
                                         Gdk.DragAction.COPY |
                                         Gdk.DragAction.MOVE)

        self.connect('delete_event', self.terminate_program)
        self.connect('key_press_event', self.__event_handler.key_press_event)
        self.connect('configure_event', self.__event_handler.resize_event)
        self.connect('window-state-event', self.__event_handler.window_state_event)

        self.__main_layout.connect('button_release_event', self.__event_handler.mouse_release_event)
        self.__main_layout.connect('scroll_event', self.__event_handler.scroll_wheel_event)
        self.__main_layout.connect('button_press_event', self.__event_handler.mouse_press_event)
        self.__main_layout.connect('motion_notify_event', self.__event_handler.mouse_move_event)
        self.__main_layout.connect('drag_data_received', self.__event_handler.drag_n_drop_event)
        self.__main_layout.connect('motion-notify-event', self.__lens.motion_event)
        self.__main_layout.connect('motion-notify-event', self.__cursor_handler.refresh)

        self.set_title(Mcomix.APP_NAME.value)
        self.restore_window_geometry()

        if config['DEFAULT_FULLSCREEN']:
            self.change_fullscreen()

        self.show_all()

        self.__file_handler.open_file_init(open_path)

    @property
    def bookmark_backend(self):
        """
        Interface for BookmarkBackend
        """

        return self.__bookmark_backend

    @property
    def thumbnailsidebar(self):
        """
        Interface for ThumbnailSidebar
        """

        return self.__thumbnailsidebar

    @property
    def statusbar(self):
        """
        Interface for Statusbar
        """

        return self.__statusbar

    @property
    def event_handler(self):
        """
        Interface for EventHandler
        """

        return self.__event_handler

    @property
    def keybindings_map(self):
        """
        Interface for KeyBindingsMap
        """

        return self.__keybindings_map

    @property
    def keybindings(self):
        """
        Interface for KeybindingManager
        """

        return self.__keybindings

    @property
    def cursor_handler(self):
        """
        Interface for CursorHandler
        """

        return self.__cursor_handler

    @property
    def lens(self):
        """
        Interface for MagnifyingLens
        """

        return self.__lens

    @property
    def layout(self):
        return self.__layout

    @property
    def main_layout(self):
        return self.__main_layout

    @property
    def hadjust(self):
        return self.__hadjust

    @property
    def vadjust(self):
        return self.__vadjust

    def _page_orientation(self):
        if ViewState.is_manga_mode:
            return PageOrientation.MANGA.value
        else:
            return PageOrientation.WESTERN.value

    def draw_image(self, scroll_to=None):
        """
        Draw the current pages and update the titlebar and statusbar
        """

        if self.__waiting_for_redraw:
            # Don't stack up redraws.
            return

        self.__waiting_for_redraw = True
        GLib.idle_add(self._draw_image, scroll_to, priority=GLib.PRIORITY_HIGH_IDLE)

    def _draw_image(self, scroll_to: int):
        # hides old images before showing new ones
        # also if in double page mode and only a single
        # image is going to be shown, prevents a ghost second image
        for i in self.__images:
            i.clear()

        if not self.__file_handler.get_file_loaded():
            self.__thumbnailsidebar.hide()
            self.__waiting_for_redraw = False
            return

        self.__thumbnailsidebar.show()

        if not self.__image_handler.page_is_available():
            # Save scroll destination for when the page becomes available.
            self.__last_scroll_destination = scroll_to
            self.__waiting_for_redraw = False
            return

        distribution_axis = ZoomAxis.DISTRIBUTION.value
        alignment_axis = ZoomAxis.ALIGNMENT.value
        # XXX limited to at most 2 pages
        pixbuf_count = 2 if ViewState.is_displaying_double else 1
        pixbuf_count_iter = range(pixbuf_count)
        pixbuf_list = list(self.__image_handler.get_pixbufs(pixbuf_count))
        do_not_transform = [ImageTools.disable_transform(x) for x in pixbuf_list]
        size_list = [[pixbuf.get_width(), pixbuf.get_height()] for pixbuf in pixbuf_list]

        # Rotation handling:
        # - apply Exif rotation on individual images
        # - apply manual rotation on whole page
        orientation = self.__page_orientation
        if config['AUTO_ROTATE_FROM_EXIF']:
            rotation_list = [ImageTools.get_implied_rotation(pixbuf) for pixbuf in pixbuf_list]
            for i in pixbuf_count_iter:
                if rotation_list[i] in (90, 270):
                    size_list[i].reverse()
        else:
            # no auto rotation
            rotation_list = [0] * len(pixbuf_list)

        rotation = config['ROTATION'] % 360
        match rotation:
            case (90 | 270):
                distribution_axis, alignment_axis = alignment_axis, distribution_axis
                orientation.reverse()
                for i in pixbuf_count_iter:
                    size_list[i].reverse()
            case 180:
                orientation.reverse()

        # Recompute the visible area size
        viewport_size = self.get_visible_area_size()
        zoom_dummy_size = list(viewport_size)
        scaled_sizes = self.__zoom.get_zoomed_size(size_list, zoom_dummy_size, distribution_axis, do_not_transform)

        self.__layout = FiniteLayout(scaled_sizes, viewport_size, orientation, distribution_axis, alignment_axis)

        self.__main_layout.set_size(*self.__layout.get_union_box().get_size())

        content_boxes = self.__layout.get_content_boxes()

        for i in pixbuf_count_iter:
            rotation_list[i] = (rotation_list[i] + rotation) % 360

            pixbuf_list[i] = ImageTools.fit_pixbuf_to_rectangle(pixbuf_list[i], scaled_sizes[i], rotation_list[i])
            pixbuf_list[i] = ImageTools.enhance(pixbuf_list[i])

            ImageTools.set_from_pixbuf(self.__images[i], pixbuf_list[i])

            self.__main_layout.move(self.__images[i], *content_boxes[i].get_position())
            self.__images[i].show()

        # Reset orientation so scrolling behaviour is sane.
        self.__layout.set_orientation(self.__page_orientation)

        if scroll_to is not None:
            destination = (scroll_to,) * 2
            self.scroll_to_predefined(destination)

        # update statusbar
        resolutions = [(*size, scaled_size[0] / size[0]) for scaled_size, size in zip(scaled_sizes, size_list, strict=True)]
        if ViewState.is_manga_mode:
            resolutions.reverse()
        self.__statusbar.set_resolution(resolutions)
        self.__statusbar.update()

        self.__waiting_for_redraw = False

    def _update_page_information(self):
        """
        Updates the window with information that can be gathered
        even when the page pixbuf(s) aren't ready yet
        """

        page = self.__image_handler.get_current_page()
        if not page:
            return

        filenames = self.__image_handler.get_page_filename(page=page)
        filesizes = self.__image_handler.get_page_filesize(page=page)

        filename = ', '.join(filenames)
        filesize = ', '.join(filesizes)

        self.__statusbar.set_page_number(page, self.__image_handler.get_number_of_pages())
        self.__statusbar.set_filename(filename)
        self.__statusbar.set_filesize(filesize)

        self.__statusbar.update()

    def _get_virtual_double_page(self, page: int = None):
        """
        Return True if the current state warrants use of virtual
        double page mode (i.e. if double page mode is on, the corresponding
        preference is set, and one of the two images that should normally
        be displayed has a width that exceeds its height), or if currently
        on the first page

        :returns: True if the current state warrants use of virtual double page mode
        """

        if page is None:
            page = self.__image_handler.get_current_page()

        if (page == 1 and
                config['VIRTUAL_DOUBLE_PAGE_FOR_FITTING_IMAGES'] & DoublePage.AS_ONE_TITLE.value and
                self.__file_handler.is_archive()):
            return True

        if (not config['DEFAULT_DOUBLE_PAGE'] or
                not config['VIRTUAL_DOUBLE_PAGE_FOR_FITTING_IMAGES'] & DoublePage.AS_ONE_WIDE.value or
                self.__image_handler.is_last_page(page)):
            return False

        for page in (page, page + 1):
            if not self.__image_handler.page_is_available(page):
                return False
            pixbuf = self.__image_handler.get_pixbuf(page)
            width, height = pixbuf.get_width(), pixbuf.get_height()
            if config['AUTO_ROTATE_FROM_EXIF']:
                rotation = ImageTools.get_implied_rotation(pixbuf)

                # if rotation not in (0, 90, 180, 270):
                #     return

                if rotation in (90, 270):
                    width, height = height, width
            if width > height:
                return True

        return False

    def _page_available(self, page: int):
        """
        Called whenever a new page is ready for displaying
        """

        # Refresh display when currently opened page becomes available.
        current_page = self.__image_handler.get_current_page()
        nb_pages = 2 if ViewState.is_displaying_double else 1
        if current_page <= page < (current_page + nb_pages):
            self._displayed_double()
            self.draw_image(scroll_to=self.__last_scroll_destination)
            self._update_page_information()

    def _on_file_opened(self):
        self._displayed_double()
        self.__thumbnailsidebar.show()

        if config['STATUSBAR_FULLPATH']:
            self.__statusbar.set_archive_filename(self.__file_handler.get_base_path())
        else:
            self.__statusbar.set_archive_filename(self.__file_handler.get_base_path().name)
        self.__statusbar.set_view_mode()
        self.__statusbar.set_filesize_archive(self.__file_handler.get_base_path())
        self.__statusbar.set_file_number(*self.__file_handler.get_file_number())
        self.__statusbar.update()

        self._update_title()

    def _on_file_closed(self):
        self.clear()
        self.__thumbnailsidebar.hide()
        self.__thumbnailsidebar.clear()

    def _new_page(self, at_bottom: bool = False):
        """
        Draw a *new* page correctly (as opposed to redrawing the same image with a new size or whatever)
        """

        if not config['KEEP_TRANSFORMATION']:
            config['ROTATION'] = 0

        if at_bottom:
            scroll_to = Scroll.END.value
        else:
            scroll_to = Scroll.START.value

        self.draw_image(scroll_to=scroll_to)

    def page_changed(self):
        """
        Called on page change
        """

        self.__events.run_events(EventType.PAGE_CHANGED)

    def _page_changed(self):
        self._displayed_double()
        self.__thumbnailsidebar.hide()
        self.__thumbnailsidebar.load_thumbnails()
        self._update_page_information()

    def set_page(self, num: int, at_bottom: bool = False):
        """
        Draws a *new* page (as opposed to redrawing the same image with a new size or whatever)
        """

        if num == self.__image_handler.get_current_page():
            return

        self.__image_handler.set_page(num)
        self.page_changed()
        self._new_page(at_bottom=at_bottom)

    def flip_page(self, number_of_pages: int, single_step: bool = False):
        if not self.__file_handler.get_file_loaded():
            return

        current_page = self.__image_handler.get_current_page()
        current_number_of_pages = self.__image_handler.get_number_of_pages()

        new_page = current_page + number_of_pages
        if (abs(number_of_pages) == 1 and
                not single_step and
                config['DEFAULT_DOUBLE_PAGE'] and
                config['DOUBLE_STEP_IN_DOUBLE_PAGE_MODE']):
            if number_of_pages == 1 and not self._get_virtual_double_page():
                new_page += 1
            elif number_of_pages == -1 and not self._get_virtual_double_page(new_page - 1):
                new_page -= 1

        if new_page <= 0:
            # Only switch to previous page when flipping one page before the
            # first one. (Note: check for (page number <= 1) to handle empty
            # archive case).
            if number_of_pages == -1 and current_page <= 1:
                return self.__file_handler.open_archive_direction(forward=False)
            # Handle empty archive case.
            new_page = min(1, current_number_of_pages)
        elif new_page > current_number_of_pages:
            if number_of_pages == 1:
                return self.__file_handler.open_archive_direction(forward=True)
            new_page = current_number_of_pages

        if new_page != current_page:
            self.set_page(new_page, at_bottom=(-1 == number_of_pages))

    def first_page(self):
        if self.__image_handler.get_number_of_pages():
            self.set_page(1)

    def last_page(self):
        number_of_pages = self.__image_handler.get_number_of_pages()
        if number_of_pages:
            self.set_page(number_of_pages)

    def page_select(self, *args):
        Pageselector(self)

    def rotate_90(self, *args):
        self.rotate_x(rotation=90)

    def rotate_180(self, *args):
        self.rotate_x(rotation=180)

    def rotate_270(self, *args):
        self.rotate_x(rotation=270)

    def rotate_x(self, rotation: int, *args):
        config['ROTATION'] = (config['ROTATION'] + rotation) % 360
        self.draw_image()

    def change_double_page(self, *args):
        config['DEFAULT_DOUBLE_PAGE'] = not config['DEFAULT_DOUBLE_PAGE']
        self._displayed_double()
        self._update_page_information()
        self.draw_image()

    def change_manga_mode(self, *args):
        config['DEFAULT_MANGA_MODE'] = not config['DEFAULT_MANGA_MODE']
        ViewState.is_manga_mode = config['DEFAULT_MANGA_MODE']
        self.__page_orientation = self._page_orientation()
        self.__statusbar.set_view_mode()
        self._update_page_information()
        self.draw_image()

    def is_fullscreen(self):
        window_state = self.get_window().get_state()
        return (window_state & Gdk.WindowState.FULLSCREEN) != 0

    def change_fullscreen(self, *args):
        # Disable action until transition if complete.

        if self.is_fullscreen():
            self.unfullscreen()

            self.__cursor_handler.auto_hide_off()

            # menu/status can only be hidden in fullscreen
            # if not hidden using .show() is the same as a NOOP
            self.__statusbar.show()
            self.__menubar.show()
        else:
            self.save_window_geometry()
            self.fullscreen()

            self.__cursor_handler.auto_hide_on()

            if config['FULLSCREEN_HIDE_STATUSBAR']:
                self.__statusbar.hide()
            if config['FULLSCREEN_HIDE_MENUBAR']:
                self.__menubar.hide()

        # No need to call draw_image explicitely,
        # as we'll be receiving a window state
        # change or resize event.

    def change_fit_mode_best(self, *args):
        self.change_zoom_mode(ZoomModes.BEST.value)

    def change_fit_mode_width(self, *args):
        self.change_zoom_mode(ZoomModes.WIDTH.value)

    def change_fit_mode_height(self, *args):
        self.change_zoom_mode(ZoomModes.HEIGHT.value)

    def change_fit_mode_size(self, *args):
        self.change_zoom_mode(ZoomModes.SIZE.value)

    def change_fit_mode_manual(self, *args):
        self.change_zoom_mode(ZoomModes.MANUAL.value)

    def change_zoom_mode(self, value: int = None):
        if value is not None:
            config['ZOOM_MODE'] = value
        self.__zoom.set_fit_mode(config['ZOOM_MODE'])
        self.__zoom.set_scale_up(config['STRETCH'])
        self.__zoom.reset_user_zoom()
        self.draw_image()

    def toggle_image_scaling(self):
        config['ENABLE_PIL_SCALING'] = not config['ENABLE_PIL_SCALING']

        self.__statusbar.update_image_scaling()
        self.draw_image()

    def change_image_scaling(self, step: int):
        if config['ENABLE_PIL_SCALING']:
            config_key = 'PIL_SCALING_FILTER'
            algos = ScalingPIL
        else:
            config_key = 'GDK_SCALING_FILTER'
            algos = ScalingGDK

        # inc/dec active algo, modulus loops algos to start on overflow
        # and end on underflow
        config[config_key] = algos((config[config_key] + step) % len(algos)).value

        self.__statusbar.update_image_scaling()
        self.draw_image()

    def change_stretch(self, *args):
        """
        Toggles stretching small images
        """

        config['STRETCH'] = not config['STRETCH']
        self.__zoom.set_scale_up(config['STRETCH'])
        self.draw_image()

    def open_dialog_about(self, *args):
        dialog = DialogChooser(DialogChoice.ABOUT)
        dialog.open_dialog(self)

    def open_dialog_enhance(self, *args):
        dialog = DialogChooser(DialogChoice.ENHANCE)
        dialog.open_dialog(self)

    def open_dialog_file_chooser(self, *args):
        dialog = DialogChooser(DialogChoice.FILECHOOSER)
        dialog.open_dialog(self)

    def open_dialog_keybindings(self, *args):
        dialog = DialogChooser(DialogChoice.KEYBINDINGS)
        dialog.open_dialog(self)

    def open_dialog_preference(self, *args):
        dialog = DialogChooser(DialogChoice.PREFERENCES)
        dialog.open_dialog(self)

    def open_dialog_properties(self, *args):
        dialog = DialogChooser(DialogChoice.PROPERTIES)
        dialog.open_dialog(self)

    def change_keep_transformation(self, *args):
        config['KEEP_TRANSFORMATION'] = not config['KEEP_TRANSFORMATION']

    def manual_zoom_in(self, *args):
        self.__zoom.zoom_in()
        self.draw_image()

    def manual_zoom_out(self, *args):
        self.__zoom.zoom_out()
        self.draw_image()

    def manual_zoom_original(self, *args):
        self.__zoom.reset_user_zoom()
        self.draw_image()

    def scroll(self, x: int, y: int):
        """
        Scroll <x> px horizontally and <y> px vertically. If <bound> is
        'first' or 'second', we will not scroll out of the first or second
        page respectively (dependent on manga mode). The <bound> argument
        only makes sense in double page mode.

        :returns: True if call resulted in new adjustment values, False otherwise
        """

        old_hadjust = self.__hadjust.get_value()
        old_vadjust = self.__vadjust.get_value()

        visible_width, visible_height = self.get_visible_area_size()

        hadjust_upper = max(0, self.__hadjust.get_upper() - visible_width)
        vadjust_upper = max(0, self.__vadjust.get_upper() - visible_height)
        hadjust_lower = 0

        new_hadjust = old_hadjust + x
        new_vadjust = old_vadjust + y

        new_hadjust = max(hadjust_lower, new_hadjust)
        new_vadjust = max(0, new_vadjust)

        new_hadjust = min(hadjust_upper, new_hadjust)
        new_vadjust = min(vadjust_upper, new_vadjust)

        self.__vadjust.set_value(new_vadjust)
        self.__hadjust.set_value(new_hadjust)

        return old_vadjust != new_vadjust or old_hadjust != new_hadjust

    def scroll_to_predefined(self, destination: tuple):
        self.__layout.scroll_to_predefined(destination)
        viewport_position = self.__layout.get_viewport_box().get_position()
        self.__hadjust.set_value(viewport_position[0])  # 2D only
        self.__vadjust.set_value(viewport_position[1])  # 2D only

    def clear(self):
        """
        Clear the currently displayed data (i.e. "close" the file)
        """

        self.set_title(Mcomix.APP_NAME.value)
        self.__statusbar.set_message('')
        self.draw_image()

    def _displayed_double(self):
        """
        sets True if two pages are currently displayed
        """

        ViewState.is_displaying_double = (
            self.__image_handler.get_current_page() and
            config['DEFAULT_DOUBLE_PAGE'] and
            not self._get_virtual_double_page() and
            not self.__image_handler.is_last_page()
        )

    def get_visible_area_size(self):
        """
        :returns: a 2-tuple with the width and height of the visible part of the main layout area
        """

        dimensions = list(self.get_size())
        size = 0

        for widget, axis in self.__toggle_axis.items():
            size = widget.get_preferred_size()
            match axis:
                case ZoomAxis.WIDTH.value:
                    size = size.natural_size.width
                case ZoomAxis.HEIGHT.value:
                    size = size.natural_size.height
            dimensions[axis] -= size

        return tuple(dimensions)

    def _update_title(self):
        """
        Set the title acording to current state
        """

        self.set_title(f'{Mcomix.APP_NAME.value} [{self.__file_handler.get_real_path()}]')

    def extract_page(self, *args):
        self.__filesystem_actions.extract_page()

    def move_file(self, *args):
        self.__filesystem_actions.move_file()

    def trash_file(self, *args):
        self.__filesystem_actions.trash_file()

    def minimize(self, *args):
        """
        Minimizes the MComix window
        """

        self.iconify()

    def get_window_geometry(self):
        return self.get_position() + self.get_size()

    def save_window_geometry(self):
        if config['WINDOW_SAVE']:
            (
                config['WINDOW_X'],
                config['WINDOW_Y'],
                config['WINDOW_WIDTH'],
                config['WINDOW_HEIGHT'],
            ) = self.get_window_geometry()

    def restore_window_geometry(self):
        if self.get_window_geometry() == (config['WINDOW_X'],
                                          config['WINDOW_Y'],
                                          config['WINDOW_WIDTH'],
                                          config['WINDOW_HEIGHT']):
            return False
        self.resize(config['WINDOW_WIDTH'], config['WINDOW_HEIGHT'])
        self.move(config['WINDOW_X'], config['WINDOW_Y'])
        return True

    def terminate_program(self, *args):
        """
        Run clean-up tasks and exit the program
        """

        if not self.is_fullscreen():
            self.save_window_geometry()

        self.hide()

        if Gtk.main_level() > 0:
            Gtk.main_quit()

        # write config file
        self.__preference_manager.write_config_file()
        self.__keybindings.write_keybindings_file()
        self.__bookmark_backend.write_bookmarks_file()

        self.__file_handler.close_file()
Пример #9
0
class BookmarkBackend:
    """
    The _BookmarkBackend is a backend for both the bookmarks menu and dialog.
    Changes in the _BookmarkBackend are mirrored in both
    """
    def __init__(self, window: MainWindow):
        super().__init__()

        self.__window = window

        self.__events = Events()

        self.__file_handler = FileHandler(None)
        self.__image_handler = ImageHandler()

        self.__bookmark_path = ConfigFiles.BOOKMARK.value
        self.__bookmark_state_dirty = False

        #: List of bookmarks
        self.__bookmarks = self.load_bookmarks_file()
        #: Modification date of bookmarks file
        self.__bookmarks_size = self.get_bookmarks_file_size()

    def add_bookmark(self, bookmark):
        """
        Add the <bookmark> to the list
        """

        self.__bookmarks.append(bookmark)

        self.__bookmark_state_dirty = True
        self.write_bookmarks_file()
        self.__bookmark_state_dirty = False

        self.__events.run_events(EventType.BOOKMARK_ADD)

    def remove_bookmark(self, bookmark):
        """
        Remove the <bookmark> from the list
        """

        self.__bookmarks.remove(bookmark)

        self.__bookmark_state_dirty = True
        self.write_bookmarks_file()
        self.__bookmark_state_dirty = False

        self.__events.run_events(EventType.BOOKMARK_REMOVE)

    def add_current_to_bookmarks(self):
        """
        Add the currently viewed page to the list
        """

        path = self.__file_handler.get_real_path()
        current_page = self.__image_handler.get_current_page()
        total_pages = self.__image_handler.get_number_of_pages()
        date_added = datetime.today().timestamp()

        same_file_bookmarks = []

        for bookmark in self.__bookmarks:
            if Path(bookmark.bookmark_path) == path:
                if bookmark.bookmark_current_page == current_page:
                    logger.info(
                        f'Bookmark already exists for file \'{path}\' on page \'{current_page}\''
                    )
                    return

                same_file_bookmarks.append(bookmark)

        # If the same file was already bookmarked, ask to replace
        # the existing bookmarks before deleting them.
        if len(same_file_bookmarks) > 0:
            response = self.show_replace_bookmark_dialog(current_page)

            # Delete old bookmarks
            if response == Gtk.ResponseType.YES:
                for bookmark in same_file_bookmarks:
                    self.remove_bookmark(bookmark)
            # Perform no action
            elif response not in (Gtk.ResponseType.YES, Gtk.ResponseType.NO):
                return

        bookmark = Bookmark(self.__window, path, current_page, total_pages,
                            date_added)
        self.add_bookmark(bookmark)

    def open_bookmark(self, path: Path, current_page: int):
        """
        Open the file and page the bookmark represents
        """

        if not path.is_file():
            dialog = MessageDialogInfo()
            dialog.set_text(primary='Bookmarked file does not exist',
                            secondary=f'{path}')
            dialog.run()
            return

        if self.__file_handler.get_real_path() != path:
            self.__file_handler.open_file_init(paths=[path],
                                               start_page=current_page)
        else:
            self.__window.set_page(current_page)

    def get_bookmarks(self):
        """
        Return all the bookmarks in the list
        """

        if not self.file_was_modified():
            return self.__bookmarks

        self.__bookmarks = self.load_bookmarks_file()
        return self.__bookmarks

    def get_bookmarks_file_size(self):
        if not self.__bookmark_path.is_file():
            return 0

        return self.__bookmark_path.stat().st_size

    def load_bookmarks_file(self):
        """
        Loads persisted bookmarks from a local file.

        :return: Tuple of (bookmarks, file mtime)
        """

        bookmarks = []

        if not Path.is_file(self.__bookmark_path):
            return bookmarks

        try:
            with Path.open(self.__bookmark_path, mode='rt',
                           encoding='utf8') as fd:
                for bookmark in yaml.safe_load(fd):
                    for item in bookmark:
                        path = Path(bookmark[item]['path'], item)
                        current_page = bookmark[item]['current_page']
                        total_pages = bookmark[item]['total_pages']
                        date_added = bookmark[item]['created']

                        # if not path.is_file():
                        #     logger.warning(f'Missing bookmark: {path}')

                        bookmarks.append(
                            Bookmark(self.__window,
                                     path=path,
                                     current_page=current_page,
                                     total_pages=total_pages,
                                     date_added=date_added))
        except Exception as ex:
            logger.error(
                f'Could not parse bookmarks file: \'{self.__bookmark_path}\'')
            logger.error(f'Exception: {ex}')

        return bookmarks

    def bookmark_pack(self, bookmark):
        """
        Returns a dict. The bookmark can be fully
        re-created using the values in the dict
        """

        return {
            bookmark.bookmark_name: {
                # YAML does not work with Pathlike objects
                # when using CSafeDumper
                'path': str(Path(bookmark.bookmark_path).parent),
                'current_page': bookmark.bookmark_current_page,
                'total_pages': bookmark.bookmark_total_pages,
                # archive_type is deprecated, to be removed before next release
                'archive_type': 0,
                'created': bookmark.bookmark_date_added
            }
        }

    def file_was_modified(self):
        """
        Checks the bookmark store's mtime to see if it has been modified
        since it was last read
        """

        if not self.__bookmark_path.is_file() or \
                (self.get_bookmarks_file_size() != self.__bookmarks_size):
            return True

        return False

    def write_bookmarks_file(self):
        """
        Store relevant bookmark info in the mcomix directory
        """

        # Merge changes in case file was modified from within other instances
        if not self.__bookmark_state_dirty:
            logger.info('No changes to write for bookmarks')
            return
        logger.info('Writing changes to bookmarks')

        if self.file_was_modified():
            new_bookmarks = self.load_bookmarks_file()
            self.__bookmarks = list(set(self.__bookmarks + new_bookmarks))

        packs = [self.bookmark_pack(bookmark) for bookmark in self.__bookmarks]
        bookmarks = yaml.dump(packs,
                              Dumper=yaml.CSafeDumper,
                              sort_keys=False,
                              allow_unicode=True,
                              width=2147483647)
        self.__bookmark_path.write_text(bookmarks)
        self.__bookmarks_size = self.get_bookmarks_file_size()

    def show_replace_bookmark_dialog(self, new_page):
        """
        Present a confirmation dialog to replace old bookmarks.

        :returns: RESPONSE_YES to create replace bookmarks,
        RESPONSE_NO to create a new bookmark, RESPONSE_CANCEL to abort creating
        a new bookmark
        """

        dialog = MessageDialogRemember()
        dialog.add_buttons(Gtk.STOCK_YES, Gtk.ResponseType.YES, Gtk.STOCK_NO,
                           Gtk.ResponseType.NO, Gtk.STOCK_CANCEL,
                           Gtk.ResponseType.CANCEL)
        dialog.set_default_response(Gtk.ResponseType.YES)
        dialog.set_should_remember_choice(
            'replace-existing-bookmark',
            (Gtk.ResponseType.YES, Gtk.ResponseType.NO))
        dialog.set_text(
            primary='The current book already contains marked pages.',
            secondary=
            f'Do you want to replace them with a new bookmark on page {new_page}?'
            'Selecting "No" will create a new bookmark.')

        return dialog.run()
Пример #10
0
class ImageHandler(metaclass=SingleInstanceMetaClass):
    """
    The FileHandler keeps track of images, pages, caches and reads files.
    When the Filehandler's methods refer to pages, they are indexed from 1,
    i.e. the first page is page 1 etc.
    Other modules should *never* read directly from the files pointed to by
    paths given by the FileHandler's methods. The files are not even
    guaranteed to exist at all times since the extraction of archives is
    threaded
    """
    def __init__(self):
        super().__init__()

        self.__events = Events()
        self.__events.add_event(EventType.FILE_AVAILABLE, self.file_available)

        self.__image_files = ImageFiles()

        #: Caching thread
        self.__threadpool = GlobalThreadPool.threadpool
        self.__lock = Lock()
        self.__cache_lock = {}
        #: Current page
        self.__current_image = None
        #: Set of images reading for decoding (i.e. already extracted)
        self.__available_images = set()
        #: List of pixbufs we want to cache
        self.__wanted_pixbufs = []
        #: Pixbuf map from page > Pixbuf
        self.__raw_pixbufs = {}

        self.__thumbnailer = Thumbnailer()

    def get_pixbuf(self, page: int):
        """
        Return the pixbuf indexed by <page> from cache.
        Pixbufs not found in cache are fetched from disk first
        """

        self._cache_pixbuf(page, force_return=False)
        return self.__raw_pixbufs[page]

    def get_pixbufs(self, number_of_bufs: int):
        """
        Returns number_of_bufs pixbufs for the image(s) that should be
        currently displayed. This method might fetch images from disk, so make
        sure that number_of_bufs is as small as possible
        """

        return [
            self.get_pixbuf(self.__current_image + i)
            for i in range(number_of_bufs)
        ]

    def do_caching(self):
        """
        Make sure that the correct pixbufs are stored in cache. These
        are (in the current implementation) the current image(s), and
        if cacheing is enabled, also the one or two pixbufs before and
        after the current page. All other pixbufs are deleted and garbage
        collected directly in order to save memory
        """

        if not self.__lock.acquire(blocking=False):
            return

        # Get list of wanted pixbufs.
        self.__wanted_pixbufs = self._ask_for_pages(self.get_current_page())

        # remove old pixbufs.
        for page in set(self.__raw_pixbufs) - set(self.__wanted_pixbufs):
            del self.__raw_pixbufs[page]

        logger.debug(f'Caching page(s): {self.__wanted_pixbufs}')

        # Start caching available images not already in cache.
        wanted_pixbufs = [
            page for page in self.__wanted_pixbufs
            if page in self.__available_images
        ]
        self.__threadpool.map_async(self._cache_pixbuf, wanted_pixbufs)

        self.__lock.release()

    def _cache_pixbuf(self, page: int, force_return: bool = True):
        with self.__cache_lock[page]:
            if page in self.__raw_pixbufs:
                return
            with self.__lock:
                if page not in self.__wanted_pixbufs and force_return:
                    return
            logger.debug(f'Caching page: {page}')
            try:
                pixbuf = ImageTools.load_pixbuf(
                    self.__image_files.get_path_from_page(page))
            except Exception as ex:
                logger.error(f'Could not load pixbuf for page: {page}')
                logger.error(f'Exception: {ex}')
                pixbuf = None
            self.__raw_pixbufs[page] = pixbuf

    def set_page(self, page: int):
        """
        Set up filehandler to the page <page_num>
        """

        self.__current_image = page
        self.do_caching()

    def cleanup(self):
        """
        Run clean-up tasks. Should be called prior to exit
        """

        self.__threadpool.renew()
        self.__wanted_pixbufs.clear()
        self.__cache_lock.clear()
        self.__image_files.cleanup()
        self.__current_image = None
        self.__available_images.clear()
        self.__raw_pixbufs.clear()

    def page_is_available(self, page: int = None):
        """
        Returns True if <page> is available and calls to get_pixbufs
        would not block. If <page> is None, the current page(s) are assumed
        """

        if page is None:
            page = self.get_current_page()

        page_list = [page]
        if ViewState.is_displaying_double and not self.is_last_page(page):
            page_list.append(page + 1)

        for page in page_list:
            if page not in self.__available_images:
                return False

        return True

    def page_available(self, page: int):
        """
        Called whenever a new page becomes available, i.e. the corresponding file has been extracted
        """

        logger.debug(f'Page is available: {page}')

        self.__cache_lock[page] = Lock()
        self.__available_images.add(page)

        # Check if we need to cache it.
        if page in self.__wanted_pixbufs:
            self.__threadpool.apply_async(self._cache_pixbuf, (page, ))

        self.__events.run_events(EventType.PAGE_AVAILABLE, page)

    def file_available(self, filepath: Path):
        """
        Called by the filehandler when a new file becomes available
        """

        # Find the page that corresponds to <filepath>
        self.page_available(self.__image_files.get_page_from_path(filepath))

    def get_number_of_pages(self):
        """
        Return the number of pages in the current archive/directory
        """

        return self.__image_files.get_total_pages()

    def get_current_page(self):
        """
        Return the current page number (starting from 1), or 0 if no file is loaded
        """

        if self.__current_image is None:
            return 0

        return self.__current_image

    def is_last_page(self, page: int = None):
        """
        is <page> the last in a book, if page is None use current page
        """

        if page is None:
            page = self.get_current_page()

        return page == self.__image_files.get_total_pages()

    def get_path_to_page(self, page: int = None):
        """
        Return the full path to the image file for <page>, or the current page if <page> is None
        """

        if page is None:
            page = self.__current_image

        return self.__image_files.get_path_from_page(page)

    def _get_page_unknown(self):
        if ViewState.is_displaying_double:
            return ['unknown', 'unknown']
        return ['unknown']

    def get_page_filename(self, page: int = None):
        """
        :param page
            A page number or if None the current page
        :returns
            [page, page + 1] if ViewState.displayed_double is True else return [page]
        """

        if not self.page_is_available(page=page):
            return self._get_page_unknown()

        if page is None:
            page = self.get_current_page()

        page_data = [self.get_path_to_page(page).name]

        if ViewState.is_displaying_double:
            page_data.append(self.get_path_to_page(page + 1).name)

            if ViewState.is_manga_mode:
                page_data.reverse()

        return page_data

    def get_page_filesize(self, page: int = None):
        """
        :param page
            A page number or if None the current page
        :returns
            [page, page + 1] if ViewState.displayed_double is True else return [page]
        """

        if not self.page_is_available(page=page):
            return self._get_page_unknown()

        if page is None:
            page = self.get_current_page()

        page_data = [FileSize(self.get_path_to_page(page))]

        if ViewState.is_displaying_double:
            page_data.append(FileSize(self.get_path_to_page(page + 1)))

            if ViewState.is_manga_mode:
                page_data.reverse()

        return page_data

    def get_size(self, page: int = None):
        """
        Return a tuple (width, height) with the size of <page>. If <page>
        is None, return the size of the current page
        """

        page_path = self.get_path_to_page(page)
        if not Path.is_file(page_path):
            return 0, 0

        return ImageTools.get_image_size(page_path)

    def get_mime_name(self, page: int = None):
        """
        Return a string with the name of the mime type of <page>. If
        <page> is None, return the mime type name of the current page
        """

        page_path = self.get_path_to_page(page)
        if not Path.is_file(page_path):
            return None

        return ImageTools.get_image_mime(page_path)

    def get_thumbnail(self, page: int, size: tuple):
        """
        Return a thumbnail pixbuf of <page> that fit in a box with
        dimensions <width>x<height>. Return a thumbnail for the current
        page if <page> is None.
        If <nowait> is True, don't wait for <page> to be available
        """

        if not self._is_page_extracted(page=page):
            # Page is not available!
            return None

        path = self.get_path_to_page(page)
        if not Path.is_file(path):
            return None

        return self.__thumbnailer(size=size, filepath=path)

    def _is_page_extracted(self, page: int):
        if page is None:
            page = self.get_current_page()

        if page in self.__available_images:
            # page is extracted
            return True

        # page is not extracted
        return False

    def _ask_for_pages(self, page: int):
        """
        Ask for pages around <page> to be given priority extraction
        """

        total_pages = self.get_number_of_pages()

        cache_start = page - config['PAGE_CACHE_BEHIND']
        if cache_start < 1:
            cache_start = 1

        cache_end = page + config['PAGE_CACHE_FORWARD']
        if cache_end > total_pages:
            cache_end = total_pages

        return list(range(cache_start, cache_end))
Пример #11
0
    def __init__(self, window: MainWindow):
        self.__window = window

        self.__events = Events()
        self.__events.add_event(EventType.PAGE_AVAILABLE, self._page_available)

        self.__image_handler = ImageHandler()

        super().__init__(title='Go to page...',
                         modal=True,
                         destroy_with_parent=True)

        self.set_modal(True)
        self.set_transient_for(window)
        self.set_size_request(560, 820)

        self.add_buttons(
            '_Go',
            Gtk.ResponseType.OK,
            '_Cancel',
            Gtk.ResponseType.CANCEL,
        )
        self.set_default_response(Gtk.ResponseType.OK)
        self.connect('response', self._response)
        self.set_resizable(True)

        self.__number_of_pages = self.__image_handler.get_number_of_pages()

        self.__selector_adjustment = Gtk.Adjustment(
            value=self.__image_handler.get_current_page(),
            lower=1,
            upper=self.__number_of_pages,
            step_increment=1,
            page_increment=1)

        page_selector = Gtk.Scale.new(Gtk.Orientation.VERTICAL,
                                      self.__selector_adjustment)
        page_selector.set_draw_value(False)
        page_selector.set_digits(0)

        page_spinner = Gtk.SpinButton.new(self.__selector_adjustment, 0.0, 0)
        page_spinner.connect('changed', self._page_text_changed)
        page_spinner.set_activates_default(True)
        page_spinner.set_numeric(True)

        pages_label = Gtk.Label(label=f' of {self.__number_of_pages}')
        pages_label.set_xalign(0.0)
        pages_label.set_yalign(0.5)

        self.__image_preview = Gtk.Image()

        # Group preview image and page selector next to each other
        preview_box = Gtk.HBox()
        preview_box.set_border_width(5)
        preview_box.set_spacing(5)
        preview_box.pack_start(self.__image_preview, True, True, 0)
        preview_box.pack_end(page_selector, False, True, 0)
        # Below them, group selection spinner and current page label
        selection_box = Gtk.HBox()
        selection_box.set_border_width(5)
        selection_box.pack_start(page_spinner, True, True, 0)
        selection_box.pack_end(pages_label, False, True, 0)

        self.get_content_area().pack_start(preview_box, True, True, 0)
        self.get_content_area().pack_end(selection_box, False, True, 0)
        self.show_all()

        self.__selector_adjustment.connect('value-changed',
                                           self._cb_value_changed)

        # Set focus on the input box.
        page_spinner.select_region(0, -1)
        page_spinner.grab_focus()

        # Currently displayed thumbnail page.
        self.__thumbnail_page = 0
        self.__threadpool = GlobalThreadPool.threadpool
        self._update_thumbnail(int(self.__selector_adjustment.props.value))
Пример #12
0
class Pageselector(Gtk.Dialog):
    """
    The Pageselector takes care of the popup page selector
    """

    __slots__ = ('__window', '__image_handler', '__number_of_pages',
                 '__selector_adjustment', '__image_preview',
                 '__thumbnail_page', '__threadpool')

    def __init__(self, window: MainWindow):
        self.__window = window

        self.__events = Events()
        self.__events.add_event(EventType.PAGE_AVAILABLE, self._page_available)

        self.__image_handler = ImageHandler()

        super().__init__(title='Go to page...',
                         modal=True,
                         destroy_with_parent=True)

        self.set_modal(True)
        self.set_transient_for(window)
        self.set_size_request(560, 820)

        self.add_buttons(
            '_Go',
            Gtk.ResponseType.OK,
            '_Cancel',
            Gtk.ResponseType.CANCEL,
        )
        self.set_default_response(Gtk.ResponseType.OK)
        self.connect('response', self._response)
        self.set_resizable(True)

        self.__number_of_pages = self.__image_handler.get_number_of_pages()

        self.__selector_adjustment = Gtk.Adjustment(
            value=self.__image_handler.get_current_page(),
            lower=1,
            upper=self.__number_of_pages,
            step_increment=1,
            page_increment=1)

        page_selector = Gtk.Scale.new(Gtk.Orientation.VERTICAL,
                                      self.__selector_adjustment)
        page_selector.set_draw_value(False)
        page_selector.set_digits(0)

        page_spinner = Gtk.SpinButton.new(self.__selector_adjustment, 0.0, 0)
        page_spinner.connect('changed', self._page_text_changed)
        page_spinner.set_activates_default(True)
        page_spinner.set_numeric(True)

        pages_label = Gtk.Label(label=f' of {self.__number_of_pages}')
        pages_label.set_xalign(0.0)
        pages_label.set_yalign(0.5)

        self.__image_preview = Gtk.Image()

        # Group preview image and page selector next to each other
        preview_box = Gtk.HBox()
        preview_box.set_border_width(5)
        preview_box.set_spacing(5)
        preview_box.pack_start(self.__image_preview, True, True, 0)
        preview_box.pack_end(page_selector, False, True, 0)
        # Below them, group selection spinner and current page label
        selection_box = Gtk.HBox()
        selection_box.set_border_width(5)
        selection_box.pack_start(page_spinner, True, True, 0)
        selection_box.pack_end(pages_label, False, True, 0)

        self.get_content_area().pack_start(preview_box, True, True, 0)
        self.get_content_area().pack_end(selection_box, False, True, 0)
        self.show_all()

        self.__selector_adjustment.connect('value-changed',
                                           self._cb_value_changed)

        # Set focus on the input box.
        page_spinner.select_region(0, -1)
        page_spinner.grab_focus()

        # Currently displayed thumbnail page.
        self.__thumbnail_page = 0
        self.__threadpool = GlobalThreadPool.threadpool
        self._update_thumbnail(int(self.__selector_adjustment.props.value))

    def _cb_value_changed(self, *args):
        """
        Called whenever the spinbox value changes. Updates the preview thumbnail
        """

        page = int(self.__selector_adjustment.props.value)
        if page != self.__thumbnail_page:
            self._update_thumbnail(page)

    def _page_text_changed(self, control, *args):
        """
        Called when the page selector has been changed. Used to instantly update
        the preview thumbnail when entering page numbers by hand
        """

        if control.get_text().isdigit():
            page = int(control.get_text())
            if 0 < page <= self.__number_of_pages:
                control.set_value(page)

    def _response(self, widget, event, *args):
        if event == Gtk.ResponseType.OK:
            self.__window.set_page(int(self.__selector_adjustment.props.value))

        self.__events.remove_event(EventType.PAGE_AVAILABLE,
                                   self._page_available)
        self.__threadpool.renew()
        self.destroy()

    def _update_thumbnail(self, page: int):
        """
        Trigger a thumbnail update
        """

        width = self.__image_preview.get_allocation().width
        height = self.__image_preview.get_allocation().height
        self.__thumbnail_page = page
        self.__threadpool.apply_async(self._generate_thumbnail,
                                      args=(page, width, height),
                                      callback=self._generate_thumbnail_cb)

    def _generate_thumbnail(self, page: int, width: int, height: int):
        """
        Generate the preview thumbnail for the page selector.
        A transparent image will be used if the page is not yet available
        """

        return page, self.__image_handler.get_thumbnail(page=page,
                                                        size=(width, height))

    def _generate_thumbnail_cb(self, params):
        page, pixbuf = params
        return self._thumbnail_finished(page, pixbuf)

    def _thumbnail_finished(self, page: int, pixbuf):
        # Don't bother if we changed page in the meantime.
        if page == self.__thumbnail_page:
            self.__image_preview.set_from_pixbuf(pixbuf)

    def _page_available(self, page: int):
        if page == int(self.__selector_adjustment.props.value):
            self._update_thumbnail(page)
Пример #13
0
class FileHandler(metaclass=SingleInstanceMetaClass):
    """
    The FileHandler keeps track of the actual files/archives opened.
    While ImageHandler takes care of pages/images, this class provides
    the raw file names for archive members and image files, extracts
    archives, and lists directories for image files
    """

    def __init__(self, window: MainWindow):
        super().__init__()

        self.__events = Events()
        self.__events.add_event(EventType.FILE_EXTRACTED, self._extracted_file)
        self.__events.add_event(EventType.FILE_LISTED, self._listed_contents)

        self.__image_handler = ImageHandler()
        self.__image_files = ImageFiles()

        #: Indicates if files/archives are currently loaded/loading.
        self.__file_loaded = False
        self.__file_loading = False
        #: False if current file is not an archive, or unrecognized format.
        self.__is_archive = False

        #: Either path to the current archive, or first file in image list.
        #: This is B{not} the path to the currently open page.
        self.__current_file = None
        #: Reference to L{MainWindow}.
        self.__window = window
        #: Path to opened archive file, or directory containing current images.
        self.__base_path = None
        #: Archive extractor.
        self.__extractor = Extractor()
        #: Provides a list of available files/archives in the open directory.
        self.__file_provider_chooser = GetFileProvider()
        self.__file_provider = None

        self.__start_page = 1

        self.__open_first_page = None

        self.update_opening_behavior()

    def update_opening_behavior(self):
        self.__open_first_page = 1 if config['OPEN_FIRST_PAGE'] else -1

    def refresh_file(self, *args, **kwargs):
        """
        Closes the current file(s)/archive and reloads them
        """

        if not self.__file_loaded:
            return

        current_file = self.get_real_path()
        if self.__is_archive:
            start_page = self.__image_handler.get_current_page()
        else:
            start_page = 1
        self.open_file(current_file, start_page)

    def open_file_init(self, paths: list, start_page: int = 1):
        """
        Open the first file pointed to in <paths>,
        and initialize the file provider.
        """

        if not paths:
            return

        self._initialize_fileprovider(path=paths)
        self.open_file(paths[0], start_page)

    def open_file(self, path: Path, start_page: int = 1):
        """
        Open the file pointed to by <path>.
        If <start_page> is not set we set the current
        page to 1 (first page), if it is set we set the current page to the
        value of <start_page>. If <start_page> is non-positive it means the
        last image.
        """

        self._close()

        self.__is_archive = ArchiveSupported.is_archive_file(path)
        self.__start_page = start_page
        self.__current_file = path

        # Actually open the file(s)/archive passed in path.
        if self.__is_archive:
            self._open_archive(self.__current_file)
            self.__file_loading = True
        else:
            image_files = self.__file_provider.list_files(mode=FileTypes.IMAGES)
            self.__base_path = self.__file_provider.get_directory()
            self._archive_opened(image_files)

    def _archive_opened(self, image_files: list):
        """
        Called once the archive has been opened and its contents listed
        """

        self.__image_files.set_image_files(image_files)
        self.file_opened()

        if not image_files:
            logger.error(f'No images in "{self.__current_file.name}"')
            return

        if self.__is_archive:
            self.__extractor.extract()

            start_page = self.__start_page
        else:
            # If no extraction is required, mark all files as available.
            for img in image_files:
                self.__image_handler.file_available(img)

            if self.__current_file.is_dir():
                # if the current_file is a directory then start at the first image
                start_page = 1
            else:
                # Set start_page to the same as current_file.
                start_page = self.__image_files.get_page_from_path(self.__current_file)

        self.__window.set_page(start_page)

    def file_opened(self):
        """
        Called when a new set of files has successfully been opened
        """

        self.__file_loaded = True

        self.__events.run_events(EventType.FILE_OPENED)

    def file_closed(self):
        """
        Called when the current file has been closed
        """

        self.__events.run_events(EventType.FILE_CLOSED)

    def close_file(self, *args):
        """
        Close the currently opened file and its provider
        """

        self._close(close_provider=True)

    def _close(self, close_provider: bool = False):
        """
        Run tasks for "closing" the currently opened file(s)
        """

        if self.__file_loaded or self.__file_loading:
            if close_provider:
                self.__file_provider = None
            if self.__is_archive:
                self.__extractor.close()
            self.__image_handler.cleanup()
            self.__file_loaded = False
            self.__file_loading = False
            self.__is_archive = False
            self.__current_file = None
            self.__base_path = None
            self.file_closed()

    def _initialize_fileprovider(self, path: list):
        """
        Creates the L{file_provider.FileProvider} for C{path}.
        If C{path} is a list, assumes that only the files in the list
        should be available. If C{path} is a string, assume that it is
        either a directory or an image file, and all files in that directory should be opened.

        :param path: List of file names, or single file/directory as string.
        """

        self.__file_provider = self.__file_provider_chooser.get_file_provider(path)

    def _open_archive(self, path: Path):
        """
        Opens the archive passed in C{path}.
        Creates an L{archive_extractor.Extractor} and extracts all images
        found within the archive.

        :returns: A tuple containing C{(image_files, image_index)}
        """

        self.__base_path = path
        try:
            self.__extractor.setup(self.__base_path)
        except Exception as ex:
            logger.error(f'failed to open archive: {self.__base_path}')
            logger.error(f'Exception: {ex}')
            raise

    def _listed_contents(self, image_files: list):
        if not self.__file_loading:
            return
        self.__file_loading = False

        self._sort_archive_images(image_files)
        self._archive_opened(image_files)

    def _sort_archive_images(self, filelist: list):
        """
        Sorts the image list passed in C{filelist} based on the sorting preference option
        """

        # sort files
        match config['SORT_ARCHIVE_BY']:
            case FileSortType.NAME.value:
                SortAlphanumeric(filelist)
            case FileSortType.NAME_LITERAL.value:
                filelist.sort()

        # sort files order
        if config['SORT_ARCHIVE_ORDER'] == FileSortDirection.DESCENDING.value:
            filelist.reverse()

    def get_file_loaded(self):
        return self.__file_loaded

    def is_archive(self):
        return self.__is_archive

    def get_base_path(self):
        return self.__base_path

    def _get_file_list(self):
        return self.__file_provider.list_files(mode=FileTypes.ARCHIVES)

    def get_file_number(self):
        if not self.__is_archive:
            # No file numbers for images.
            return 0, 0

        file_list = self._get_file_list()
        current_index = file_list.index(self.__current_file)

        return current_index + 1, len(file_list)

    def get_real_path(self):
        """
        Return the "real" path to the currently viewed file, i.e. the
        full path to the archive or the full path to the currently viewed image
        """

        if self.__is_archive:
            return self.__base_path

        return self.__image_handler.get_path_to_page()

    def open_archive_direction(self, forward: bool, *args):
        """
        Opens the archive that comes directly after the currently loaded
        archive in that archive's directory listing if forward=True else
        opens the archive that comes directly before the currently loaded
        archive in that archive's directory listing. sorted alphabetically.

        :returns True if a new archive was opened, False otherwise
        """

        if not self.__is_archive:
            return False

        files = self._get_file_list()
        if self.__base_path not in files:
            return False

        current_index = files.index(self.__base_path)

        if forward:
            next_file = files[current_index + 1:]
            next_page = 1
        else:
            next_file = reversed(files[:current_index])
            next_page = self.__open_first_page

        for path in next_file:
            self._close()
            self.open_file(path, next_page)
            return True

        return False

    def _extracted_file(self, name: str):
        """
        Called when the extractor finishes extracting the file at
        <name>. This name is relative to the temporary directory
        the files were extracted to
        """

        if not self.__file_loaded:
            return
        self.__events.run_events(EventType.FILE_AVAILABLE, name)
Пример #14
0
class Extractor:
    """
    Extractor is a threaded class for extracting different archive formats.
    The Extractor can be loaded with paths to archives and a path to a
    destination directory. Once an archive has been set and its contents
    listed, it is possible to filter out the files to be extracted and set the
    order in which they should be extracted.  The extraction can then be
    started in a new thread in which files are extracted one by one, and a
    signal is sent on a condition after each extraction, so that it is possible
    for other threads to wait on specific files to be ready
    """
    def __init__(self):
        super().__init__()

        self.__events = Events()
        self.__threadpool = GlobalThreadPool.threadpool
        self.__extractor = None
        self.__condition = threading.Condition()

    def setup(self, archive: Path):
        """
        Setup the extractor with archive <src> and destination dir <dst>.
        Return a threading.Condition related to the is_ready() method, or
        None if the format of <src> isn't supported
        """

        self.__extractor = LibarchiveExtractor(archive)

        self.__threadpool.apply_async(self._list_contents,
                                      callback=self._list_contents_cb,
                                      error_callback=self._error_cb)

    def stop(self):
        """
        Signal the extractor to stop extracting and kill the extracting
        thread. Blocks until the extracting thread has terminated
        """

        self.__threadpool.terminate()
        self.__threadpool.join()
        self.__threadpool.renew()

    def extract(self):
        """
        Start extracting the files in the file list one by one using a
        new thread. Every time a new file is extracted a notify() will be
        signalled on the Condition that was returned by setup()
        """

        with self.__condition:
            self.__threadpool.apply_async(self._extract_all_files,
                                          error_callback=self._error_cb)

    def _file_listed(self, extractor, files: list):
        """
        Called after the contents of the archive has been listed
        """

        self.__events.run_events(EventType.FILE_LISTED, files)

    def _file_extracted(self, extractor, filename: Path):
        """
        Called whenever a new file is extracted and ready
        """

        self.__events.run_events(EventType.FILE_EXTRACTED, filename)

    def close(self):
        """
        Close any open file objects, need only be called manually if the
        extract() method isn't called
        """

        self.stop()
        if self.__extractor:
            self.__extractor.close()

    def _extraction_finished(self, name: Path):
        if self.__threadpool.closed:
            return True

        with self.__condition:
            self.__condition.notify_all()

        self._file_extracted(self, name)

    def _extract_all_files(self):
        for name in self.__extractor.iter_extract():
            if self._extraction_finished(name):
                return

    def _list_contents(self):
        return [
            Path(self.__extractor.destination_path, image)
            for image in self.__extractor.iter_contents()
        ]

    def _list_contents_cb(self, files: list):
        self._file_listed(self, files)

    def _error_cb(self, name, etype, value, tb):
        # Better to ignore any failed extractions (e.g. from a corrupt
        # archive) than to crash here and leave the main thread in a
        # possible infinite block. Damaged or missing files *should* be
        # handled gracefully by the main program anyway.

        logger.error(f'Extraction error: {value}')
        logger.error(f'Traceback:\n{"".join(traceback.format_tb(tb)).strip()}')
Пример #15
0
    def __init__(self, window: MainWindow):
        super().__init__()

        self.__window = window

        events = Events()
        events.add_event(EventType.PAGE_AVAILABLE, self._on_page_available)
        events.add_event(EventType.PAGE_CHANGED, self._on_page_change)

        self.__file_handler = FileHandler(None)
        self.__image_handler = ImageHandler()

        #: Thumbnail load status
        self.__loaded = False
        #: Selected row in treeview
        self.__currently_selected_row = 0

        self.__thumbnail_size = config['THUMBNAIL_SIZE'] + 2  # plus border
        self.__width_padding = self.__thumbnail_size + 10
        self.__empty_thumbnail = self._create_empty_thumbnail()

        self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.ALWAYS)
        # Disable stupid overlay scrollbars...
        self.set_overlay_scrolling(False)

        # models - contains data
        self.__thumbnail_liststore = Gtk.ListStore(int, GdkPixbuf.Pixbuf, bool)

        # view - responsible for laying out the columns
        self.__treeview = ThumbnailTreeView(
            self.__thumbnail_liststore,
            0,  # UID
            1,  # pixbuf
            2,  # status
        )
        self.__treeview.set_headers_visible(False)
        self.__treeview.generate_thumbnail = self._generate_thumbnail
        self.__treeview.set_activate_on_single_click(True)

        self.__treeview.connect_after('drag_begin', self._drag_begin)
        self.__treeview.connect('drag_data_get', self._drag_data_get)
        self.__treeview.connect('row-activated', self._row_activated_event)

        # enable drag and dropping of images from thumbnail bar to some file manager
        self.__treeview.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
                                                 [('text/uri-list', 0, 0)],
                                                 Gdk.DragAction.COPY)

        # Page column
        self.__thumbnail_page_treeviewcolumn = Gtk.TreeViewColumn(None)
        self.__treeview.append_column(self.__thumbnail_page_treeviewcolumn)
        self.__text_cellrenderer = Gtk.CellRendererText()
        # Right align page numbers.
        self.__text_cellrenderer.set_property('xalign', 1.0)
        self.__thumbnail_page_treeviewcolumn.set_sizing(
            Gtk.TreeViewColumnSizing.FIXED)
        self.__thumbnail_page_treeviewcolumn.pack_start(
            self.__text_cellrenderer, False)
        self.__thumbnail_page_treeviewcolumn.add_attribute(
            self.__text_cellrenderer, 'text', 0)
        self.__thumbnail_page_treeviewcolumn.set_visible(False)

        # Pixbuf column
        self.__thumbnail_image_treeviewcolumn = Gtk.TreeViewColumn(None)
        self.__treeview.append_column(self.__thumbnail_image_treeviewcolumn)
        self.__pixbuf_cellrenderer = Gtk.CellRendererPixbuf()
        self.__thumbnail_image_treeviewcolumn.set_sizing(
            Gtk.TreeViewColumnSizing.FIXED)
        self.__thumbnail_image_treeviewcolumn.set_fixed_width(
            self.__width_padding)
        self.__thumbnail_image_treeviewcolumn.pack_start(
            self.__pixbuf_cellrenderer, True)
        self.__thumbnail_image_treeviewcolumn.add_attribute(
            self.__pixbuf_cellrenderer, 'pixbuf', 1)

        self.__treeview.set_fixed_height_mode(True)
        self.__treeview.set_can_focus(False)

        self.add(self.__treeview)
        self.show_all()
Пример #16
0
    def __init__(self, window: MainWindow):
        super().__init__()

        self.set_transient_for(window)

        self.__window = window

        events = Events()
        events.add_event(EventType.FILE_CLOSED, self._on_book_close)
        events.add_event(EventType.PAGE_AVAILABLE, self._on_page_available)
        events.add_event(EventType.PAGE_CHANGED, self._on_page_change)

        self.__image_handler = ImageHandler()

        reset = Gtk.Button.new_with_label('Reset')
        self.add_action_widget(reset, Gtk.ResponseType.REJECT)
        save = Gtk.Button.new_with_label('Save')
        self.add_action_widget(save, Gtk.ResponseType.APPLY)
        self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)

        self.set_resizable(False)
        self.connect('response', self._response)
        self.set_default_response(Gtk.ResponseType.OK)

        self.__pixbuf = None

        vbox = Gtk.VBox(homogeneous=False, spacing=10)
        self.set_border_width(4)
        vbox.set_border_width(6)
        self.vbox.add(vbox)

        self.__hist_image = Gtk.Image()
        self.__hist_image.set_size_request(262, 170)
        vbox.pack_start(self.__hist_image, True, True, 0)
        vbox.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), True, True, 0)

        hbox = Gtk.HBox(homogeneous=False, spacing=4)
        vbox.pack_start(hbox, False, False, 2)
        vbox_left = Gtk.VBox(homogeneous=False, spacing=4)
        vbox_right = Gtk.VBox(homogeneous=False, spacing=4)
        hbox.pack_start(vbox_left, False, False, 2)
        hbox.pack_start(vbox_right, True, True, 2)

        def _create_scale(label_text: str, config_key: str):
            label = Gtk.Label(label=label_text)
            label.set_xalign(1.0)
            label.set_yalign(0.5)
            label.set_use_underline(True)
            vbox_left.pack_start(label, True, False, 2)
            adj = Gtk.Adjustment(value=0.0, lower=-1.0, upper=1.0, step_increment=0.01, page_increment=0.1)
            scale = Gtk.Scale.new(Gtk.Orientation.HORIZONTAL, adj)
            scale.set_digits(2)
            scale.set_value(config[config_key] - 1)
            scale.set_value_pos(Gtk.PositionType.RIGHT)
            scale.connect('value-changed', self._change_values)
            label.set_mnemonic_widget(scale)
            vbox_right.pack_start(scale, True, False, 2)
            return scale

        self.__brightness_scale = _create_scale('Brightness:', 'BRIGHTNESS')
        self.__contrast_scale = _create_scale('Contrast:', 'CONTRAST')
        self.__saturation_scale = _create_scale('Saturation:', 'SATURATION')
        self.__sharpness_scale = _create_scale('Sharpness:', 'SHARPNESS')

        vbox.pack_start(Gtk.Separator.new(Gtk.Orientation.HORIZONTAL), True, True, 0)

        self.__autocontrast_button = Gtk.CheckButton.new_with_mnemonic('_Automatically adjust contrast')
        vbox.pack_start(self.__autocontrast_button, False, False, 2)
        self.__autocontrast_button.connect('toggled', self._change_values)
        self.__autocontrast_button.set_active(config['AUTO_CONTRAST'])
        self.__contrast_scale.set_sensitive(not self.__autocontrast_button.get_active())

        self._on_page_change()

        self.show_all()