mstuttgart/pynocchio

View on GitHub
pynocchio/main_window_view.py

Summary

Maintainability
D
2 days
Test Coverage
import logging

from PyQt5 import QtCore, QtGui, QtWidgets

try:
    import qdarkgraystyle
except ImportError:
    pass

from .about_dialog import AboutDialog
from .bookmark import TemporaryBookmark
from .bookmark_manager_dialog import BookmarkManagerDialog
from .exception import (InvalidTypeFileException, LoadComicsException,
                        NoDataFindException)
from .go_to_page_dialog import GoToDialog
from .not_found_dialog import NotFoundDialog
from .thumbnails import ThumbnailsDock
from .uic_files import main_window_view_ui
from .utility import IMAGE_FILE_FORMATS, COMPACT_FILE_FORMATS, file_exist

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class MainWindowView(QtWidgets.QMainWindow):

    MAX_RECENT_FILES = 5
    MAX_BOOKMARK_FILES = 5

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

        self.ui = main_window_view_ui.Ui_MainWindowView()
        self.ui.setupUi(self)

        self.default_stylesheet = \
            QtWidgets.QApplication.instance().styleSheet()
        print(self.default_stylesheet)

        MainWindowView.MAX_RECENT_FILES = len(
            self.ui.menu_recent_files.actions())

        MainWindowView.MAX_BOOKMARK_FILES = \
            len(self.ui.menu_recent_bookmarks.actions())

        self.ui.menu_recent_files.menuAction().setVisible(False)

        self.thumbnails_dock = ThumbnailsDock()
        self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, self.thumbnails_dock)
        self.thumbnails_dock.visibilityChanged.connect(
            self.on_thumbnails_dock_changed)

        self.ui.qscroll_area_viewer.resized.connect(
            self.update_current_view_container_size)

        self.extra_shortcuts = self._define_extra_shortcuts()
        self.global_shortcuts = self._define_global_shortcuts()
        self.create_connections()
        self.centralize_window()

        self.update_recent_file_actions()

        self.update_settings()

        self.model.load_progress.connect(
            self.ui.statusbar.set_progressbar_value)

        self.vertical_animation = QtCore.QPropertyAnimation(
            self.ui.qscroll_area_viewer.verticalScrollBar())

        self.last_scroll_position = 0

    @QtCore.pyqtSlot()
    def on_action_open_file_triggered(self):
        cb_formats = ' '.join(['*' + cb for cb in COMPACT_FILE_FORMATS])
        img_formats = ' '.join(['*' + img for img in IMAGE_FILE_FORMATS])
        all_files = '%s %s' % (cb_formats, img_formats)

        filename = QtWidgets.QFileDialog().getOpenFileName(
            self, self.tr('Open Comic File'),
            self.model.current_directory,
            self.tr(
                'all supported files (%s);; '
                'zip files (*.zip *.cbz);; rar files (*.rar *.cbr);; '
                'tar files (*.tar *.cbt);; image files (%s);; '
                'all files (*)' % (all_files, img_formats)))

        if filename:
            logger.info('Opening file')

            initial_page = self.get_page_from_temporary_bookmarks(filename[0])

            self.open_comics(filename[0], initial_page)

    @QtCore.pyqtSlot()
    def on_action_save_image_triggered(self):
        img_formats = ' '.join(['*' + img for img in IMAGE_FILE_FORMATS])

        if self.model.comic:

            path = self.model.current_directory + \
                self.model.get_current_page_title()
            file_path = QtWidgets.QFileDialog().getSaveFileName(
                self, self.tr('Save Current Page'), path,
                self.tr('images (%s)' % (img_formats)))

            if file_path:
                logger.info('Saving image')
                self.model.save_current_page_image(file_path[0])

    @QtCore.pyqtSlot()
    def on_action_previous_page_triggered(self):
        if self.model.previous_page():
            self.update_viewer_content()
            self.update_navigation_actions()
            vert_scroll_bar = self.ui.qscroll_area_viewer.verticalScrollBar()
            vert_scroll_bar.setValue(self.last_scroll_position)
        elif self.ui.action_page_across_files.isChecked():
            self.on_action_previous_comic_triggered()
            self.on_action_last_page_triggered()

    @QtCore.pyqtSlot()
    def on_action_next_page_triggered(self):
        if self.model.next_page():
            vert_scroll_bar = self.ui.qscroll_area_viewer.verticalScrollBar()
            self.last_scroll_position = vert_scroll_bar.sliderPosition()
            self.update_viewer_content()
            self.update_navigation_actions()
        elif self.ui.action_page_across_files.isChecked():
            self.on_action_next_comic_triggered()

    @QtCore.pyqtSlot()
    def on_action_first_page_triggered(self):
        self.model.first_page()
        self.update_viewer_content()
        self.update_navigation_actions()

    @QtCore.pyqtSlot()
    def on_action_last_page_triggered(self):
        self.model.last_page()
        self.update_viewer_content()
        self.update_navigation_actions()

    @QtCore.pyqtSlot()
    def on_action_previous_comic_triggered(self):
        try:
            self.open_comics(self.model.previous_comic())
        except NoDataFindException as exc:
            logger.exception(exc.message)

        self.update_navigation_actions()

    @QtCore.pyqtSlot()
    def on_action_next_comic_triggered(self):
        try:
            self.open_comics(self.model.next_comic())
        except NoDataFindException as exc:
            logger.exception(exc.message)

        self.update_navigation_actions()

    @QtCore.pyqtSlot()
    def on_action_rotate_left_triggered(self):
        self.model.rotate_left()
        self.update_viewer_content()

    @QtCore.pyqtSlot()
    def on_action_rotate_right_triggered(self):
        self.model.rotate_right()
        self.update_viewer_content()

    @QtCore.pyqtSlot()
    def on_action_go_to_page_triggered(self):
        go_to_dlg = GoToDialog(self.model.comic_page_handler, parent=self)
        go_to_dlg.show()
        ret = go_to_dlg.exec_()

        if ret == QtWidgets.QDialog.Accepted:
            self._go_to_page(go_to_dlg.handler.current_page_index)

    def _go_to_page(self, idx):
        self.model.set_current_page_index(idx)
        self.update_viewer_content()
        self.update_navigation_actions()

    @QtCore.pyqtSlot()
    def on_action_add_bookmark_triggered(self):
        self.model.add_bookmark()
        self.update_bookmark_actions()

    @QtCore.pyqtSlot()
    def on_action_remove_bookmark_triggered(self):
        self.model.remove_bookmark(self.model.get_comic_path())
        self.update_bookmark_actions()

    @QtCore.pyqtSlot()
    def on_action_bookmark_manager_triggered(self):
        bookmark_dialog = BookmarkManagerDialog(self, parent=self)
        bookmark_dialog.show()
        bookmark_dialog.exec_()

    @QtCore.pyqtSlot()
    def on_action_preference_dialog_triggered(self):
        pass

    @QtCore.pyqtSlot()
    def on_action_original_fit_triggered(self):
        self.model.original_fit()
        self.update_viewer_content()

    @QtCore.pyqtSlot()
    def on_action_vertical_fit_triggered(self):
        self.model.vertical_fit()
        self.update_viewer_content()

    @QtCore.pyqtSlot()
    def on_action_horizontal_fit_triggered(self):
        self.model.horizontal_fit()
        self.update_viewer_content()

    @QtCore.pyqtSlot()
    def on_action_best_fit_triggered(self):
        self.model.best_fit()
        self.update_viewer_content()

    @QtCore.pyqtSlot()
    def on_action_page_fit_triggered(self):
        self.model.page_fit()
        self.update_viewer_content()

    @QtCore.pyqtSlot()
    def on_action_fullscreen_triggered(self):

        if self.isFullScreen():
            self.ui.menubar.show()
            if self.ui.action_show_toolbar.isChecked():
                self.ui.toolbar.show()
            if self.ui.action_show_statusbar.isChecked():
                self.ui.statusbar.show()
            self.showNormal()

            for sc in self.global_shortcuts:
                sc.setEnabled(False)
        else:
            self.ui.menubar.hide()
            self.ui.toolbar.hide()
            self.ui.statusbar.hide()
            self.showFullScreen()
            for sc in self.global_shortcuts:
                sc.setEnabled(True)

    @QtCore.pyqtSlot(bool)
    def on_action_double_page_mode_triggered(self, checked):
        self.model.double_page_mode(checked)
        self.update_viewer_content()
        self.ui.action_manga_mode.setEnabled(checked)

    @QtCore.pyqtSlot(bool)
    def on_action_manga_mode_triggered(self, checked):
        self.model.manga_page_mode(checked)
        self.update_viewer_content()

    @QtCore.pyqtSlot()
    def on_action_show_toolbar_triggered(self):
        if self.ui.action_show_toolbar.isChecked():
            self.ui.toolbar.show()
        else:
            self.ui.toolbar.hide()

    @QtCore.pyqtSlot()
    def on_action_show_statusbar_triggered(self):
        if self.ui.action_show_statusbar.isChecked():
            self.ui.statusbar.show()
            self.update_status_bar()
        else:
            self.ui.statusbar.hide()

    @QtCore.pyqtSlot()
    def on_action_show_thumbnails_triggered(self):
        if self.ui.action_show_thumbnails.isChecked():
            self.thumbnails_dock.show()
        else:
            self.thumbnails_dock.hide()

    @QtCore.pyqtSlot()
    def on_action_shrink_only_triggered(self):
        self.model.resize_always = not self.ui.action_shrink_only.isChecked()
        self.update_viewer_content()

    @QtCore.pyqtSlot()
    def on_action_about_triggered(self):
        ab_dlg = AboutDialog(self)
        ab_dlg.show()
        ab_dlg.exec_()

    @QtCore.pyqtSlot()
    def on_action_about_qt_triggered(self):
        QtWidgets.QMessageBox().aboutQt(self, self.tr('About Qt'))

    @QtCore.pyqtSlot()
    def on_action_exit_triggered(self):
        self.close()

    @QtCore.pyqtSlot()
    def on_thumbnails_dock_changed(self):
        self.ui.action_show_thumbnails.setChecked(
            self.thumbnails_dock.isVisible())

    @QtCore.pyqtSlot(bool)
    def on_action_dark_style_triggered(self):
        qApp = QtWidgets.QApplication.instance()
        if self.ui.action_dark_style.isChecked():
            try:
                qApp.setStyleSheet(qdarkgraystyle.load_stylesheet())
            except NameError:
                pass
        else:
            qApp.setStyleSheet(self.default_stylesheet)

    def create_connections(self):

        # Define group to action fit items and load fit of settings file
        self.ui.action_group_view = QtWidgets.QActionGroup(self)

        self.ui.action_group_view.addAction(self.ui.action_original_fit)
        self.ui.action_group_view.addAction(self.ui.action_vertical_fit)
        self.ui.action_group_view.addAction(self.ui.action_horizontal_fit)
        self.ui.action_group_view.addAction(self.ui.action_best_fit)
        self.ui.action_group_view.addAction(self.ui.action_page_fit)

        view_adjust = self.model.load_view_adjust(
            self.ui.action_group_view.checkedAction().objectName())

        # Define that action fit is checked
        for act in self.ui.action_group_view.actions():
            if act.objectName() == view_adjust:
                act.setChecked(True)
                self.model.fit_type = act.objectName()

        # Connect recent file menu
        for act in self.ui.menu_recent_files.actions():
            act.triggered.connect(self.open_recent_file)

        # Connect recent bookmark menu
        for act in self.ui.menu_recent_bookmarks.actions():
            act.triggered.connect(self.open_recent_bookmark)

        # update recent bookmark menu when mouse hover
        self.ui.menu_recent_bookmarks.aboutToShow.connect(
            self.update_recent_bookmarks_menu)

    def _define_extra_shortcuts(self):

        shortcuts = []

        sequence = {
            'Up': self.on_action_previous_page_triggered,
            'PgUp': self.on_action_previous_page_triggered,
            'Down': self.on_action_next_page_triggered,
            'PgDown': self.on_action_next_page_triggered,
            'Ctrl+Up': self.on_action_first_page_triggered,
            'Home': self.on_action_first_page_triggered,
            'Ctrl+Down': self.on_action_last_page_triggered,
            'End': self.on_action_last_page_triggered,
        }

        for key, value in list(sequence.items()):
            s = QtWidgets.QShortcut(QtGui.QKeySequence(key),
                                    self.ui.qscroll_area_viewer, value)
            s.setEnabled(True)

        return shortcuts

    def _define_global_shortcuts(self):

        shortcuts = []

        sequence = {
            'Esc': self.on_action_fullscreen_triggered,
        }

        for key, value in list(sequence.items()):
            s = QtWidgets.QShortcut(QtGui.QKeySequence(key),
                                    self.ui.qscroll_area_viewer, value)
            s.setEnabled(False)
            shortcuts.append(s)

        return shortcuts

    def get_page_from_temporary_bookmarks(self, path):

        bk = self.model.get_bookmark_from_path(path=path,
                                               table=TemporaryBookmark)
        initial_page = None

        if bk:
            msg = QtWidgets.QMessageBox()
            msg.setWindowTitle(self.tr(
                'Continue reading from page %d?' % bk.comic_page))
            msg.setIcon(QtWidgets.QMessageBox.Question)
            msg.setText(self.tr('<p>You stopped reading here.</p>'
                                '<p> If you choose <b>"Yes"</b>, reading will '
                                'resume on <b>page %d</b>. </p>'
                                '<p>Otherwise, the first page will be '
                                'loaded.</p>'
                                % bk.comic_page))
            msg.setStandardButtons(
                QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
            msg.setDefaultButton(QtWidgets.QMessageBox.Ok)
            q = QtGui.QPixmap()
            q.loadFromData(bk.page_data)
            q = q.scaledToWidth(msg.width() * 0.2,
                                QtCore.Qt.SmoothTransformation)
            msg.setIconPixmap(q)

            option = msg.exec_()

            if option == QtWidgets.QMessageBox.Ok:
                initial_page = bk.comic_page - 1

        return initial_page

    def open_comics(self, filename, initial_page=None):
        if filename:
            logger.info('Opening comic %s', filename)

            try:

                # Load comic
                self.model.load(filename, initial_page)

                # Update label and scroll_area_viewer
                self.update_viewer_content()

                # set window title
                self.setWindowTitle(self.model.get_comic_title())

                # Enable window actions
                self.enable_actions()

                # Update status bar data
                self.update_status_bar()

                # Add this comic like recent file
                self.set_current_file(filename)

                # Update status of add and remove bookmark buttons
                self.update_bookmark_actions()

                # Update next page, previous page, next and previous comics
                # actions
                self.update_navigation_actions()

                self.update_thumbnails()

                # Register view like listener of ComicPageHandler events
                # self.model.comic_page_handler.listener.append(self)

                return True

            except LoadComicsException as excp:
                QtWidgets.QMessageBox().warning(self,
                                                'LoadComicsException',
                                                self.tr(excp.message),
                                                QtWidgets.QMessageBox.Close)
            except InvalidTypeFileException as excp:
                QtWidgets.QMessageBox().warning(self,
                                                'InvalidTypeFileException',
                                                self.tr(excp.message),
                                                QtWidgets.QMessageBox.Close)

        return False

    def open_recent_file(self):
        action = self.sender()
        if action:
            filename = action.data()
            if file_exist(filename):
                initial_page = self.get_page_from_temporary_bookmarks(filename)
                self.open_comics(filename, initial_page=initial_page)
            else:
                files = self.model.load_recent_files()
                files.remove(filename)
                self.model.save_recent_files(files)
                self.update_recent_file_actions()
                not_found_dialog = NotFoundDialog(self)
                not_found_dialog.show()
                not_found_dialog.exec_()

    def set_current_file(self, filename):

        # Load recent files list
        files = self.model.load_recent_files()

        try:
            # Remove the current file from recent file list
            files.remove(filename)
        except ValueError:
            pass

        # Insert it on top of recent file list
        files.insert(0, filename)
        del files[MainWindowView.MAX_RECENT_FILES:]

        # Save recent file list
        self.model.save_recent_files(files)

        # Update text and data of recent file actions
        self.update_recent_file_actions()

    def update_recent_file_actions(self):

        files = self.model.load_recent_files()
        num_recent_files = len(files) if files else 0
        num_recent_files = min(num_recent_files,
                               MainWindowView.MAX_RECENT_FILES)

        self.ui.menu_recent_files.menuAction().setVisible(True if files else
                                                          False)
        recent_file_actions = self.ui.menu_recent_files.actions()

        for i in range(num_recent_files):
            text = QtCore.QFileInfo(files[i]).fileName()
            recent_file_actions[i].setText(text)
            recent_file_actions[i].setData(files[i])
            recent_file_actions[i].setVisible(True)
            recent_file_actions[i].setStatusTip(files[i])

        for j in range(num_recent_files, MainWindowView.MAX_RECENT_FILES):
            recent_file_actions[j].setVisible(False)

    def update_bookmark_actions(self):
        is_bookmark = self.model.is_bookmark()
        self.ui.action_remove_bookmark.setVisible(is_bookmark)
        self.ui.action_add_bookmark.setVisible(not is_bookmark)

        bookmark_list = self.model.get_bookmark_list(
            MainWindowView.MAX_BOOKMARK_FILES)
        self.ui.menu_recent_bookmarks.menuAction().setVisible(
            True if bookmark_list else False)

    def update_recent_bookmarks_menu(self):

        bk_actions = self.ui.menu_recent_bookmarks.actions()
        bookmark_list = self.model.get_bookmark_list(len(bk_actions))

        num_bookmarks_files = len(bookmark_list) if bookmark_list else 0
        num_bookmarks_files = min(num_bookmarks_files,
                                  MainWindowView.MAX_BOOKMARK_FILES)

        for i in range(num_bookmarks_files):
            bk_text = '%s [%d]' % (bookmark_list[i].comic_name,
                                   bookmark_list[i].comic_page)
            bk_actions[i].setText(bk_text)
            bk_actions[i].setData(bookmark_list[i].comic_page)
            bk_actions[i].setStatusTip(bookmark_list[i].comic_path)
            bk_actions[i].setVisible(True)

        for j in range(num_bookmarks_files, MainWindowView.MAX_BOOKMARK_FILES):
            bk_actions[j].setVisible(False)

    def update_settings(self):
        settings = self.model.load_toggles()
        self.ui.action_show_toolbar.setChecked(settings['show_toolbar'])
        self.on_action_show_toolbar_triggered()
        self.ui.action_show_statusbar.setChecked(settings['show_statusbar'])
        self.on_action_show_statusbar_triggered()
        self.ui.action_show_thumbnails.setChecked(settings['show_thumbnails'])
        self.on_action_show_thumbnails_triggered()
        self.ui.action_shrink_only.setChecked(settings['shrink_only'])
        self.on_action_shrink_only_triggered()
        self.ui.action_page_across_files.setChecked(
            settings['page_across_files'])
        self.ui.action_dark_style.setChecked(settings['dark_style'])
        self.on_action_dark_style_triggered()

    def update_thumbnails(self):
        self.thumbnails_dock.clear()
        num = self.model.get_current_page_number()-1
        self.thumbnails_dock.populate(current=num)

    def open_recent_bookmark(self):
        action = self.sender()
        if action:
            filename = action.statusTip()
            if file_exist(filename):
                self.open_comics(action.statusTip(), action.data() - 1)
            else:
                self.model.remove_bookmark(action.statusTip())
                self.update_bookmark_actions()

    def enable_actions(self):

        action_list = self.ui.menu_file.actions()
        action_list += self.ui.menu_view.actions()
        action_list += self.ui.menu_navigation.actions()
        action_list += self.ui.menu_bookmarks.actions()

        for action in action_list:
            action.setEnabled(True)

    def update_navigation_actions(self):

        # is_first_page = self.model.is_first_page()
        # is_last_page = self.model.is_last_page()

        # self.ui.action_previous_page.setEnabled(
        #     not self.model.is_first_comic())
        # self.ui.action_first_page.setEnabled(not self.model.is_first_comic())
        #
        # self.ui.action_next_page.setEnabled(not self.model.is_last_comic())
        # self.ui.action_last_page.setEnabled(not self.model.is_last_comic())

        self.ui.action_previous_comic.setEnabled(
            not self.model.is_first_comic())

        self.ui.action_next_comic.setEnabled(
            not self.model.is_last_comic())

    def update_status_bar(self):

        if self.model.comic:
            page_number = self.model.get_current_page_number()
            total_pages = self.model.get_number_of_pages()
            page_width = self.model.get_current_page().width()
            page_height = self.model.get_current_page().height()
            original_width = self.model.get_current_page().original_width
            original_height = self.model.get_current_page().original_height
            page_title = self.model.get_current_page_title()

            if self.ui.statusbar.isVisible():
                self.ui.statusbar.set_comic_page(page_number, total_pages)
                self.ui.statusbar.set_page_resolution(
                    page_width, page_height, original_width, original_height)
                self.ui.statusbar.set_comic_path(page_title)

    def centralize_window(self):
        screen = QtWidgets.QDesktopWidget().screenGeometry()
        size = self.geometry()
        x_center = (screen.width() - size.width()) / 2
        y_center = (screen.height() - size.height()) / 2
        self.move(x_center, y_center)
        size = self.size()
        pos = self.pos()
        size, pos, state = self.model.load_window(size, pos)
        self.resize(size)
        self.move(pos)
        if (state):
            self.restoreState(state)

    def update_viewer_content(self):
        content = self.model.get_current_page()

        if content:
            self.ui.label.setPixmap(content)
            self.thumbnails_dock.highlight(
                self.model.get_current_page_number()-1)
            self.ui.qscroll_area_viewer.reset_scroll_position()
            self.update_status_bar()

    def update_current_view_container_size(self):
        margins = (self.ui.qscroll_area_viewer.size() -
                   self.ui.qscroll_area_viewer.contentsRect().size())
        self.model.scroll_area_size = \
            self.ui.qscroll_area_viewer.size() - 2*margins
        self.model.scroll_bar_size = \
            self.ui.qscroll_area_viewer.style().pixelMetric(
                QtWidgets.QStyle.PM_ScrollBarExtent)
        self.update_viewer_content()

    def keyPressEvent(self, event):

        if event.key() == QtCore.Qt.Key_F:
            self.on_action_fullscreen_triggered()

        elif event.key() == QtCore.Qt.Key_Up:
            vert_scroll_bar = self.ui.qscroll_area_viewer.verticalScrollBar()
            next_pos = vert_scroll_bar.sliderPosition() - self.height() * 0.8

            self.vertical_animation.setDuration(250)
            self.vertical_animation.setStartValue(
                vert_scroll_bar.sliderPosition())
            self.vertical_animation.setEndValue(next_pos)
            self.vertical_animation.start()

        elif event.key() == QtCore.Qt.Key_Down:
            vert_scroll_bar = self.ui.qscroll_area_viewer.verticalScrollBar()
            next_pos = vert_scroll_bar.sliderPosition() + self.height() * 0.8

            self.vertical_animation.setDuration(250)
            self.vertical_animation.setStartValue(
                vert_scroll_bar.sliderPosition())
            self.vertical_animation.setEndValue(next_pos)
            self.vertical_animation.start()

        super(MainWindowView, self).keyPressEvent(event)

    def mouseDoubleClickEvent(self, event):
        if event.button() == QtCore.Qt.LeftButton:
            self.on_action_fullscreen_triggered()
        super(MainWindowView, self).mousePressEvent(event)

    def contextMenuEvent(self, event):
        self.ui.menu_context.exec(event.globalPos())
        super(MainWindowView, self).contextMenuEvent(event)

    def wheelEvent(self, event):
        if event.angleDelta().y() < 0:
            self.on_action_next_page_triggered()
        else:
            self.on_action_previous_page_triggered()
        event.accept()

    def closeEvent(self, event):
        self.model.save_settings()

        try:
            if self.model.is_first_page() or self.model.is_last_page():
                self.model.remove_bookmark(table=TemporaryBookmark)
            else:
                self.model.add_bookmark(table=TemporaryBookmark)
        except AttributeError as exc:
            logger.warning(exc)

        super(MainWindowView, self).close()
        event.accept()