Example #1
0
class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
    def __init__(self):
        super(MainUI, self).__init__()
        self.setupUi(self)

        # Set window icon
        self.setWindowIcon(
            QtGui.QIcon(':/images/Lector.png'))

        # Central Widget - Make borders disappear
        self.centralWidget().layout().setContentsMargins(0, 0, 0, 0)
        self.gridLayout_2.setContentsMargins(0, 0, 0, 0)

        # Initialize translation function
        self._translate = QtCore.QCoreApplication.translate

        # Create library widgets
        self.listView = DragDropListView(self, self.listPage)
        self.gridLayout_4.addWidget(self.listView, 0, 0, 1, 1)

        self.tableView = DragDropTableView(self, self.tablePage)
        self.gridLayout_3.addWidget(self.tableView, 0, 0, 1, 1)

        # Empty variables that will be infested soon
        self.settings = {}
        self.thread = None  # Background Thread
        self.current_contentView = None  # For fullscreening purposes
        self.display_profiles = None
        self.current_profile_index = None
        self.comic_profile = {}
        self.database_path = None
        self.active_library_filters = []
        self.active_docks = []

        # Initialize application
        Settings(self).read_settings()  # This should populate all variables that need
                                        # to be remembered across sessions

        # Initialize icon factory
        self.QImageFactory = QImageFactory(self)

        # Initialize toolbars
        self.libraryToolBar = LibraryToolBar(self)
        self.bookToolBar = BookToolBar(self)

        # Widget declarations
        self.libraryFilterMenu = QtWidgets.QMenu()
        self.statusMessage = QtWidgets.QLabel()

        # Reference variables
        self.alignment_dict = {
            'left': self.bookToolBar.alignLeft,
            'right': self.bookToolBar.alignRight,
            'center': self.bookToolBar.alignCenter,
            'justify': self.bookToolBar.alignJustify}

        # Create the database in case it doesn't exist
        database.DatabaseInit(self.database_path)

        # Initialize settings dialog
        self.settingsDialog = SettingsUI(self)

        # Initialize metadata dialog
        self.metadataDialog = MetadataUI(self)

        # Initialize definition view dialog
        self.definitionDialog = DefinitionsUI(self)

        # Make the statusbar invisible by default
        self.statusBar.setVisible(False)

        # Statusbar widgets
        self.statusMessage.setObjectName('statusMessage')
        self.statusBar.addPermanentWidget(self.statusMessage)
        self.errorButton = QtWidgets.QPushButton(self.statusBar)
        self.errorButton.setIcon(QtGui.QIcon(':/images/error.svg'))
        self.errorButton.setFlat(True)
        self.errorButton.setVisible(False)
        self.errorButton.setToolTip('What hast thou done?')
        self.errorButton.clicked.connect(self.show_errors)
        self.statusBar.addPermanentWidget(self.errorButton)
        self.sorterProgress = QtWidgets.QProgressBar()
        self.sorterProgress.setMaximumWidth(300)
        self.sorterProgress.setObjectName('sorterProgress')
        sorter.progressbar = self.sorterProgress  # This is so that updates can be
                                                  # connected to setValue
        self.statusBar.addWidget(self.sorterProgress)
        self.sorterProgress.setVisible(False)

        # Application wide temporary directory
        self.temp_dir = QtCore.QTemporaryDir()

        # Init the Library
        self.lib_ref = Library(self)

        # Initialize Cover loading functions
        # Must be after the Library init
        self.cover_functions = CoverLoadingAndCulling(self)

        # Init the culling timer
        self.culling_timer = QtCore.QTimer()
        self.culling_timer.setSingleShot(True)
        self.culling_timer.timeout.connect(self.cover_functions.cull_covers)

        # Initialize profile modification functions
        self.profile_functions = ViewProfileModification(self)

        # Toolbar display
        # Maybe make this a persistent option
        self.settings['show_bars'] = True

        # Library toolbar
        self.libraryToolBar.addButton.triggered.connect(self.add_books)
        self.libraryToolBar.deleteButton.triggered.connect(self.delete_books)
        self.libraryToolBar.coverViewButton.triggered.connect(self.switch_library_view)
        self.libraryToolBar.tableViewButton.triggered.connect(self.switch_library_view)
        self.libraryToolBar.reloadLibraryButton.triggered.connect(
            self.settingsDialog.start_library_scan)
        self.libraryToolBar.colorButton.triggered.connect(self.get_color)
        self.libraryToolBar.settingsButton.triggered.connect(
            lambda: self.show_settings(0))
        self.libraryToolBar.aboutButton.triggered.connect(
            lambda: self.show_settings(3))
        self.libraryToolBar.searchBar.textChanged.connect(self.lib_ref.update_proxymodels)
        self.libraryToolBar.sortingBox.activated.connect(self.lib_ref.update_proxymodels)
        self.libraryToolBar.libraryFilterButton.setPopupMode(QtWidgets.QToolButton.InstantPopup)
        self.libraryToolBar.searchBar.textChanged.connect(self.statusbar_visibility)
        self.addToolBar(self.libraryToolBar)

        if self.settings['current_view'] == 0:
            self.libraryToolBar.coverViewButton.trigger()
        else:
            self.libraryToolBar.tableViewButton.trigger()

        # Book toolbar
        self.bookToolBar.addBookmarkButton.triggered.connect(
            lambda: self.tabWidget.currentWidget().sideDock.bookmarks.add_bookmark())
        self.bookToolBar.bookmarkButton.triggered.connect(
            lambda: self.tabWidget.currentWidget().toggle_side_dock(0))
        self.bookToolBar.annotationButton.triggered.connect(
            lambda: self.tabWidget.currentWidget().toggle_side_dock(1))
        self.bookToolBar.searchButton.triggered.connect(
            lambda: self.tabWidget.currentWidget().toggle_side_dock(2))
        self.bookToolBar.distractionFreeButton.triggered.connect(
            self.toggle_distraction_free)
        self.bookToolBar.fullscreenButton.triggered.connect(
            lambda: self.tabWidget.currentWidget().go_fullscreen())

        self.bookToolBar.doublePageButton.triggered.connect(self.change_page_view)
        self.bookToolBar.mangaModeButton.triggered.connect(self.change_page_view)
        self.bookToolBar.invertButton.triggered.connect(self.change_page_view)
        if self.settings['double_page_mode']:
            self.bookToolBar.doublePageButton.setChecked(True)
        if self.settings['manga_mode']:
            self.bookToolBar.mangaModeButton.setChecked(True)
        if self.settings['invert_colors']:
            self.bookToolBar.invertButton.setChecked(True)

        for count, i in enumerate(self.display_profiles):
            self.bookToolBar.profileBox.setItemData(count, i, QtCore.Qt.UserRole)
        self.bookToolBar.profileBox.currentIndexChanged.connect(
            self.profile_functions.format_contentView)
        self.bookToolBar.profileBox.setCurrentIndex(self.current_profile_index)

        self.bookToolBar.fontBox.currentFontChanged.connect(self.modify_font)
        self.bookToolBar.fontSizeBox.currentIndexChanged.connect(self.modify_font)
        self.bookToolBar.lineSpacingUp.triggered.connect(self.modify_font)
        self.bookToolBar.lineSpacingDown.triggered.connect(self.modify_font)
        self.bookToolBar.paddingUp.triggered.connect(self.modify_font)
        self.bookToolBar.paddingDown.triggered.connect(self.modify_font)
        self.bookToolBar.resetProfile.triggered.connect(
            self.profile_functions.reset_profile)

        profile_index = self.bookToolBar.profileBox.currentIndex()
        current_profile = self.bookToolBar.profileBox.itemData(
            profile_index, QtCore.Qt.UserRole)
        for i in self.alignment_dict.items():
            i[1].triggered.connect(self.modify_font)
        self.alignment_dict[current_profile['text_alignment']].setChecked(True)

        self.bookToolBar.zoomIn.triggered.connect(
            self.modify_comic_view)
        self.bookToolBar.zoomOut.triggered.connect(
            self.modify_comic_view)
        self.bookToolBar.fitWidth.triggered.connect(
            lambda: self.modify_comic_view(False))
        self.bookToolBar.bestFit.triggered.connect(
            lambda: self.modify_comic_view(False))
        self.bookToolBar.originalSize.triggered.connect(
            lambda: self.modify_comic_view(False))
        self.bookToolBar.comicBGColor.clicked.connect(
            self.get_color)

        self.bookToolBar.colorBoxFG.clicked.connect(self.get_color)
        self.bookToolBar.colorBoxBG.clicked.connect(self.get_color)
        self.bookToolBar.tocBox.currentIndexChanged.connect(self.set_toc_position)
        self.addToolBar(self.bookToolBar)

        # Make the correct toolbar visible
        self.current_tab = self.tabWidget.currentIndex()
        self.tab_switch()
        self.tabWidget.currentChanged.connect(self.tab_switch)

        # Tab Widget formatting
        self.tabWidget.setTabsClosable(True)
        self.tabWidget.setDocumentMode(True)
        self.tabWidget.tabBarClicked.connect(self.tab_disallow_library_movement)

        # Get list of available parsers
        self.available_parsers = '*.' + ' *.'.join(sorter.available_parsers)
        logger.info('Available parsers: ' + self.available_parsers)

        # The Library tab gets no button
        self.tabWidget.tabBar().setTabButton(
            0, QtWidgets.QTabBar.RightSide, None)
        self.tabWidget.widget(0).is_library = True
        self.tabWidget.tabCloseRequested.connect(self.tab_close)
        self.tabWidget.setTabBarAutoHide(True)

        # Init display models
        self.lib_ref.generate_model('build')
        self.lib_ref.generate_proxymodels()
        self.lib_ref.generate_library_tags()
        self.set_library_filter()
        self.start_culling_timer()

        # ListView
        self.listView.setGridSize(QtCore.QSize(175, 240))
        self.listView.setMouseTracking(True)
        self.listView.verticalScrollBar().setSingleStep(9)
        self.listView.doubleClicked.connect(self.library_doubleclick)
        self.listView.setItemDelegate(LibraryDelegate(self.temp_dir.path(), self))
        self.listView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.listView.customContextMenuRequested.connect(self.generate_library_context_menu)
        self.listView.verticalScrollBar().valueChanged.connect(self.start_culling_timer)
        self.listView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
        self.listView.setAcceptDrops(True)

        self.listView.setStyleSheet(
            "QListView {{background-color: {0}}}".format(
                self.settings['listview_background'].name()))

        # TODO
        # Maybe use this for readjusting the border of the focus rectangle
        # in the listView. Maybe this is a job for QML?

        # self.listView.setStyleSheet(
        #     "QListView::item:selected { border-color:blue; border-style:outset;"
        #     "border-width:2px; color:black; }")

        # TableView
        self.tableView.doubleClicked.connect(self.library_doubleclick)
        self.tableView.horizontalHeader().setSectionResizeMode(
            QtWidgets.QHeaderView.Interactive)
        self.tableView.horizontalHeader().setSortIndicator(
            2, QtCore.Qt.AscendingOrder)
        self.tableView.setColumnHidden(0, True)
        self.tableView.horizontalHeader().setHighlightSections(False)
        if self.settings['main_window_headers']:
            for count, i in enumerate(self.settings['main_window_headers']):
                self.tableView.horizontalHeader().resizeSection(count, int(i))
        self.tableView.horizontalHeader().resizeSection(5, 30)
        self.tableView.horizontalHeader().setStretchLastSection(True)
        self.tableView.horizontalHeader().sectionClicked.connect(
            self.lib_ref.tableProxyModel.sort_table_columns)
        self.lib_ref.tableProxyModel.sort_table_columns(2)
        self.tableView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.tableView.customContextMenuRequested.connect(
            self.generate_library_context_menu)

        # Keyboard shortcuts
        self.ksDistractionFree = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+D'), self)
        self.ksDistractionFree.setContext(QtCore.Qt.ApplicationShortcut)
        self.ksDistractionFree.activated.connect(self.toggle_distraction_free)

        self.ksOpenFile = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+O'), self)
        self.ksOpenFile.setContext(QtCore.Qt.ApplicationShortcut)
        self.ksOpenFile.activated.connect(self.add_books)

        self.ksExitAll = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+Q'), self)
        self.ksExitAll.setContext(QtCore.Qt.ApplicationShortcut)
        self.ksExitAll.activated.connect(self.closeEvent)

        self.ksCloseTab = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+W'), self)
        self.ksCloseTab.setContext(QtCore.Qt.ApplicationShortcut)
        self.ksCloseTab.activated.connect(self.tab_close)

        self.ksDeletePressed = QtWidgets.QShortcut(QtGui.QKeySequence('Delete'), self)
        self.ksDeletePressed.setContext(QtCore.Qt.ApplicationShortcut)
        self.ksDeletePressed.activated.connect(self.delete_pressed)

        self.listView.setFocus()
        self.open_books_at_startup()

        # Scan the library @ startup
        if self.settings['scan_library']:
            self.settingsDialog.start_library_scan()

    def open_books_at_startup(self):
        # Last open books and command line books aren't being opened together
        # so that command line books are processed last and therefore retain focus

        # Open last... open books.
        # Then set the value to None for the next run
        if self.settings['last_open_books']:
            files_to_open = {i: None for i in self.settings['last_open_books']}
            self.open_files(files_to_open)
        else:
            self.settings['last_open_tab'] = None

        # Open input files if specified
        cl_parser = QtCore.QCommandLineParser()
        cl_parser.process(QtWidgets.qApp)
        my_args = cl_parser.positionalArguments()
        if my_args:
            file_list = [QtCore.QFileInfo(i).absoluteFilePath() for i in my_args]
            self.process_post_hoc_files(file_list, True)

    def process_post_hoc_files(self, file_list, open_files_after_processing):
        # Takes care of both dragged and dropped files
        # As well as files sent as command line arguments
        file_list = [i for i in file_list if os.path.exists(i)]
        if not file_list:
            return

        books = sorter.BookSorter(
            file_list,
            ('addition', 'manual'),
            self.database_path,
            self.settings,
            self.temp_dir.path())

        parsed_books, errors = books.initiate_threads()
        if not parsed_books and not open_files_after_processing:
            return

        database.DatabaseFunctions(self.database_path).add_to_database(parsed_books)
        self.lib_ref.generate_model('addition', parsed_books, True)

        file_dict = {i: None for i in file_list}
        if open_files_after_processing:
            self.open_files(file_dict)

        self.move_on(errors)

    def open_files(self, path_hash_dictionary):
        # file_paths is expected to be a dictionary
        # This allows for threading file opening
        # Which should speed up multiple file opening
        # especially @ application start
        file_paths = [i for i in path_hash_dictionary]

        for filename in path_hash_dictionary.items():

            file_md5 = filename[1]
            if not file_md5:
                try:
                    with open(filename[0], 'rb') as current_book:
                        first_bytes = current_book.read(1024 * 32)  # First 32KB of the file
                        file_md5 = hashlib.md5(first_bytes).hexdigest()
                except FileNotFoundError:
                    return

            # Remove any already open files
            # Set focus to last file in case only one is open
            for i in range(1, self.tabWidget.count()):
                tab_metadata = self.tabWidget.widget(i).metadata
                if tab_metadata['hash'] == file_md5:
                    file_paths.remove(filename[0])
                    if not file_paths:
                        self.tabWidget.setCurrentIndex(i)
                        return

        if not file_paths:
            return

        logger.info(
            'Attempting to open: ' + ', '.join(file_paths))

        contents, errors = sorter.BookSorter(
            file_paths,
            ('reading', None),
            self.database_path,
            self.settings,
            self.temp_dir.path()).initiate_threads()

        if errors:
            self.display_error_notification(errors)

        if not contents:
            logger.error('No parseable files found')
            return

        successfully_opened = []
        for i in contents:
            # New tabs are created here
            # Initial position adjustment is carried out by the tab itself
            file_data = contents[i]
            Tab(file_data, self)
            successfully_opened.append(file_data['path'])
        logger.info(
            'Successfully opened: ' + ', '.join(file_paths))

        if self.settings['last_open_tab'] == 'library':
            self.tabWidget.setCurrentIndex(0)
            self.listView.setFocus()
            self.settings['last_open_tab'] = None
            return

        for i in range(1, self.tabWidget.count()):
            this_path = self.tabWidget.widget(i).metadata['path']
            if self.settings['last_open_tab'] == this_path:
                self.tabWidget.setCurrentIndex(i)
                self.settings['last_open_tab'] = None
                return

        self.tabWidget.setCurrentIndex(self.tabWidget.count() - 1)

    def add_books(self):
        dialog_prompt = self._translate('Main_UI', 'Add books to database')
        ebooks_string = self._translate('Main_UI', 'eBooks')
        opened_files = QtWidgets.QFileDialog.getOpenFileNames(
            self, dialog_prompt, self.settings['last_open_path'],
            f'{ebooks_string}({self.available_parsers})')

        if not opened_files[0]:
            return

        self.settingsDialog.okButton.setEnabled(False)
        self.libraryToolBar.reloadLibraryButton.setEnabled(False)

        self.settings['last_open_path'] = os.path.dirname(opened_files[0][0])
        self.statusBar.setVisible(True)
        self.sorterProgress.setVisible(True)
        self.statusMessage.setText(self._translate('Main_UI', 'Adding books...'))
        self.thread = BackGroundBookAddition(
            opened_files[0], self.database_path, 'manual', self)
        self.thread.finished.connect(
            lambda: self.move_on(self.thread.errors))
        self.thread.start()

    def get_selection(self):
        selected_indexes = None

        if self.listView.isVisible():
            selected_books = self.lib_ref.itemProxyModel.mapSelectionToSource(
                self.listView.selectionModel().selection())
            selected_indexes = [i.indexes()[0] for i in selected_books]

        elif self.tableView.isVisible():
            selected_books = self.tableView.selectionModel().selectedRows()
            selected_indexes = [
                self.lib_ref.tableProxyModel.mapToSource(i) for i in selected_books]

        return selected_indexes

    def delete_books(self, selected_indexes=None):
        # Get a list of QItemSelection objects
        # What we're interested in is the indexes()[0] in each of them
        # That gives a list of indexes from the view model
        selected_indexes = self.get_selection()
        if not selected_indexes:
            return

        # Deal with message box selection
        def ifcontinue(box_button):
            if box_button.text() != '&Yes':
                return

            # Persistent model indexes are required beause deletion mutates the model
            # Generate and delete by persistent index
            delete_hashes = [
                self.lib_ref.libraryModel.data(
                    i, QtCore.Qt.UserRole + 6) for i in selected_indexes]
            persistent_indexes = [
                QtCore.QPersistentModelIndex(i) for i in selected_indexes]

            for i in persistent_indexes:
                self.lib_ref.libraryModel.removeRow(i.row())

            # Update the database in the background
            self.thread = BackGroundBookDeletion(
                delete_hashes, self.database_path)
            self.thread.finished.connect(self.move_on)
            self.thread.start()

        # Generate a message box to confirm deletion
        confirm_deletion = QtWidgets.QMessageBox()
        deletion_prompt = self._translate(
            'Main_UI', f'Delete book(s)?')
        confirm_deletion.setText(deletion_prompt)
        confirm_deletion.setIcon(QtWidgets.QMessageBox.Question)
        confirm_deletion.setWindowTitle(self._translate('Main_UI', 'Confirm deletion'))
        confirm_deletion.setStandardButtons(
            QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
        confirm_deletion.buttonClicked.connect(ifcontinue)
        confirm_deletion.show()
        confirm_deletion.exec_()

    def delete_pressed(self):
        if self.tabWidget.currentIndex() == 0:
            self.delete_books()

    def move_on(self, errors=None):
        self.settingsDialog.okButton.setEnabled(True)
        self.settingsDialog.okButton.setToolTip(
            self._translate('Main_UI', 'Save changes and start library scan'))
        self.libraryToolBar.reloadLibraryButton.setEnabled(True)

        self.sorterProgress.setVisible(False)
        self.sorterProgress.setValue(0)

        # The errors argument is a list and will only be present
        # in case of addition and reading
        if errors:
            self.display_error_notification(errors)
        else:
            if self.libraryToolBar.searchBar.text() == '':
                self.statusBar.setVisible(False)

        self.lib_ref.update_proxymodels()
        self.lib_ref.generate_library_tags()

        self.statusMessage.setText(
            str(self.lib_ref.itemProxyModel.rowCount()) +
            self._translate('Main_UI', ' books'))

        if not self.settings['perform_culling']:
            self.cover_functions.load_all_covers()

    def switch_library_view(self):
        if self.libraryToolBar.coverViewButton.isChecked():
            self.stackedWidget.setCurrentIndex(0)
            self.libraryToolBar.sortingBoxAction.setVisible(True)
        else:
            self.stackedWidget.setCurrentIndex(1)
            self.libraryToolBar.sortingBoxAction.setVisible(False)

        self.resizeEvent()

    def tab_switch(self):
        try:
            # Disallow library tab movement
            # Does not need to be looped since the library
            # tab can only ever go to position 1
            if not self.tabWidget.widget(0).is_library:
                self.tabWidget.tabBar().moveTab(1, 0)

            if self.current_tab != 0:
                self.tabWidget.widget(
                    self.current_tab).update_last_accessed_time()
        except AttributeError:
            pass

        self.current_tab = self.tabWidget.currentIndex()

        # Hide all side docks whenever a tab is switched
        for i in range(1, self.tabWidget.count()):
            self.tabWidget.widget(i).sideDock.setVisible(False)

        # If library
        if self.tabWidget.currentIndex() == 0:
            self.resizeEvent()
            self.start_culling_timer()

            if self.settings['show_bars']:
                self.bookToolBar.hide()
                self.libraryToolBar.show()

            if self.lib_ref.itemProxyModel:
                # Making the proxy model available doesn't affect
                # memory utilization at all. Bleh.
                self.statusMessage.setText(
                    str(self.lib_ref.itemProxyModel.rowCount()) +
                    self._translate('Main_UI', ' Books'))

            if self.libraryToolBar.searchBar.text() != '':
                self.statusBar.setVisible(True)

        else:
            if self.settings['show_bars']:
                self.bookToolBar.show()
                self.libraryToolBar.hide()

            current_tab = self.tabWidget.currentWidget()
            self.bookToolBar.tocBox.setModel(current_tab.tocModel)
            self.bookToolBar.tocTreeView.expandAll()
            current_tab.set_tocBox_index(None, None)

            # Needed to set the contentView widget background
            # on first run. Subsequent runs might be redundant,
            # but it doesn't seem to visibly affect performance
            self.profile_functions.format_contentView()
            self.statusBar.setVisible(False)

            if self.bookToolBar.fontButton.isChecked():
                self.bookToolBar.customize_view_on()
            else:
                if current_tab.are_we_doing_images_only:
                    self.bookToolBar.searchButton.setVisible(False)
                    self.bookToolBar.annotationButton.setVisible(False)
                    self.bookToolBar.bookSeparator2.setVisible(False)
                    self.bookToolBar.bookSeparator3.setVisible(False)
                else:
                    self.bookToolBar.searchButton.setVisible(True)
                    self.bookToolBar.annotationButton.setVisible(True)
                    self.bookToolBar.bookSeparator2.setVisible(True)
                    self.bookToolBar.bookSeparator3.setVisible(True)

    def tab_close(self, tab_index=None):
        if not tab_index:
            tab_index = self.tabWidget.currentIndex()
            if tab_index == 0:
                return

        tab_metadata = self.tabWidget.widget(tab_index).metadata

        self.thread = BackGroundTabUpdate(
            self.database_path, [tab_metadata])
        self.thread.start()

        self.tabWidget.widget(tab_index).update_last_accessed_time()

        self.tabWidget.widget(tab_index).deleteLater()
        self.tabWidget.widget(tab_index).setParent(None)
        gc.collect()

    def tab_disallow_library_movement(self, tab_index):
        # Makes the library tab immovable
        if tab_index == 0:
            self.tabWidget.setMovable(False)
        else:
            self.tabWidget.setMovable(True)

    def set_toc_position(self, event=None):
        currentIndex = self.bookToolBar.tocTreeView.currentIndex()
        required_position = currentIndex.data(QtCore.Qt.UserRole)
        if not required_position:
            return  # Initial startup might return a None

        # The set_content method is universal
        # It's going to do position tracking
        current_tab = self.tabWidget.currentWidget()
        current_tab.set_content(required_position, False, True)

    def library_doubleclick(self, index):
        sender = self.sender().objectName()

        if sender == 'listView':
            source_index = self.lib_ref.itemProxyModel.mapToSource(index)
        elif sender == 'tableView':
            source_index = self.lib_ref.tableProxyModel.mapToSource(index)

        item = self.lib_ref.libraryModel.item(source_index.row(), 0)
        metadata = item.data(QtCore.Qt.UserRole + 3)
        path = {metadata['path']: metadata['hash']}

        self.open_files(path)

    def display_error_notification(self, errors):
        self.statusBar.setVisible(True)
        self.errorButton.setVisible(True)

    def show_errors(self):
        # TODO
        # Create a separate viewing area for errors
        # before showing the log

        self.show_settings(3)
        self.settingsDialog.aboutTabWidget.setCurrentIndex(1)
        self.errorButton.setVisible(False)
        self.statusBar.setVisible(False)

    def statusbar_visibility(self):
        if self.sender() == self.libraryToolBar.searchBar:
            if self.libraryToolBar.searchBar.text() == '':
                self.statusBar.setVisible(False)
            else:
                self.statusBar.setVisible(True)

    def show_settings(self, stacked_widget_index):
        if not self.settingsDialog.isVisible():
            self.settingsDialog.show()
            index = self.settingsDialog.listModel.index(
                stacked_widget_index, 0)
            self.settingsDialog.listView.setCurrentIndex(index)
        else:
            self.settingsDialog.hide()

    #==================================================================
    # The contentView modification functions are in the guifunctions
    # module. self.profile_functions is the reference here.

    def get_color(self):
        self.profile_functions.get_color(
            self.sender().objectName())

    def modify_font(self):
        self.profile_functions.modify_font(
            self.sender().objectName())

    def modify_comic_view(self, key_pressed=None):
        if key_pressed:
            signal_sender = None
        else:
            signal_sender = self.sender().objectName()

        self.profile_functions.modify_comic_view(
            signal_sender, key_pressed)

    #=================================================================

    def change_page_view(self, key_pressed=False):
        # Set zoom mode to best fit to
        # make the transition less jarring
        # if the sender isn't the invert colors button
        if self.sender() != self.bookToolBar.invertButton:
            self.comic_profile['zoom_mode'] = 'bestFit'

        # Toggle Double page mode / manga mode on keypress
        if key_pressed == QtCore.Qt.Key_D:
            self.bookToolBar.doublePageButton.setChecked(
                not self.bookToolBar.doublePageButton.isChecked())
        if key_pressed == QtCore.Qt.Key_M:
            self.bookToolBar.mangaModeButton.setChecked(
                not self.bookToolBar.mangaModeButton.isChecked())

        # Change settings according to the
        # current state of each of the toolbar buttons
        self.settings['double_page_mode'] = self.bookToolBar.doublePageButton.isChecked()
        self.settings['manga_mode'] = self.bookToolBar.mangaModeButton.isChecked()
        self.settings['invert_colors'] = self.bookToolBar.invertButton.isChecked()

        # Switch page to whatever index is selected in the tocBox
        current_tab = self.tabWidget.currentWidget()
        chapter_number = current_tab.metadata['position']['current_chapter']
        current_tab.set_content(chapter_number, False)

    def generate_library_context_menu(self, position):
        index = self.sender().indexAt(position)
        if not index.isValid():
            return

        # It's worth remembering that these are indexes of the libraryModel
        # and NOT of the proxy models
        selected_indexes = self.get_selection()

        context_menu = QtWidgets.QMenu()

        openAction = context_menu.addAction(
            self.QImageFactory.get_image('view-readermode'),
            self._translate('Main_UI', 'Start reading'))

        editAction = None
        if len(selected_indexes) == 1:
            editAction = context_menu.addAction(
                self.QImageFactory.get_image('edit-rename'),
                self._translate('Main_UI', 'Edit'))

        deleteAction = context_menu.addAction(
            self.QImageFactory.get_image('trash-empty'),
            self._translate('Main_UI', 'Delete'))
        readAction = context_menu.addAction(
            QtGui.QIcon(':/images/checkmark.svg'),
            self._translate('Main_UI', 'Mark read'))
        unreadAction = context_menu.addAction(
            QtGui.QIcon(':/images/xmark.svg'),
            self._translate('Main_UI', 'Mark unread'))

        action = context_menu.exec_(self.sender().mapToGlobal(position))

        if action == openAction:
            books_to_open = {}
            for i in selected_indexes:
                metadata = self.lib_ref.libraryModel.data(i, QtCore.Qt.UserRole + 3)
                books_to_open[metadata['path']] = metadata['hash']

            self.open_files(books_to_open)

        if action == editAction:
            edit_book = selected_indexes[0]
            is_cover_loaded = self.lib_ref.libraryModel.data(
                edit_book, QtCore.Qt.UserRole + 8)

            # Loads a cover in case culling is enabled and the table view is visible
            if not is_cover_loaded:
                book_hash = self.lib_ref.libraryModel.data(
                    edit_book, QtCore.Qt.UserRole + 6)
                book_item = self.lib_ref.libraryModel.item(edit_book.row())
                book_cover = database.DatabaseFunctions(
                    self.database_path).fetch_covers_only([book_hash])[0][1]
                self.cover_functions.cover_loader(book_item, book_cover)

            cover = self.lib_ref.libraryModel.item(
                edit_book.row()).icon()
            title = self.lib_ref.libraryModel.data(
                edit_book, QtCore.Qt.UserRole)
            author = self.lib_ref.libraryModel.data(
                edit_book, QtCore.Qt.UserRole + 1)
            year = str(self.lib_ref.libraryModel.data(
                edit_book, QtCore.Qt.UserRole + 2))  # Text cannot be int
            tags = self.lib_ref.libraryModel.data(
                edit_book, QtCore.Qt.UserRole + 4)

            self.metadataDialog.load_book(
                cover, title, author, year, tags, edit_book)
            self.metadataDialog.show()

        if action == deleteAction:
            self.delete_books(selected_indexes)

        if action == readAction or action == unreadAction:
            for i in selected_indexes:
                metadata = self.lib_ref.libraryModel.data(i, QtCore.Qt.UserRole + 3)
                book_hash = self.lib_ref.libraryModel.data(i, QtCore.Qt.UserRole + 6)
                position = metadata['position']

                if position:
                    if action == readAction:
                        position['is_read'] = True
                        position['scroll_value'] = 1
                    elif action == unreadAction:
                        position['is_read'] = False
                        position['current_chapter'] = 1
                        position['scroll_value'] = 0
                else:
                    position = {}
                    if action == readAction:
                        position['is_read'] = True

                metadata['position'] = position

                position_perc = None
                last_accessed_time = None
                if action == readAction:
                    last_accessed_time = QtCore.QDateTime().currentDateTime()
                    position_perc = 1

                self.lib_ref.libraryModel.setData(i, metadata, QtCore.Qt.UserRole + 3)
                self.lib_ref.libraryModel.setData(i, position_perc, QtCore.Qt.UserRole + 7)
                self.lib_ref.libraryModel.setData(i, last_accessed_time, QtCore.Qt.UserRole + 12)
                self.lib_ref.update_proxymodels()

                database_dict = {
                    'Position': position,
                    'LastAccessed': last_accessed_time}

                database.DatabaseFunctions(
                    self.database_path).modify_metadata(database_dict, book_hash)

    def generate_library_filter_menu(self, directory_list=None):
        self.libraryFilterMenu.clear()

        def generate_name(path_data):
            this_filter = path_data[1]
            if not this_filter:
                this_filter = os.path.basename(
                    path_data[0]).title()
            return this_filter

        filter_actions = []
        filter_list = []
        if directory_list:
            checked = [i for i in directory_list if i[3] == QtCore.Qt.Checked]
            filter_list = list(map(generate_name, checked))
            filter_list.sort()

        filter_list.append(self._translate('Main_UI', 'Manually Added'))
        filter_actions = [QtWidgets.QAction(i, self.libraryFilterMenu) for i in filter_list]

        filter_all = QtWidgets.QAction('All', self.libraryFilterMenu)
        filter_actions.append(filter_all)

        for i in filter_actions:
            i.setCheckable(True)
            i.setChecked(True)
            i.triggered.connect(self.set_library_filter)

        self.libraryFilterMenu.addActions(filter_actions)
        self.libraryFilterMenu.insertSeparator(filter_all)
        self.libraryToolBar.libraryFilterButton.setMenu(self.libraryFilterMenu)

    def set_library_filter(self, event=None):
        self.active_library_filters = []
        something_was_unchecked = False

        if self.sender():  # Program startup sends a None here
            if self.sender().text() == 'All':
                for i in self.libraryFilterMenu.actions():
                    i.setChecked(self.sender().isChecked())

        for i in self.libraryFilterMenu.actions()[:-2]:
            if i.isChecked():
                self.active_library_filters.append(i.text())
            else:
                something_was_unchecked = True

        if something_was_unchecked:
            self.libraryFilterMenu.actions()[-1].setChecked(False)
        else:
            self.libraryFilterMenu.actions()[-1].setChecked(True)

        self.lib_ref.update_proxymodels()

    def toggle_distraction_free(self):
        self.settings['show_bars'] = not self.settings['show_bars']

        if self.tabWidget.count() > 1:
            self.tabWidget.tabBar().setVisible(
                self.settings['show_bars'])

        current_tab = self.tabWidget.currentIndex()
        if current_tab == 0:
            self.libraryToolBar.setVisible(
                not self.libraryToolBar.isVisible())
        else:
            self.bookToolBar.setVisible(
                not self.bookToolBar.isVisible())

        self.start_culling_timer()

    def start_culling_timer(self):
        if self.settings['perform_culling']:
            self.culling_timer.start(30)

    def resizeEvent(self, event=None):
        if event:
            # This implies a vertical resize event only
            # We ain't about that lifestyle
            if event.oldSize().width() == event.size().width():
                return

        # The hackiness of this hack is just...
        default_size = 170  # This is size of the QIcon (160 by default) +
                            # minimum margin needed between thumbnails

        # for n icons, the n + 1th icon will appear at > n +1.11875
        # First, calculate the number of images per row
        i = self.listView.viewport().width() / default_size
        rem = i - int(i)
        if rem >= .21875 and rem <= .9999:
            num_images = int(i)
        else:
            num_images = int(i) - 1

        # The rest is illustrated using informative variable names
        space_occupied = num_images * default_size
        # 12 is the scrollbar width
        # Larger numbers keep reduce flickering but also increase
        # the distance from the scrollbar
        space_left = (
            self.listView.viewport().width() - space_occupied - 19)
        try:
            layout_extra_space_per_image = space_left // num_images
            self.listView.setGridSize(
                QtCore.QSize(default_size + layout_extra_space_per_image, 250))
            self.start_culling_timer()
        except ZeroDivisionError:  # Initial resize is ignored
            return

    def closeEvent(self, event=None):
        if event:
            event.ignore()

        self.hide()
        self.metadataDialog.hide()
        self.settingsDialog.hide()
        self.definitionDialog.hide()
        self.temp_dir.remove()
        for this_dock in self.active_docks:
            try:
                this_dock.setVisible(False)
            except RuntimeError:
                pass

        self.settings['last_open_books'] = []
        if self.tabWidget.count() > 1:

            # All tabs must be iterated upon here
            all_metadata = []
            for i in range(1, self.tabWidget.count()):
                tab_metadata = self.tabWidget.widget(i).metadata
                all_metadata.append(tab_metadata)

                if self.settings['remember_files']:
                    self.settings['last_open_books'].append(tab_metadata['path'])

            Settings(self).save_settings()
            self.thread = BackGroundTabUpdate(
                self.database_path, all_metadata)
            self.thread.finished.connect(self.database_care)
            self.thread.start()

        else:
            Settings(self).save_settings()
            self.database_care()

    def database_care(self):
        database.DatabaseFunctions(self.database_path).vacuum_database()
        QtWidgets.qApp.exit()
Example #2
0
class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
    def __init__(self):
        super(MainUI, self).__init__()
        self.setupUi(self)

        # Set window icon
        self.setWindowIcon(QtGui.QIcon(':/images/Lector.png'))

        # Central Widget - Make borders disappear
        self.centralWidget().layout().setContentsMargins(0, 0, 0, 0)
        self.gridLayout_2.setContentsMargins(0, 0, 0, 0)

        # Initialize translation function
        self._translate = QtCore.QCoreApplication.translate

        # Create library widgets
        self.listView = DragDropListView(self, self.listPage)
        self.gridLayout_4.addWidget(self.listView, 0, 0, 1, 1)

        self.tableView = DragDropTableView(self, self.tablePage)
        self.gridLayout_3.addWidget(self.tableView, 0, 0, 1, 1)

        # Empty variables that will be infested soon
        self.settings = {}
        self.thread = None  # Background Thread
        self.current_contentView = None  # For fullscreening purposes
        self.display_profiles = None
        self.current_profile_index = None
        self.comic_profile = {}
        self.database_path = None
        self.active_library_filters = []
        self.active_bookmark_docks = []

        # Initialize application
        Settings(self).read_settings(
        )  # This should populate all variables that need
        # to be remembered across sessions

        # Initialize icon factory
        self.QImageFactory = QImageFactory(self)

        # Initialize toolbars
        self.libraryToolBar = LibraryToolBar(self)
        self.bookToolBar = BookToolBar(self)

        # Widget declarations
        self.libraryFilterMenu = QtWidgets.QMenu()
        self.statusMessage = QtWidgets.QLabel()

        # Reference variables
        self.alignment_dict = {
            'left': self.bookToolBar.alignLeft,
            'right': self.bookToolBar.alignRight,
            'center': self.bookToolBar.alignCenter,
            'justify': self.bookToolBar.alignJustify
        }

        # Create the database in case it doesn't exist
        database.DatabaseInit(self.database_path)

        # Initialize settings dialog
        self.settingsDialog = SettingsUI(self)

        # Initialize metadata dialog
        self.metadataDialog = MetadataUI(self)

        # Initialize definition view dialog
        self.definitionDialog = DefinitionsUI(self)

        # Make the statusbar invisible by default
        self.statusBar.setVisible(False)

        # Statusbar widgets
        self.statusMessage.setObjectName('statusMessage')
        self.statusBar.addPermanentWidget(self.statusMessage)
        self.sorterProgress = QtWidgets.QProgressBar()
        self.sorterProgress.setMaximumWidth(300)
        self.sorterProgress.setObjectName('sorterProgress')
        sorter.progressbar = self.sorterProgress  # This is so that updates can be
        # connected to setValue
        self.statusBar.addWidget(self.sorterProgress)
        self.sorterProgress.setVisible(False)

        # Application wide temporary directory
        self.temp_dir = QtCore.QTemporaryDir()

        # Init the Library
        self.lib_ref = Library(self)

        # Initialize Cover loading functions
        # Must be after the Library init
        self.cover_functions = CoverLoadingAndCulling(self)

        # Init the culling timer
        self.culling_timer = QtCore.QTimer()
        self.culling_timer.setSingleShot(True)
        self.culling_timer.timeout.connect(self.cover_functions.cull_covers)

        # Initialize profile modification functions
        self.profile_functions = ViewProfileModification(self)

        # Toolbar display
        # Maybe make this a persistent option
        self.settings['show_bars'] = True

        # Library toolbar
        self.libraryToolBar.addButton.triggered.connect(self.add_books)
        self.libraryToolBar.deleteButton.triggered.connect(self.delete_books)
        self.libraryToolBar.coverViewButton.triggered.connect(
            self.switch_library_view)
        self.libraryToolBar.tableViewButton.triggered.connect(
            self.switch_library_view)
        self.libraryToolBar.reloadLibraryButton.triggered.connect(
            self.settingsDialog.start_library_scan)
        self.libraryToolBar.colorButton.triggered.connect(self.get_color)
        self.libraryToolBar.settingsButton.triggered.connect(
            self.show_settings)
        self.libraryToolBar.searchBar.textChanged.connect(
            self.lib_ref.update_proxymodels)
        self.libraryToolBar.sortingBox.activated.connect(
            self.lib_ref.update_proxymodels)
        self.libraryToolBar.libraryFilterButton.setPopupMode(
            QtWidgets.QToolButton.InstantPopup)
        self.libraryToolBar.searchBar.textChanged.connect(
            self.statusbar_visibility)
        self.addToolBar(self.libraryToolBar)

        if self.settings['current_view'] == 0:
            self.libraryToolBar.coverViewButton.trigger()
        else:
            self.libraryToolBar.tableViewButton.trigger()

        # Book toolbar
        self.bookToolBar.annotationButton.triggered.connect(
            self.toggle_dock_widgets)
        self.bookToolBar.addBookmarkButton.triggered.connect(self.add_bookmark)
        self.bookToolBar.bookmarkButton.triggered.connect(
            self.toggle_dock_widgets)
        self.bookToolBar.distractionFreeButton.triggered.connect(
            self.toggle_distraction_free)
        self.bookToolBar.fullscreenButton.triggered.connect(
            self.set_fullscreen)

        self.bookToolBar.singlePageButton.triggered.connect(
            self.change_page_view)
        self.bookToolBar.doublePageButton.triggered.connect(
            self.change_page_view)
        if self.settings['page_view_button'] == 'singlePageButton':
            self.bookToolBar.singlePageButton.setChecked(True)
        else:
            self.bookToolBar.doublePageButton.setChecked(True)

        for count, i in enumerate(self.display_profiles):
            self.bookToolBar.profileBox.setItemData(count, i,
                                                    QtCore.Qt.UserRole)
        self.bookToolBar.profileBox.currentIndexChanged.connect(
            self.profile_functions.format_contentView)
        self.bookToolBar.profileBox.setCurrentIndex(self.current_profile_index)
        self.bookToolBar.searchBar.textChanged.connect(self.search_book)

        self.bookToolBar.fontBox.currentFontChanged.connect(self.modify_font)
        self.bookToolBar.fontSizeBox.currentIndexChanged.connect(
            self.modify_font)
        self.bookToolBar.lineSpacingUp.triggered.connect(self.modify_font)
        self.bookToolBar.lineSpacingDown.triggered.connect(self.modify_font)
        self.bookToolBar.paddingUp.triggered.connect(self.modify_font)
        self.bookToolBar.paddingDown.triggered.connect(self.modify_font)
        self.bookToolBar.resetProfile.triggered.connect(
            self.profile_functions.reset_profile)

        profile_index = self.bookToolBar.profileBox.currentIndex()
        current_profile = self.bookToolBar.profileBox.itemData(
            profile_index, QtCore.Qt.UserRole)
        for i in self.alignment_dict.items():
            i[1].triggered.connect(self.modify_font)
        self.alignment_dict[current_profile['text_alignment']].setChecked(True)

        self.bookToolBar.zoomIn.triggered.connect(self.modify_comic_view)
        self.bookToolBar.zoomOut.triggered.connect(self.modify_comic_view)
        self.bookToolBar.fitWidth.triggered.connect(self.modify_comic_view)
        self.bookToolBar.bestFit.triggered.connect(self.modify_comic_view)
        self.bookToolBar.originalSize.triggered.connect(self.modify_comic_view)
        self.bookToolBar.comicBGColor.clicked.connect(self.get_color)

        self.bookToolBar.colorBoxFG.clicked.connect(self.get_color)
        self.bookToolBar.colorBoxBG.clicked.connect(self.get_color)
        self.bookToolBar.tocBox.currentIndexChanged.connect(
            self.set_toc_position)
        self.addToolBar(self.bookToolBar)

        # Get the stylesheet of the default QLineEdit
        self.lineEditStyleSheet = self.bookToolBar.searchBar.styleSheet()

        # Make the correct toolbar visible
        self.current_tab = self.tabWidget.currentIndex()
        self.tab_switch()
        self.tabWidget.currentChanged.connect(self.tab_switch)

        # Tab closing
        self.tabWidget.setTabsClosable(True)

        # Get list of available parsers
        self.available_parsers = '*.' + ' *.'.join(sorter.available_parsers)
        print('Available parsers: ' + self.available_parsers)

        # The Library tab gets no button
        self.tabWidget.tabBar().setTabButton(0, QtWidgets.QTabBar.RightSide,
                                             None)
        self.tabWidget.tabCloseRequested.connect(self.tab_close)
        self.tabWidget.setTabBarAutoHide(True)

        # Init display models
        self.lib_ref.generate_model('build')
        self.lib_ref.generate_proxymodels()
        self.lib_ref.generate_library_tags()
        self.set_library_filter()
        self.start_culling_timer()

        # ListView
        self.listView.setGridSize(QtCore.QSize(175, 240))
        self.listView.setMouseTracking(True)
        self.listView.verticalScrollBar().setSingleStep(9)
        self.listView.doubleClicked.connect(self.library_doubleclick)
        self.listView.setItemDelegate(
            LibraryDelegate(self.temp_dir.path(), self))
        self.listView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.listView.customContextMenuRequested.connect(
            self.generate_library_context_menu)
        self.listView.verticalScrollBar().valueChanged.connect(
            self.start_culling_timer)
        self.listView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
        self.listView.setAcceptDrops(True)

        self.listView.setStyleSheet(
            "QListView {{background-color: {0}}}".format(
                self.settings['listview_background'].name()))

        # TODO
        # Maybe use this for readjusting the border of the focus rectangle
        # in the listView. Maybe this is a job for QML?

        # self.listView.setStyleSheet(
        #     "QListView::item:selected { border-color:blue; border-style:outset;"
        #     "border-width:2px; color:black; }")

        # TableView
        self.tableView.doubleClicked.connect(self.library_doubleclick)
        self.tableView.horizontalHeader().setSectionResizeMode(
            QtWidgets.QHeaderView.Interactive)
        self.tableView.horizontalHeader().setSortIndicator(
            2, QtCore.Qt.AscendingOrder)
        self.tableView.setColumnHidden(0, True)
        self.tableView.horizontalHeader().setHighlightSections(False)
        if self.settings['main_window_headers']:
            for count, i in enumerate(self.settings['main_window_headers']):
                self.tableView.horizontalHeader().resizeSection(count, int(i))
        self.tableView.horizontalHeader().resizeSection(5, 30)
        self.tableView.horizontalHeader().setStretchLastSection(False)
        self.tableView.horizontalHeader().sectionClicked.connect(
            self.lib_ref.tableProxyModel.sort_table_columns)
        self.tableView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.tableView.customContextMenuRequested.connect(
            self.generate_library_context_menu)

        # Keyboard shortcuts
        self.ksDistractionFree = QtWidgets.QShortcut(
            QtGui.QKeySequence('Ctrl+D'), self)
        self.ksDistractionFree.setContext(QtCore.Qt.ApplicationShortcut)
        self.ksDistractionFree.activated.connect(self.toggle_distraction_free)

        self.ksOpenFile = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+O'),
                                              self)
        self.ksOpenFile.setContext(QtCore.Qt.ApplicationShortcut)
        self.ksOpenFile.activated.connect(self.add_books)

        self.ksExitAll = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+Q'),
                                             self)
        self.ksExitAll.setContext(QtCore.Qt.ApplicationShortcut)
        self.ksExitAll.activated.connect(self.closeEvent)

        self.ksCloseTab = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+W'),
                                              self)
        self.ksCloseTab.setContext(QtCore.Qt.ApplicationShortcut)
        self.ksCloseTab.activated.connect(self.tab_close)

        self.ksDeletePressed = QtWidgets.QShortcut(
            QtGui.QKeySequence('Delete'), self)
        self.ksDeletePressed.setContext(QtCore.Qt.ApplicationShortcut)
        self.ksDeletePressed.activated.connect(self.delete_pressed)

        self.listView.setFocus()
        self.open_books_at_startup()

        # Scan the library @ startup
        if self.settings['scan_library']:
            self.settingsDialog.start_library_scan()

    def open_books_at_startup(self):
        # Last open books and command line books aren't being opened together
        # so that command line books are processed last and therefore retain focus

        # Open last... open books.
        # Then set the value to None for the next run
        if self.settings['last_open_books']:
            files_to_open = {i: None for i in self.settings['last_open_books']}
            self.open_files(files_to_open)
        else:
            self.settings['last_open_tab'] = None

        # Open input files if specified
        cl_parser = QtCore.QCommandLineParser()
        cl_parser.process(QtWidgets.qApp)
        my_args = cl_parser.positionalArguments()
        if my_args:
            file_list = [
                QtCore.QFileInfo(i).absoluteFilePath() for i in my_args
            ]
            self.process_post_hoc_files(file_list, True)

    def process_post_hoc_files(self, file_list, open_files_after_processing):
        # Takes care of both dragged and dropped files
        # As well as files sent as command line arguments

        file_list = [i for i in file_list if os.path.exists(i)]
        if not file_list:
            return

        books = sorter.BookSorter(file_list, ('addition', 'manual'),
                                  self.database_path,
                                  self.settings['auto_tags'],
                                  self.temp_dir.path())

        parsed_books = books.initiate_threads()
        if not parsed_books and not open_files_after_processing:
            return

        database.DatabaseFunctions(
            self.database_path).add_to_database(parsed_books)
        self.lib_ref.generate_model('addition', parsed_books, True)

        file_dict = {i: None for i in file_list}
        if open_files_after_processing:
            self.open_files(file_dict)

        self.move_on()

    def open_files(self, path_hash_dictionary):
        # file_paths is expected to be a dictionary
        # This allows for threading file opening
        # Which should speed up multiple file opening
        # especially @ application start

        file_paths = [i for i in path_hash_dictionary]

        for filename in path_hash_dictionary.items():

            file_md5 = filename[1]
            if not file_md5:
                try:
                    with open(filename[0], 'rb') as current_book:
                        first_bytes = current_book.read(
                            1024 * 32)  # First 32KB of the file
                        file_md5 = hashlib.md5(first_bytes).hexdigest()
                except FileNotFoundError:
                    return

            # Remove any already open files
            # Set focus to last file in case only one is open
            for i in range(1, self.tabWidget.count()):
                tab_metadata = self.tabWidget.widget(i).metadata
                if tab_metadata['hash'] == file_md5:
                    file_paths.remove(filename[0])
                    if not file_paths:
                        self.tabWidget.setCurrentIndex(i)
                        return

        if not file_paths:
            return

        def finishing_touches():
            self.profile_functions.format_contentView()
            self.start_culling_timer()

        print('Attempting to open: ' + ', '.join(file_paths))

        contents = sorter.BookSorter(file_paths, ('reading', None),
                                     self.database_path, True,
                                     self.temp_dir.path()).initiate_threads()

        # TODO
        # Notification feedback in case all books return nothing

        if not contents:
            return

        for i in contents:
            # New tabs are created here
            # Initial position adjustment is carried out by the tab itself
            file_data = contents[i]
            Tab(file_data, self)

        if self.settings['last_open_tab'] == 'library':
            self.tabWidget.setCurrentIndex(0)
            self.listView.setFocus()
            self.settings['last_open_tab'] = None
            return

        for i in range(1, self.tabWidget.count()):
            this_path = self.tabWidget.widget(i).metadata['path']
            if self.settings['last_open_tab'] == this_path:
                self.tabWidget.setCurrentIndex(i)
                self.settings['last_open_tab'] = None
                finishing_touches()
                return

        self.tabWidget.setCurrentIndex(self.tabWidget.count() - 1)
        finishing_touches()

    def start_culling_timer(self):
        if self.settings['perform_culling']:
            self.culling_timer.start(30)

    def add_bookmark(self):
        if self.tabWidget.currentIndex() != 0:
            self.tabWidget.widget(self.tabWidget.currentIndex()).add_bookmark()

    def resizeEvent(self, event=None):
        if event:
            # This implies a vertical resize event only
            # We ain't about that lifestyle
            if event.oldSize().width() == event.size().width():
                return

        # The hackiness of this hack is just...
        default_size = 170  # This is size of the QIcon (160 by default) +
        # minimum margin is needed between thumbnails

        # for n icons, the n + 1th icon will appear at > n +1.11875
        # First, calculate the number of images per row
        i = self.listView.viewport().width() / default_size
        rem = i - int(i)
        if rem >= .21875 and rem <= .9999:
            num_images = int(i)
        else:
            num_images = int(i) - 1

        # The rest is illustrated using informative variable names
        space_occupied = num_images * default_size
        # 12 is the scrollbar width
        # Larger numbers keep reduce flickering but also increase
        # the distance from the scrollbar
        space_left = (self.listView.viewport().width() - space_occupied - 19)
        try:
            layout_extra_space_per_image = space_left // num_images
            self.listView.setGridSize(
                QtCore.QSize(default_size + layout_extra_space_per_image, 250))
            self.start_culling_timer()
        except ZeroDivisionError:  # Initial resize is ignored
            return

    def add_books(self):
        dialog_prompt = self._translate('Main_UI', 'Add books to database')
        ebooks_string = self._translate('Main_UI', 'eBooks')
        opened_files = QtWidgets.QFileDialog.getOpenFileNames(
            self, dialog_prompt, self.settings['last_open_path'],
            f'{ebooks_string} ({self.available_parsers})')

        if not opened_files[0]:
            return

        self.settingsDialog.okButton.setEnabled(False)
        self.libraryToolBar.reloadLibraryButton.setEnabled(False)

        self.settings['last_open_path'] = os.path.dirname(opened_files[0][0])
        self.statusBar.setVisible(True)
        self.sorterProgress.setVisible(True)
        self.statusMessage.setText(
            self._translate('Main_UI', 'Adding books...'))
        self.thread = BackGroundBookAddition(opened_files[0],
                                             self.database_path, 'manual',
                                             self)
        self.thread.finished.connect(self.move_on)
        self.thread.start()

    def get_selection(self, library_widget):
        selected_indexes = None

        if library_widget == self.listView:
            selected_books = self.lib_ref.itemProxyModel.mapSelectionToSource(
                self.listView.selectionModel().selection())
            selected_indexes = [i.indexes()[0] for i in selected_books]

        elif library_widget == self.tableView:
            selected_books = self.tableView.selectionModel().selectedRows()
            selected_indexes = [
                self.lib_ref.tableProxyModel.mapToSource(i)
                for i in selected_books
            ]

        return selected_indexes

    def delete_books(self, selected_indexes=None):
        if not selected_indexes:
            # Get a list of QItemSelection objects
            # What we're interested in is the indexes()[0] in each of them
            # That gives a list of indexes from the view model
            if self.listView.isVisible():
                selected_indexes = self.get_selection(self.listView)

            elif self.tableView.isVisible():
                selected_indexes = self.get_selection(self.tableView)

        if not selected_indexes:
            return

        # Deal with message box selection
        def ifcontinue(box_button):
            if box_button.text() != '&Yes':
                return

            # Persistent model indexes are required beause deletion mutates the model
            # Generate and delete by persistent index
            delete_hashes = [
                self.lib_ref.libraryModel.data(i, QtCore.Qt.UserRole + 6)
                for i in selected_indexes
            ]
            persistent_indexes = [
                QtCore.QPersistentModelIndex(i) for i in selected_indexes
            ]

            for i in persistent_indexes:
                self.lib_ref.libraryModel.removeRow(i.row())

            # Update the database in the background
            self.thread = BackGroundBookDeletion(delete_hashes,
                                                 self.database_path)
            self.thread.finished.connect(self.move_on)
            self.thread.start()

        # Generate a message box to confirm deletion
        selected_number = len(selected_indexes)
        confirm_deletion = QtWidgets.QMessageBox()
        deletion_prompt = self._translate(
            'Main_UI', f'Delete {selected_number} book(s)?')
        confirm_deletion.setText(deletion_prompt)
        confirm_deletion.setIcon(QtWidgets.QMessageBox.Question)
        confirm_deletion.setWindowTitle(
            self._translate('Main_UI', 'Confirm deletion'))
        confirm_deletion.setStandardButtons(QtWidgets.QMessageBox.Yes
                                            | QtWidgets.QMessageBox.No)
        confirm_deletion.buttonClicked.connect(ifcontinue)
        confirm_deletion.show()
        confirm_deletion.exec_()

    def delete_pressed(self):
        if self.tabWidget.currentIndex() == 0:
            self.delete_books()

    def move_on(self):
        self.settingsDialog.okButton.setEnabled(True)
        self.settingsDialog.okButton.setToolTip(
            self._translate('Main_UI', 'Save changes and start library scan'))
        self.libraryToolBar.reloadLibraryButton.setEnabled(True)

        self.sorterProgress.setVisible(False)
        self.sorterProgress.setValue(0)

        if self.libraryToolBar.searchBar.text() == '':
            self.statusBar.setVisible(False)

        self.lib_ref.update_proxymodels()
        self.lib_ref.generate_library_tags()

        self.statusMessage.setText(
            str(self.lib_ref.itemProxyModel.rowCount()) +
            self._translate('Main_UI', ' books'))

        if not self.settings['perform_culling']:
            self.cover_functions.load_all_covers()

    def switch_library_view(self):
        if self.libraryToolBar.coverViewButton.isChecked():
            self.stackedWidget.setCurrentIndex(0)
            self.libraryToolBar.sortingBoxAction.setVisible(True)
        else:
            self.stackedWidget.setCurrentIndex(1)
            self.libraryToolBar.sortingBoxAction.setVisible(False)

        self.resizeEvent()

    def tab_switch(self):
        try:
            if self.current_tab != 0:
                self.tabWidget.widget(
                    self.current_tab).update_last_accessed_time()
        except AttributeError:
            pass

        self.current_tab = self.tabWidget.currentIndex()

        # Hide bookmark and annotation widgets
        for i in range(1, self.tabWidget.count()):
            self.tabWidget.widget(i).bookmarkDock.setVisible(False)
            self.tabWidget.widget(i).annotationDock.setVisible(False)

        if self.tabWidget.currentIndex() == 0:

            self.resizeEvent()
            self.start_culling_timer()
            if self.settings['show_bars']:
                self.bookToolBar.hide()
                self.libraryToolBar.show()

            if self.lib_ref.itemProxyModel:
                # Making the proxy model available doesn't affect
                # memory utilization at all. Bleh.
                self.statusMessage.setText(
                    str(self.lib_ref.itemProxyModel.rowCount()) +
                    self._translate('Main_UI', ' Books'))

            if self.libraryToolBar.searchBar.text() != '':
                self.statusBar.setVisible(True)

        else:

            if self.settings['show_bars']:
                self.bookToolBar.show()
                self.libraryToolBar.hide()

            current_tab = self.tabWidget.widget(self.tabWidget.currentIndex())
            current_metadata = current_tab.metadata

            if self.bookToolBar.fontButton.isChecked():
                self.bookToolBar.customize_view_on()

            current_position = current_metadata['position']
            current_toc = [i[0] for i in current_metadata['content']]

            self.bookToolBar.tocBox.blockSignals(True)
            self.bookToolBar.tocBox.clear()
            self.bookToolBar.tocBox.addItems(current_toc)
            if current_position:
                self.bookToolBar.tocBox.setCurrentIndex(
                    current_position['current_chapter'] - 1)
                if not current_metadata['images_only']:
                    current_tab.hiddenButton.animateClick(25)
            self.bookToolBar.tocBox.blockSignals(False)

            self.profile_functions.format_contentView()

            self.statusBar.setVisible(False)

    def tab_close(self, tab_index=None):
        if not tab_index:
            tab_index = self.tabWidget.currentIndex()
            if tab_index == 0:
                return

        tab_metadata = self.tabWidget.widget(tab_index).metadata

        self.thread = BackGroundTabUpdate(self.database_path, [tab_metadata])
        self.thread.start()

        self.tabWidget.widget(tab_index).update_last_accessed_time()

        self.tabWidget.widget(tab_index).deleteLater()
        self.tabWidget.widget(tab_index).setParent(None)
        gc.collect()

        self.bookToolBar.bookmarkButton.setChecked(False)
        self.bookToolBar.annotationButton.setChecked(False)

    def set_toc_position(self, event=None):
        current_tab = self.tabWidget.widget(self.tabWidget.currentIndex())

        current_tab.metadata['position']['current_chapter'] = event + 1
        current_tab.metadata['position']['is_read'] = False

        # Go on to change the value of the Table of Contents box
        current_tab.change_chapter_tocBox()
        current_tab.contentView.record_position()

        self.profile_functions.format_contentView()

    def set_fullscreen(self):
        current_tab = self.tabWidget.currentIndex()
        current_tab_widget = self.tabWidget.widget(current_tab)
        current_tab_widget.go_fullscreen()

    def toggle_dock_widgets(self):
        sender = self.sender()
        current_tab = self.tabWidget.currentIndex()
        current_tab_widget = self.tabWidget.widget(current_tab)

        if sender == self.bookToolBar.bookmarkButton:
            current_tab_widget.toggle_bookmarks()

        if sender == self.bookToolBar.annotationButton:
            current_tab_widget.toggle_annotations()

    def library_doubleclick(self, index):
        sender = self.sender().objectName()

        if sender == 'listView':
            source_index = self.lib_ref.itemProxyModel.mapToSource(index)
        elif sender == 'tableView':
            source_index = self.lib_ref.tableProxyModel.mapToSource(index)

        item = self.lib_ref.libraryModel.item(source_index.row(), 0)
        metadata = item.data(QtCore.Qt.UserRole + 3)
        path = {metadata['path']: metadata['hash']}

        self.open_files(path)

    def statusbar_visibility(self):
        if self.sender() == self.libraryToolBar.searchBar:
            if self.libraryToolBar.searchBar.text() == '':
                self.statusBar.setVisible(False)
            else:
                self.statusBar.setVisible(True)

    def show_settings(self):
        if not self.settingsDialog.isVisible():
            self.settingsDialog.show()
        else:
            self.settingsDialog.hide()

    #____________________________________________
    # The contentView modification functions are in the guifunctions
    # module. self.profile_functions is the reference here.

    def get_color(self):
        self.profile_functions.get_color(self.sender().objectName())

    def modify_font(self):
        self.profile_functions.modify_font(self.sender().objectName())

    def modify_comic_view(self, key_pressed=None):
        if key_pressed:
            signal_sender = None
        else:
            signal_sender = self.sender().objectName()
        self.profile_functions.modify_comic_view(signal_sender, key_pressed)

    #____________________________________________

    def change_page_view(self):
        self.settings['page_view_button'] = self.sender().objectName()
        chapter_number = self.bookToolBar.tocBox.currentIndex()

        # Switch page to whatever index is selected in the tocBox
        current_tab = self.tabWidget.currentWidget()
        required_content = current_tab.metadata['content'][chapter_number][1]
        current_tab.contentView.loadImage(required_content)

    def search_book(self, search_text):
        current_tab = self.tabWidget.currentIndex()
        if not (current_tab != 0 and
                not self.tabWidget.widget(current_tab).are_we_doing_images_only
                ):
            return

        contentView = self.tabWidget.widget(current_tab).contentView

        text_cursor = contentView.textCursor()
        something_found = True
        if search_text:
            text_cursor.setPosition(0, QtGui.QTextCursor.MoveAnchor)
            contentView.setTextCursor(text_cursor)
            contentView.verticalScrollBar().setValue(
                contentView.verticalScrollBar().maximum())
            something_found = contentView.find(search_text)
        else:
            text_cursor.clearSelection()
            contentView.setTextCursor(text_cursor)

        if not something_found:
            self.bookToolBar.searchBar.setStyleSheet("QLineEdit {color: red;}")
        else:
            self.bookToolBar.searchBar.setStyleSheet(self.lineEditStyleSheet)

    def generate_library_context_menu(self, position):
        index = self.sender().indexAt(position)
        if not index.isValid():
            return

        # It's worth remembering that these are indexes of the libraryModel
        # and NOT of the proxy models
        selected_indexes = self.get_selection(self.sender())

        context_menu = QtWidgets.QMenu()

        openAction = context_menu.addAction(
            self.QImageFactory.get_image('view-readermode'),
            self._translate('Main_UI', 'Start reading'))

        editAction = None
        if len(selected_indexes) == 1:
            editAction = context_menu.addAction(
                self.QImageFactory.get_image('edit-rename'),
                self._translate('Main_UI', 'Edit'))

        deleteAction = context_menu.addAction(
            self.QImageFactory.get_image('trash-empty'),
            self._translate('Main_UI', 'Delete'))
        readAction = context_menu.addAction(
            QtGui.QIcon(':/images/checkmark.svg'),
            self._translate('Main_UI', 'Mark read'))
        unreadAction = context_menu.addAction(
            QtGui.QIcon(':/images/xmark.svg'),
            self._translate('Main_UI', 'Mark unread'))

        action = context_menu.exec_(self.sender().mapToGlobal(position))

        if action == openAction:
            books_to_open = {}
            for i in selected_indexes:
                metadata = self.lib_ref.libraryModel.data(
                    i, QtCore.Qt.UserRole + 3)
                books_to_open[metadata['path']] = metadata['hash']

            self.open_files(books_to_open)

        if action == editAction:
            edit_book = selected_indexes[0]
            is_cover_loaded = self.lib_ref.libraryModel.data(
                edit_book, QtCore.Qt.UserRole + 8)

            # Loads a cover in case culling is enabled and the table view is visible
            if not is_cover_loaded:
                book_hash = self.lib_ref.libraryModel.data(
                    edit_book, QtCore.Qt.UserRole + 6)
                book_item = self.lib_ref.libraryModel.item(edit_book.row())
                book_cover = database.DatabaseFunctions(
                    self.database_path).fetch_covers_only([book_hash])[0][1]
                self.cover_functions.cover_loader(book_item, book_cover)

            cover = self.lib_ref.libraryModel.item(edit_book.row()).icon()
            title = self.lib_ref.libraryModel.data(edit_book,
                                                   QtCore.Qt.UserRole)
            author = self.lib_ref.libraryModel.data(edit_book,
                                                    QtCore.Qt.UserRole + 1)
            year = str(
                self.lib_ref.libraryModel.data(edit_book, QtCore.Qt.UserRole +
                                               2))  # Text cannot be int
            tags = self.lib_ref.libraryModel.data(edit_book,
                                                  QtCore.Qt.UserRole + 4)

            self.metadataDialog.load_book(cover, title, author, year, tags,
                                          edit_book)
            self.metadataDialog.show()

        if action == deleteAction:
            self.delete_books(selected_indexes)

        if action == readAction or action == unreadAction:
            for i in selected_indexes:
                metadata = self.lib_ref.libraryModel.data(
                    i, QtCore.Qt.UserRole + 3)
                book_hash = self.lib_ref.libraryModel.data(
                    i, QtCore.Qt.UserRole + 6)
                position = metadata['position']

                if position:
                    if action == readAction:
                        position['is_read'] = True
                        position['scroll_value'] = 1
                    elif action == unreadAction:
                        position['is_read'] = False
                        position['current_chapter'] = 1
                        position['scroll_value'] = 0
                else:
                    position = {}
                    if action == readAction:
                        position['is_read'] = True

                metadata['position'] = position

                position_perc = None
                last_accessed_time = None
                if action == readAction:
                    last_accessed_time = QtCore.QDateTime().currentDateTime()
                    position_perc = 1

                self.lib_ref.libraryModel.setData(i, metadata,
                                                  QtCore.Qt.UserRole + 3)
                self.lib_ref.libraryModel.setData(i, position_perc,
                                                  QtCore.Qt.UserRole + 7)
                self.lib_ref.libraryModel.setData(i, last_accessed_time,
                                                  QtCore.Qt.UserRole + 12)
                self.lib_ref.update_proxymodels()

                database_dict = {
                    'Position': position,
                    'LastAccessed': last_accessed_time
                }

                database.DatabaseFunctions(self.database_path).modify_metadata(
                    database_dict, book_hash)

    def generate_library_filter_menu(self, directory_list=None):
        self.libraryFilterMenu.clear()

        def generate_name(path_data):
            this_filter = path_data[1]
            if not this_filter:
                this_filter = os.path.basename(path_data[0]).title()
            return this_filter

        filter_actions = []
        filter_list = []
        if directory_list:
            checked = [i for i in directory_list if i[3] == QtCore.Qt.Checked]
            filter_list = list(map(generate_name, checked))
            filter_list.sort()

        filter_list.append(self._translate('Main_UI', 'Manually Added'))
        filter_actions = [
            QtWidgets.QAction(i, self.libraryFilterMenu) for i in filter_list
        ]

        filter_all = QtWidgets.QAction('All', self.libraryFilterMenu)
        filter_actions.append(filter_all)

        for i in filter_actions:
            i.setCheckable(True)
            i.setChecked(True)
            i.triggered.connect(self.set_library_filter)

        self.libraryFilterMenu.addActions(filter_actions)
        self.libraryFilterMenu.insertSeparator(filter_all)
        self.libraryToolBar.libraryFilterButton.setMenu(self.libraryFilterMenu)

    def set_library_filter(self, event=None):
        self.active_library_filters = []
        something_was_unchecked = False

        if self.sender():  # Program startup sends a None here
            if self.sender().text() == 'All':
                for i in self.libraryFilterMenu.actions():
                    i.setChecked(self.sender().isChecked())

        for i in self.libraryFilterMenu.actions()[:-2]:
            if i.isChecked():
                self.active_library_filters.append(i.text())
            else:
                something_was_unchecked = True

        if something_was_unchecked:
            self.libraryFilterMenu.actions()[-1].setChecked(False)
        else:
            self.libraryFilterMenu.actions()[-1].setChecked(True)

        self.lib_ref.update_proxymodels()

    def toggle_distraction_free(self):
        self.settings['show_bars'] = not self.settings['show_bars']

        if self.tabWidget.count() > 1:
            self.tabWidget.tabBar().setVisible(self.settings['show_bars'])

        current_tab = self.tabWidget.currentIndex()
        if current_tab == 0:
            self.libraryToolBar.setVisible(not self.libraryToolBar.isVisible())
        else:
            self.bookToolBar.setVisible(not self.bookToolBar.isVisible())

        self.start_culling_timer()

    def closeEvent(self, event=None):
        if event:
            event.ignore()

        self.hide()
        self.metadataDialog.hide()
        self.settingsDialog.hide()
        self.definitionDialog.hide()
        self.temp_dir.remove()
        for this_dock in self.active_bookmark_docks:
            try:
                this_dock.setVisible(False)
            except RuntimeError:
                pass

        self.settings['last_open_books'] = []
        if self.tabWidget.count() > 1:

            # All tabs must be iterated upon here
            all_metadata = []
            for i in range(1, self.tabWidget.count()):
                tab_metadata = self.tabWidget.widget(i).metadata
                all_metadata.append(tab_metadata)

                if self.settings['remember_files']:
                    self.settings['last_open_books'].append(
                        tab_metadata['path'])

            Settings(self).save_settings()
            self.thread = BackGroundTabUpdate(self.database_path, all_metadata)
            self.thread.finished.connect(self.database_care)
            self.thread.start()

        else:
            Settings(self).save_settings()
            self.database_care()

    def database_care(self):
        database.DatabaseFunctions(self.database_path).vacuum_database()
        QtWidgets.qApp.exit()
Example #3
0
class MainUI(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
    def __init__(self):
        super(MainUI, self).__init__()
        self.setupUi(self)

        # Initialize translation function
        self._translate = QtCore.QCoreApplication.translate

        # Empty variables that will be infested soon
        self.settings = {}
        self.thread = None  # Background Thread
        self.current_contentView = None  # For fullscreening purposes
        self.display_profiles = None
        self.current_profile_index = None
        self.comic_profile = {}
        self.database_path = None
        self.active_library_filters = []

        # Initialize application
        Settings(self).read_settings()  # This should populate all variables that need
                                        # to be remembered across sessions

        # Initialize icon factory
        self.QImageFactory = QImageFactory(self)

        # Initialize toolbars
        self.libraryToolBar = LibraryToolBar(self)
        self.bookToolBar = BookToolBar(self)

        # Widget declarations
        self.libraryFilterMenu = QtWidgets.QMenu()
        self.statusMessage = QtWidgets.QLabel()
        self.distractionFreeToggle = QtWidgets.QToolButton()
        self.reloadLibrary = QtWidgets.QToolButton()

        # Create the database in case it doesn't exist
        database.DatabaseInit(self.database_path)

        # Initialize settings dialog
        self.settingsDialog = SettingsUI(self)

        # Initialize metadata dialog
        self.metadataDialog = MetadataUI(self)

        # Initialize definition view dialog
        self.definitionDialog = DefinitionsUI(self)

        # Statusbar widgets
        self.statusMessage.setObjectName('statusMessage')
        self.statusBar.addPermanentWidget(self.statusMessage)
        self.sorterProgress = QtWidgets.QProgressBar()
        self.sorterProgress.setMaximumWidth(300)
        self.sorterProgress.setObjectName('sorterProgress')
        sorter.progressbar = self.sorterProgress  # This is so that updates can be
                                                  # connected to setValue
        self.statusBar.addWidget(self.sorterProgress)
        self.sorterProgress.setVisible(False)

        # Statusbar + Toolbar Visibility
        self.distractionFreeToggle.setIcon(self.QImageFactory.get_image('visibility'))
        self.distractionFreeToggle.setObjectName('distractionFreeToggle')
        self.distractionFreeToggle.setToolTip(
            self._translate('Main_UI', 'Toggle distraction free mode (Ctrl + D)'))
        self.distractionFreeToggle.setAutoRaise(True)
        self.distractionFreeToggle.clicked.connect(self.toggle_distraction_free)
        self.statusBar.addPermanentWidget(self.distractionFreeToggle)

        # Application wide temporary directory
        self.temp_dir = QtCore.QTemporaryDir()

        # Init the culling timer
        self.culling_timer = QtCore.QTimer()
        self.culling_timer.setSingleShot(True)
        self.culling_timer.timeout.connect(self.cull_covers)

        # Init the Library
        self.lib_ref = Library(self)

        # Toolbar display
        # Maybe make this a persistent option
        self.settings['show_bars'] = True

        # Library toolbar
        self.libraryToolBar.addButton.triggered.connect(self.add_books)
        self.libraryToolBar.deleteButton.triggered.connect(self.delete_books)
        self.libraryToolBar.coverViewButton.triggered.connect(self.switch_library_view)
        self.libraryToolBar.tableViewButton.triggered.connect(self.switch_library_view)
        self.libraryToolBar.colorButton.triggered.connect(self.get_color)
        self.libraryToolBar.settingsButton.triggered.connect(self.show_settings)
        self.libraryToolBar.searchBar.textChanged.connect(self.lib_ref.update_proxymodels)
        self.libraryToolBar.sortingBox.activated.connect(self.lib_ref.update_proxymodels)
        self.libraryToolBar.libraryFilterButton.setPopupMode(QtWidgets.QToolButton.InstantPopup)
        self.addToolBar(self.libraryToolBar)

        if self.settings['current_view'] == 0:
            self.libraryToolBar.coverViewButton.trigger()
        else:
            self.libraryToolBar.tableViewButton.trigger()

        # Book toolbar
        self.bookToolBar.addBookmarkButton.triggered.connect(self.add_bookmark)
        self.bookToolBar.bookmarkButton.triggered.connect(self.toggle_dock_widget)
        self.bookToolBar.fullscreenButton.triggered.connect(self.set_fullscreen)

        for count, i in enumerate(self.display_profiles):
            self.bookToolBar.profileBox.setItemData(count, i, QtCore.Qt.UserRole)
        self.bookToolBar.profileBox.currentIndexChanged.connect(self.format_contentView)
        self.bookToolBar.profileBox.setCurrentIndex(self.current_profile_index)

        self.bookToolBar.fontBox.currentFontChanged.connect(self.modify_font)
        self.bookToolBar.fontSizeBox.currentIndexChanged.connect(self.modify_font)
        self.bookToolBar.lineSpacingUp.triggered.connect(self.modify_font)
        self.bookToolBar.lineSpacingDown.triggered.connect(self.modify_font)
        self.bookToolBar.paddingUp.triggered.connect(self.modify_font)
        self.bookToolBar.paddingDown.triggered.connect(self.modify_font)
        self.bookToolBar.resetProfile.triggered.connect(self.reset_profile)

        self.alignment_dict = {
            'left': self.bookToolBar.alignLeft,
            'right': self.bookToolBar.alignRight,
            'center': self.bookToolBar.alignCenter,
            'justify': self.bookToolBar.alignJustify}

        profile_index = self.bookToolBar.profileBox.currentIndex()
        current_profile = self.bookToolBar.profileBox.itemData(
            profile_index, QtCore.Qt.UserRole)
        for i in self.alignment_dict.items():
            i[1].triggered.connect(self.modify_font)
        self.alignment_dict[current_profile['text_alignment']].setChecked(True)

        self.bookToolBar.zoomIn.triggered.connect(self.modify_comic_view)
        self.bookToolBar.zoomOut.triggered.connect(self.modify_comic_view)
        self.bookToolBar.fitWidth.triggered.connect(self.modify_comic_view)
        self.bookToolBar.bestFit.triggered.connect(self.modify_comic_view)
        self.bookToolBar.originalSize.triggered.connect(self.modify_comic_view)
        self.bookToolBar.comicBGColor.clicked.connect(self.get_color)

        self.bookToolBar.colorBoxFG.clicked.connect(self.get_color)
        self.bookToolBar.colorBoxBG.clicked.connect(self.get_color)
        self.bookToolBar.tocBox.currentIndexChanged.connect(self.set_toc_position)
        self.addToolBar(self.bookToolBar)

        # Make the correct toolbar visible
        self.current_tab = self.tabWidget.currentIndex()
        self.tab_switch()
        self.tabWidget.currentChanged.connect(self.tab_switch)

        # Tab closing
        self.tabWidget.setTabsClosable(True)

        # Get list of available parsers
        self.available_parsers = '*.' + ' *.'.join(sorter.available_parsers)
        print('Available parsers: ' + self.available_parsers)

        # The library refresh button on the Library tab
        self.reloadLibrary.setIcon(self.QImageFactory.get_image('reload'))
        self.reloadLibrary.setObjectName('reloadLibrary')
        self.reloadLibrary.setToolTip(self._translate('Main_UI', 'Scan library'))
        self.reloadLibrary.setAutoRaise(True)
        self.reloadLibrary.clicked.connect(self.settingsDialog.start_library_scan)

        self.tabWidget.tabBar().setTabButton(
            0, QtWidgets.QTabBar.RightSide, self.reloadLibrary)
        self.tabWidget.tabCloseRequested.connect(self.tab_close)

        # Init display models
        self.lib_ref.generate_model('build')
        self.lib_ref.generate_proxymodels()
        self.lib_ref.generate_library_tags()
        self.set_library_filter()
        self.start_culling_timer()

        # ListView
        self.listView.setGridSize(QtCore.QSize(175, 240))
        self.listView.setMouseTracking(True)
        self.listView.verticalScrollBar().setSingleStep(9)
        self.listView.doubleClicked.connect(self.library_doubleclick)
        self.listView.setItemDelegate(LibraryDelegate(self.temp_dir.path(), self))
        self.listView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.listView.customContextMenuRequested.connect(self.generate_library_context_menu)
        self.listView.verticalScrollBar().valueChanged.connect(self.start_culling_timer)

        self.listView.setStyleSheet(
            "QListView {{background-color: {0}}}".format(
                self.settings['listview_background'].name()))

        # TODO
        # Maybe use this for readjusting the border of the focus rectangle
        # in the listView. Maybe this is a job for QML?

        # self.listView.setStyleSheet(
        #     "QListView::item:selected { border-color:blue; border-style:outset;"
        #     "border-width:2px; color:black; }")

        # TableView
        self.tableView.doubleClicked.connect(self.library_doubleclick)
        self.tableView.horizontalHeader().setSectionResizeMode(
            QtWidgets.QHeaderView.Interactive)
        self.tableView.horizontalHeader().setSortIndicator(
            2, QtCore.Qt.AscendingOrder)
        self.tableView.setColumnHidden(0, True)
        self.tableView.horizontalHeader().setHighlightSections(False)
        if self.settings['main_window_headers']:
            for count, i in enumerate(self.settings['main_window_headers']):
                self.tableView.horizontalHeader().resizeSection(count, int(i))
        self.tableView.horizontalHeader().resizeSection(5, 1)
        self.tableView.horizontalHeader().setStretchLastSection(True)
        self.tableView.horizontalHeader().sectionClicked.connect(
            self.lib_ref.table_proxy_model.sort_table_columns)
        self.tableView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.tableView.customContextMenuRequested.connect(self.generate_library_context_menu)

        # Keyboard shortcuts
        self.ksDistractionFree = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+D'), self)
        self.ksDistractionFree.setContext(QtCore.Qt.ApplicationShortcut)
        self.ksDistractionFree.activated.connect(self.toggle_distraction_free)

        self.ksOpenFile = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+O'), self)
        self.ksOpenFile.setContext(QtCore.Qt.ApplicationShortcut)
        self.ksOpenFile.activated.connect(self.add_books)

        self.ksExitAll = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+Q'), self)
        self.ksExitAll.setContext(QtCore.Qt.ApplicationShortcut)
        self.ksExitAll.activated.connect(self.closeEvent)

        self.ksCloseTab = QtWidgets.QShortcut(QtGui.QKeySequence('Ctrl+W'), self)
        self.ksCloseTab.setContext(QtCore.Qt.ApplicationShortcut)
        self.ksCloseTab.activated.connect(self.tab_close)

        self.listView.setFocus()
        self.open_books_at_startup()

        # Scan the library @ startup
        if self.settings['scan_library']:
            self.settingsDialog.start_library_scan()

    def open_books_at_startup(self):
        # Last open books and command line books aren't being opened together
        # so that command line books are processed last and therefore retain focus

        # Open last... open books.
        # Then set the value to None for the next run
        if self.settings['last_open_books']:
            files_to_open = {i: None for i in self.settings['last_open_books']}
            self.open_files(files_to_open)
        else:
            self.settings['last_open_tab'] = None

        # Open input files if specified
        cl_parser = QtCore.QCommandLineParser()
        cl_parser.process(QtWidgets.qApp)
        my_args = cl_parser.positionalArguments()
        if my_args:
            file_list = [QtCore.QFileInfo(i).absoluteFilePath() for i in my_args]
            books = sorter.BookSorter(
                file_list,
                'addition',
                self.database_path,
                self.settings['auto_tags'],
                self.temp_dir.path())

            parsed_books = books.initiate_threads()
            if not parsed_books:
                return

            database.DatabaseFunctions(self.database_path).add_to_database(parsed_books)
            self.lib_ref.generate_model('addition', parsed_books, True)

            file_dict = {QtCore.QFileInfo(i).absoluteFilePath(): None for i in my_args}
            self.open_files(file_dict)

            self.move_on()

    def cull_covers(self, event=None):
        blank_pixmap = QtGui.QPixmap()
        blank_pixmap.load(':/images/blank.png')  # Keep this. Removing it causes the
                                                 # listView to go blank on a resize

        all_indexes = set()
        for i in range(self.lib_ref.item_proxy_model.rowCount()):
            all_indexes.add(self.lib_ref.item_proxy_model.index(i, 0))

        y_range = list(range(0, self.listView.viewport().height(), 100))
        y_range.extend((-20, self.listView.viewport().height() + 20))
        x_range = range(0, self.listView.viewport().width(), 80)

        visible_indexes = set()
        for i in y_range:
            for j in x_range:
                this_index = self.listView.indexAt(QtCore.QPoint(j, i))
                visible_indexes.add(this_index)

        invisible_indexes = all_indexes - visible_indexes
        for i in invisible_indexes:
            model_index = self.lib_ref.item_proxy_model.mapToSource(i)
            this_item = self.lib_ref.view_model.item(model_index.row())

            if this_item:
                this_item.setIcon(QtGui.QIcon(blank_pixmap))
                this_item.setData(False, QtCore.Qt.UserRole + 8)

        hash_index_dict = {}
        hash_list = []
        for i in visible_indexes:
            model_index = self.lib_ref.item_proxy_model.mapToSource(i)

            book_hash = self.lib_ref.view_model.data(
                model_index, QtCore.Qt.UserRole + 6)
            cover_displayed = self.lib_ref.view_model.data(
                model_index, QtCore.Qt.UserRole + 8)

            if book_hash and not cover_displayed:
                hash_list.append(book_hash)
                hash_index_dict[book_hash] = model_index

        all_covers = database.DatabaseFunctions(
            self.database_path).fetch_covers_only(hash_list)

        for i in all_covers:
            book_hash = i[0]
            cover = i[1]
            model_index = hash_index_dict[book_hash]

            book_item = self.lib_ref.view_model.item(model_index.row())
            self.cover_loader(book_item, cover)

    def start_culling_timer(self):
        if self.settings['perform_culling']:
            self.culling_timer.start(30)

    def load_all_covers(self):
        all_covers_db = database.DatabaseFunctions(
            self.database_path).fetch_data(
                ('Hash', 'CoverImage',),
                'books',
                {'Hash': ''},
                'LIKE')

        if not all_covers_db:
            return

        all_covers = {
            i[0]: i[1] for i in all_covers_db}

        for i in range(self.lib_ref.view_model.rowCount()):
            this_item = self.lib_ref.view_model.item(i, 0)

            is_cover_already_displayed = this_item.data(QtCore.Qt.UserRole + 8)
            if is_cover_already_displayed:
                continue

            book_hash = this_item.data(QtCore.Qt.UserRole + 6)
            cover = all_covers[book_hash]
            self.cover_loader(this_item, cover)

    def cover_loader(self, item, cover):
        img_pixmap = QtGui.QPixmap()
        if cover:
            img_pixmap.loadFromData(cover)
        else:
            img_pixmap.load(':/images/NotFound.png')
        img_pixmap = img_pixmap.scaled(420, 600, QtCore.Qt.IgnoreAspectRatio)
        item.setIcon(QtGui.QIcon(img_pixmap))
        item.setData(True, QtCore.Qt.UserRole + 8)

    def add_bookmark(self):
        if self.tabWidget.currentIndex() != 0:
            self.tabWidget.widget(self.tabWidget.currentIndex()).add_bookmark()

    def resizeEvent(self, event=None):
        if event:
            # This implies a vertical resize event only
            # We ain't about that lifestyle
            if event.oldSize().width() == event.size().width():
                return

        # The hackiness of this hack is just...
        default_size = 170  # This is size of the QIcon (160 by default) +
                            # minimum margin is needed between thumbnails

        # for n icons, the n + 1th icon will appear at > n +1.11875
        # First, calculate the number of images per row
        i = self.listView.viewport().width() / default_size
        rem = i - int(i)
        if rem >= .11875 and rem <= .9999:
            num_images = int(i)
        else:
            num_images = int(i) - 1

        # The rest is illustrated using informative variable names
        space_occupied = num_images * default_size
        # 12 is the scrollbar width
        # Larger numbers keep reduce flickering but also increase
        # the distance from the scrollbar
        space_left = (
            self.listView.viewport().width() - space_occupied - 19)
        try:
            layout_extra_space_per_image = space_left // num_images
            self.listView.setGridSize(
                QtCore.QSize(default_size + layout_extra_space_per_image, 250))
            self.start_culling_timer()
        except ZeroDivisionError:  # Initial resize is ignored
            return

    def add_books(self):
        # TODO
        # Remember file addition modality
        # If a file is added from here, it should not be removed
        # from the libary in case of a database refresh

        dialog_prompt = self._translate('Main_UI', 'Add books to database')
        ebooks_string = self._translate('Main_UI', 'eBooks')
        opened_files = QtWidgets.QFileDialog.getOpenFileNames(
            self, dialog_prompt, self.settings['last_open_path'],
            f'{ebooks_string} ({self.available_parsers})')

        if not opened_files[0]:
            return

        self.settingsDialog.okButton.setEnabled(False)
        self.reloadLibrary.setEnabled(False)

        self.settings['last_open_path'] = os.path.dirname(opened_files[0][0])
        self.sorterProgress.setVisible(True)
        self.statusMessage.setText(self._translate('Main_UI', 'Adding books...'))
        self.thread = BackGroundBookAddition(
            opened_files[0], self.database_path, False, self)
        self.thread.finished.connect(self.move_on)
        self.thread.start()

    def get_selection(self, library_widget):
        selected_indexes = None

        if library_widget == self.listView:
            selected_books = self.lib_ref.item_proxy_model.mapSelectionToSource(
                self.listView.selectionModel().selection())
            selected_indexes = [i.indexes()[0] for i in selected_books]

        elif library_widget == self.tableView:
            selected_books = self.tableView.selectionModel().selectedRows()
            selected_indexes = [
                self.lib_ref.table_proxy_model.mapToSource(i) for i in selected_books]

        return selected_indexes

    def delete_books(self, selected_indexes=None):
        # TODO
        # ? Mirror selection
        # Ask if library files are to be excluded from further scans
        # Make a checkbox for this

        if not selected_indexes:
            # Get a list of QItemSelection objects
            # What we're interested in is the indexes()[0] in each of them
            # That gives a list of indexes from the view model
            if self.listView.isVisible():
                selected_indexes = self.get_selection(self.listView)

            elif self.tableView.isVisible():
                selected_indexes = self.get_selection(self.tableView)

        if not selected_indexes:
            return

        # Deal with message box selection
        def ifcontinue(box_button):
            if box_button.text() != '&Yes':
                return

            # Persistent model indexes are required beause deletion mutates the model
            # Generate and delete by persistent index
            delete_hashes = [
                self.lib_ref.view_model.data(
                    i, QtCore.Qt.UserRole + 6) for i in selected_indexes]
            persistent_indexes = [QtCore.QPersistentModelIndex(i) for i in selected_indexes]

            for i in persistent_indexes:
                self.lib_ref.view_model.removeRow(i.row())

            # Update the database in the background
            self.thread = BackGroundBookDeletion(
                delete_hashes, self.database_path, self)
            self.thread.finished.connect(self.move_on)
            self.thread.start()

        # Generate a message box to confirm deletion
        selected_number = len(selected_indexes)
        confirm_deletion = QtWidgets.QMessageBox()
        deletion_prompt = self._translate(
            'Main_UI', f'Delete {selected_number} book(s)?')
        confirm_deletion.setText(deletion_prompt)
        confirm_deletion.setIcon(QtWidgets.QMessageBox.Question)
        confirm_deletion.setWindowTitle(self._translate('Main_UI', 'Confirm deletion'))
        confirm_deletion.setStandardButtons(
            QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
        confirm_deletion.buttonClicked.connect(ifcontinue)
        confirm_deletion.show()
        confirm_deletion.exec_()

    def move_on(self):
        self.settingsDialog.okButton.setEnabled(True)
        self.settingsDialog.okButton.setToolTip(
            self._translate('Main_UI', 'Save changes and start library scan'))
        self.reloadLibrary.setEnabled(True)

        self.sorterProgress.setVisible(False)
        self.sorterProgress.setValue(0)

        self.lib_ref.update_proxymodels()
        self.lib_ref.generate_library_tags()

        self.statusMessage.setText(
            str(self.lib_ref.item_proxy_model.rowCount()) + ' books')

        if not self.settings['perform_culling']:
            self.load_all_covers()

    def switch_library_view(self):
        if self.libraryToolBar.coverViewButton.isChecked():
            self.stackedWidget.setCurrentIndex(0)
            self.libraryToolBar.sortingBoxAction.setVisible(True)
        else:
            self.stackedWidget.setCurrentIndex(1)
            self.libraryToolBar.sortingBoxAction.setVisible(False)

        self.resizeEvent()

    def tab_switch(self):
        try:
            if self.current_tab != 0:
                self.tabWidget.widget(
                    self.current_tab).update_last_accessed_time()
        except AttributeError:
            pass

        self.current_tab = self.tabWidget.currentIndex()

        if self.tabWidget.currentIndex() == 0:

            self.resizeEvent()
            self.start_culling_timer()
            if self.settings['show_bars']:
                self.bookToolBar.hide()
                self.libraryToolBar.show()

            if self.lib_ref.item_proxy_model:
                # Making the proxy model available doesn't affect
                # memory utilization at all. Bleh.
                self.statusMessage.setText(
                    str(self.lib_ref.item_proxy_model.rowCount()) +
                    self._translate('Main_UI', ' Books'))
        else:

            if self.settings['show_bars']:
                self.bookToolBar.show()
                self.libraryToolBar.hide()

            current_tab = self.tabWidget.widget(
                self.tabWidget.currentIndex())
            current_metadata = current_tab.metadata

            if self.bookToolBar.fontButton.isChecked():
                self.bookToolBar.customize_view_on()

            current_title = current_metadata['title']
            current_author = current_metadata['author']
            current_position = current_metadata['position']
            current_toc = [i[0] for i in current_metadata['content']]

            self.bookToolBar.tocBox.blockSignals(True)
            self.bookToolBar.tocBox.clear()
            self.bookToolBar.tocBox.addItems(current_toc)
            if current_position:
                self.bookToolBar.tocBox.setCurrentIndex(
                    current_position['current_chapter'] - 1)
                if not current_metadata['images_only']:
                    current_tab.set_scroll_value(False)
            self.bookToolBar.tocBox.blockSignals(False)

            self.format_contentView()

            self.statusMessage.setText(
                current_author + ' - ' + current_title)

    def tab_close(self, tab_index=None):
        if not tab_index:
            tab_index = self.tabWidget.currentIndex()
            if tab_index == 0:
                return

        tab_metadata = self.tabWidget.widget(tab_index).metadata

        self.thread = BackGroundTabUpdate(
            self.database_path, [tab_metadata])
        self.thread.start()

        self.tabWidget.widget(tab_index).update_last_accessed_time()
        self.tabWidget.removeTab(tab_index)

    def set_toc_position(self, event=None):
        current_tab = self.tabWidget.widget(self.tabWidget.currentIndex())

        # We're updating the underlying model to have real-time
        # updates on the read status

        # Set a baseline model index in case the item gets deleted
        # E.g It's open in a tab and deleted from the library
        model_index = None
        start_index = self.lib_ref.view_model.index(0, 0)
        # Find index of the model item that corresponds to the tab
        matching_item = self.lib_ref.view_model.match(
            start_index,
            QtCore.Qt.UserRole + 6,
            current_tab.metadata['hash'],
            1, QtCore.Qt.MatchExactly)
        if matching_item:
            model_row = matching_item[0].row()
            model_index = self.lib_ref.view_model.index(model_row, 0)

        current_tab.metadata[
            'position']['current_chapter'] = event + 1
        current_tab.metadata[
            'position']['is_read'] = False

        # TODO
        # This doesn't update correctly
        # try:
        #     position_perc = (
        #         current_tab.metadata[
        #             'current_chapter'] * 100 / current_tab.metadata['total_chapters'])
        # except KeyError:
        #     position_perc = None

        if model_index:
            self.lib_ref.view_model.setData(
                model_index, current_tab.metadata, QtCore.Qt.UserRole + 3)
            # self.lib_ref.view_model.setData(
            #     model_index, position_perc, QtCore.Qt.UserRole + 7)

        # Go on to change the value of the Table of Contents box
        current_tab.change_chapter_tocBox()
        self.format_contentView()

    def set_fullscreen(self):
        current_tab = self.tabWidget.currentIndex()
        current_tab_widget = self.tabWidget.widget(current_tab)
        current_tab_widget.go_fullscreen()

    def toggle_dock_widget(self):
        sender = self.sender().objectName()
        current_tab = self.tabWidget.currentIndex()
        current_tab_widget = self.tabWidget.widget(current_tab)

        # TODO
        # Extend this to other context related functions
        # Make this fullscreenable

        if sender == 'bookmarkButton':
            current_tab_widget.toggle_bookmarks()

    def library_doubleclick(self, index):
        sender = self.sender().objectName()

        if sender == 'listView':
            source_index = self.lib_ref.item_proxy_model.mapToSource(index)
        elif sender == 'tableView':
            source_index = self.lib_ref.table_proxy_model.mapToSource(index)

        item = self.lib_ref.view_model.item(source_index.row(), 0)
        metadata = item.data(QtCore.Qt.UserRole + 3)
        path = {metadata['path']: metadata['hash']}

        self.open_files(path)

    def open_files(self, path_hash_dictionary):
        # file_paths is expected to be a dictionary
        # This allows for threading file opening
        # Which should speed up multiple file opening
        # especially @ application start

        file_paths = [i for i in path_hash_dictionary]

        for filename in path_hash_dictionary.items():

            file_md5 = filename[1]
            if not file_md5:
                try:
                    with open(filename[0], 'rb') as current_book:
                        first_bytes = current_book.read(1024 * 32)  # First 32KB of the file
                        file_md5 = hashlib.md5(first_bytes).hexdigest()
                except FileNotFoundError:
                    return

            # Remove any already open files
            # Set focus to last file in case only one is open
            for i in range(1, self.tabWidget.count()):
                tab_metadata = self.tabWidget.widget(i).metadata
                if tab_metadata['hash'] == file_md5:
                    file_paths.remove(filename[0])
                    if not file_paths:
                        self.tabWidget.setCurrentIndex(i)
                        return

        if not file_paths:
            return

        def finishing_touches():
            self.format_contentView()
            self.start_culling_timer()

        print('Attempting to open: ' + ', '.join(file_paths))

        contents = sorter.BookSorter(
            file_paths,
            'reading',
            self.database_path,
            True,
            self.temp_dir.path()).initiate_threads()

        for i in contents:
            # New tabs are created here
            # Initial position adjustment is carried out by the tab itself
            file_data = contents[i]
            Tab(file_data, self.tabWidget)

        if self.settings['last_open_tab'] == 'library':
            self.tabWidget.setCurrentIndex(0)
            self.listView.setFocus()
            self.settings['last_open_tab'] = None
            return

        for i in range(1, self.tabWidget.count()):
            this_path = self.tabWidget.widget(i).metadata['path']
            if self.settings['last_open_tab'] == this_path:
                self.tabWidget.setCurrentIndex(i)
                self.settings['last_open_tab'] = None
                finishing_touches()
                return

        self.tabWidget.setCurrentIndex(self.tabWidget.count() - 1)
        finishing_touches()

    # TODO
    # def dropEvent

    def get_color(self):
        def open_color_dialog(current_color):
            color_dialog = QtWidgets.QColorDialog()
            new_color = color_dialog.getColor(current_color)
            if new_color.isValid():  # Returned in case cancel is pressed
                return new_color
            else:
                return current_color

        signal_sender = self.sender().objectName()

        # Special cases that don't affect (comic)book display
        if signal_sender == 'libraryBackground':
            current_color = self.settings['listview_background']
            new_color = open_color_dialog(current_color)
            self.listView.setStyleSheet("QListView {{background-color: {0}}}".format(
                new_color.name()))
            self.settings['listview_background'] = new_color
            return

        if signal_sender == 'dialogBackground':
            current_color = self.settings['dialog_background']
            new_color = open_color_dialog(current_color)
            self.settings['dialog_background'] = new_color
            return new_color

        profile_index = self.bookToolBar.profileBox.currentIndex()
        current_profile = self.bookToolBar.profileBox.itemData(
            profile_index, QtCore.Qt.UserRole)

        # Retain current values on opening a new dialog
        if signal_sender == 'fgColor':
            current_color = current_profile['foreground']
            new_color = open_color_dialog(current_color)
            self.bookToolBar.colorBoxFG.setStyleSheet(
                'background-color: %s' % new_color.name())
            current_profile['foreground'] = new_color

        elif signal_sender == 'bgColor':
            current_color = current_profile['background']
            new_color = open_color_dialog(current_color)
            self.bookToolBar.colorBoxBG.setStyleSheet(
                'background-color: %s' % new_color.name())
            current_profile['background'] = new_color

        elif signal_sender == 'comicBGColor':
            current_color = self.comic_profile['background']
            new_color = open_color_dialog(current_color)
            self.bookToolBar.comicBGColor.setStyleSheet(
                'background-color: %s' % new_color.name())
            self.comic_profile['background'] = new_color

        self.bookToolBar.profileBox.setItemData(
            profile_index, current_profile, QtCore.Qt.UserRole)
        self.format_contentView()

    def modify_font(self):
        signal_sender = self.sender().objectName()
        profile_index = self.bookToolBar.profileBox.currentIndex()
        current_profile = self.bookToolBar.profileBox.itemData(
            profile_index, QtCore.Qt.UserRole)

        if signal_sender == 'fontBox':
            current_profile['font'] = self.bookToolBar.fontBox.currentFont().family()

        if signal_sender == 'fontSizeBox':
            old_size = current_profile['font_size']
            new_size = self.bookToolBar.fontSizeBox.itemText(
                self.bookToolBar.fontSizeBox.currentIndex())
            if new_size.isdigit():
                current_profile['font_size'] = new_size
            else:
                current_profile['font_size'] = old_size

        if signal_sender == 'lineSpacingUp' and current_profile['line_spacing'] < 200:
            current_profile['line_spacing'] += 5
        if signal_sender == 'lineSpacingDown' and current_profile['line_spacing'] > 90:
            current_profile['line_spacing'] -= 5

        if signal_sender == 'paddingUp':
            current_profile['padding'] += 5
        if signal_sender == 'paddingDown':
            current_profile['padding'] -= 5

        alignment_dict = {
            'alignLeft': 'left',
            'alignRight': 'right',
            'alignCenter': 'center',
            'alignJustify': 'justify'}
        if signal_sender in alignment_dict:
            current_profile['text_alignment'] = alignment_dict[signal_sender]

        self.bookToolBar.profileBox.setItemData(
            profile_index, current_profile, QtCore.Qt.UserRole)
        self.format_contentView()

    def modify_comic_view(self, key_pressed=None):
        if key_pressed:
            signal_sender = None
        else:
            signal_sender = self.sender().objectName()

        current_tab = self.tabWidget.widget(self.tabWidget.currentIndex())

        self.bookToolBar.fitWidth.setChecked(False)
        self.bookToolBar.bestFit.setChecked(False)
        self.bookToolBar.originalSize.setChecked(False)

        if signal_sender == 'zoomOut' or key_pressed == QtCore.Qt.Key_Minus:
            self.comic_profile['zoom_mode'] = 'manualZoom'
            self.comic_profile['padding'] += 50

            # This prevents infinite zoom out
            if self.comic_profile['padding'] * 2 > current_tab.contentView.viewport().width():
                self.comic_profile['padding'] -= 50

        if signal_sender == 'zoomIn' or key_pressed in (QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal):
            self.comic_profile['zoom_mode'] = 'manualZoom'
            self.comic_profile['padding'] -= 50

            # This prevents infinite zoom in
            if self.comic_profile['padding'] < 0:
                self.comic_profile['padding'] = 0

        if signal_sender == 'fitWidth' or key_pressed == QtCore.Qt.Key_W:
            self.comic_profile['zoom_mode'] = 'fitWidth'
            self.comic_profile['padding'] = 0
            self.bookToolBar.fitWidth.setChecked(True)

        # Padding in the following cases is decided by
        # the image pixmap loaded by the widget
        if signal_sender == 'bestFit' or key_pressed == QtCore.Qt.Key_B:
            self.comic_profile['zoom_mode'] = 'bestFit'
            self.bookToolBar.bestFit.setChecked(True)

        if signal_sender == 'originalSize' or key_pressed == QtCore.Qt.Key_O:
            self.comic_profile['zoom_mode'] = 'originalSize'
            self.bookToolBar.originalSize.setChecked(True)

        self.format_contentView()

    def format_contentView(self):
        # TODO
        # See what happens if a font isn't installed

        current_tab = self.tabWidget.widget(
            self.tabWidget.currentIndex())

        try:
            current_metadata = current_tab.metadata
        except AttributeError:
            return

        if current_metadata['images_only']:
            background = self.comic_profile['background']
            padding = self.comic_profile['padding']
            zoom_mode = self.comic_profile['zoom_mode']

            if zoom_mode == 'fitWidth':
                self.bookToolBar.fitWidth.setChecked(True)
            if zoom_mode == 'bestFit':
                self.bookToolBar.bestFit.setChecked(True)
            if zoom_mode == 'originalSize':
                self.bookToolBar.originalSize.setChecked(True)

            self.bookToolBar.comicBGColor.setStyleSheet(
                'background-color: %s' % background.name())

            current_tab.format_view(
                None, None, None, background, padding, None, None)

        else:
            profile_index = self.bookToolBar.profileBox.currentIndex()
            current_profile = self.bookToolBar.profileBox.itemData(
                profile_index, QtCore.Qt.UserRole)

            font = current_profile['font']
            foreground = current_profile['foreground']
            background = current_profile['background']
            padding = current_profile['padding']
            font_size = current_profile['font_size']
            line_spacing = current_profile['line_spacing']
            text_alignment = current_profile['text_alignment']

            # Change toolbar widgets to match new settings
            self.bookToolBar.fontBox.blockSignals(True)
            self.bookToolBar.fontSizeBox.blockSignals(True)
            self.bookToolBar.fontBox.setCurrentText(font)
            current_index = self.bookToolBar.fontSizeBox.findText(
                str(font_size), QtCore.Qt.MatchExactly)
            self.bookToolBar.fontSizeBox.setCurrentIndex(current_index)
            self.bookToolBar.fontBox.blockSignals(False)
            self.bookToolBar.fontSizeBox.blockSignals(False)

            self.alignment_dict[current_profile['text_alignment']].setChecked(True)

            self.bookToolBar.colorBoxFG.setStyleSheet(
                'background-color: %s' % foreground.name())
            self.bookToolBar.colorBoxBG.setStyleSheet(
                'background-color: %s' % background.name())

            current_tab.format_view(
                font, font_size, foreground,
                background, padding, line_spacing,
                text_alignment)

    def reset_profile(self):
        current_profile_index = self.bookToolBar.profileBox.currentIndex()
        current_profile_default = Settings(self).default_profiles[current_profile_index]
        self.bookToolBar.profileBox.setItemData(
            current_profile_index, current_profile_default, QtCore.Qt.UserRole)
        self.format_contentView()

    def show_settings(self):
        if not self.settingsDialog.isVisible():
            self.settingsDialog.show()
        else:
            self.settingsDialog.hide()

    def generate_library_context_menu(self, position):
        index = self.sender().indexAt(position)
        if not index.isValid():
            return

        # It's worth remembering that these are indexes of the view_model
        # and NOT of the proxy models
        selected_indexes = self.get_selection(self.sender())

        context_menu = QtWidgets.QMenu()

        openAction = context_menu.addAction(
            self.QImageFactory.get_image('view-readermode'),
            self._translate('Main_UI', 'Start reading'))

        editAction = None
        if len(selected_indexes) == 1:
            editAction = context_menu.addAction(
                self.QImageFactory.get_image('edit-rename'),
                self._translate('Main_UI', 'Edit'))

        deleteAction = context_menu.addAction(
            self.QImageFactory.get_image('trash-empty'),
            self._translate('Main_UI', 'Delete'))
        readAction = context_menu.addAction(
            QtGui.QIcon(':/images/checkmark.svg'),
            self._translate('Main_UI', 'Mark read'))
        unreadAction = context_menu.addAction(
            QtGui.QIcon(':/images/xmark.svg'),
            self._translate('Main_UI', 'Mark unread'))

        action = context_menu.exec_(self.sender().mapToGlobal(position))

        if action == openAction:
            books_to_open = {}
            for i in selected_indexes:
                metadata = self.lib_ref.view_model.data(i, QtCore.Qt.UserRole + 3)
                books_to_open[metadata['path']] = metadata['hash']

            self.open_files(books_to_open)

        if action == editAction:
            edit_book = selected_indexes[0]
            metadata = self.lib_ref.view_model.data(
                edit_book, QtCore.Qt.UserRole + 3)
            is_cover_loaded = self.lib_ref.view_model.data(
                edit_book, QtCore.Qt.UserRole + 8)

            # Loads a cover in case culling is enabled and the table view is visible
            if not is_cover_loaded:
                book_hash = self.lib_ref.view_model.data(
                    edit_book, QtCore.Qt.UserRole + 6)
                book_item = self.lib_ref.view_model.item(edit_book.row())
                book_cover = database.DatabaseFunctions(
                    self.database_path).fetch_covers_only([book_hash])[0][1]
                self.cover_loader(book_item, book_cover)

            cover = self.lib_ref.view_model.item(edit_book.row()).icon()
            title = metadata['title']
            author = metadata['author']
            year = str(metadata['year'])
            tags = metadata['tags']

            self.metadataDialog.load_book(
                cover, title, author, year, tags, edit_book)

            self.metadataDialog.show()

        if action == deleteAction:
            self.delete_books(selected_indexes)

        if action == readAction or action == unreadAction:
            for i in selected_indexes:
                metadata = self.lib_ref.view_model.data(i, QtCore.Qt.UserRole + 3)
                book_hash = self.lib_ref.view_model.data(i, QtCore.Qt.UserRole + 6)
                position = metadata['position']

                if position:
                    if action == readAction:
                        position['is_read'] = True
                        position['scroll_value'] = 1
                    elif action == unreadAction:
                        position['is_read'] = False
                        position['current_chapter'] = 1
                        position['scroll_value'] = 0
                else:
                    position = {}
                    if action == readAction:
                        position['is_read'] = True

                metadata['position'] = position

                position_perc = None
                last_accessed_time = None
                if action == readAction:
                    last_accessed_time = QtCore.QDateTime().currentDateTime()
                    position_perc = 100

                self.lib_ref.view_model.setData(i, metadata, QtCore.Qt.UserRole + 3)
                self.lib_ref.view_model.setData(i, position_perc, QtCore.Qt.UserRole + 7)
                self.lib_ref.view_model.setData(i, last_accessed_time, QtCore.Qt.UserRole + 12)
                self.lib_ref.update_proxymodels()

                database_dict = {
                    'Position': position,
                    'LastAccessed': last_accessed_time}

                database.DatabaseFunctions(
                    self.database_path).modify_metadata(database_dict, book_hash)

    def generate_library_filter_menu(self, directory_list=None):
        self.libraryFilterMenu.clear()

        def generate_name(path_data):
            this_filter = path_data[1]
            if not this_filter:
                this_filter = os.path.basename(
                    path_data[0]).title()
            return this_filter

        filter_actions = []
        filter_list = []
        if directory_list:
            checked = [i for i in directory_list if i[3] == QtCore.Qt.Checked]
            filter_list = list(map(generate_name, checked))
            filter_list.sort()
            filter_list.append(self._translate('Main_UI', 'Manually Added'))
            filter_actions = [QtWidgets.QAction(i, self.libraryFilterMenu) for i in filter_list]

        filter_all = QtWidgets.QAction('All', self.libraryFilterMenu)
        filter_actions.append(filter_all)
        for i in filter_actions:
            i.setCheckable(True)
            i.setChecked(True)
            i.triggered.connect(self.set_library_filter)

        self.libraryFilterMenu.addActions(filter_actions)
        self.libraryFilterMenu.insertSeparator(filter_all)
        self.libraryToolBar.libraryFilterButton.setMenu(self.libraryFilterMenu)

    def set_library_filter(self, event=None):
        self.active_library_filters = []
        something_was_unchecked = False

        if self.sender():  # Program startup sends a None here
            if self.sender().text() == 'All':
                for i in self.libraryFilterMenu.actions():
                    i.setChecked(self.sender().isChecked())

        for i in self.libraryFilterMenu.actions()[:-2]:
            if i.isChecked():
                self.active_library_filters.append(i.text())
            else:
                something_was_unchecked = True

        if something_was_unchecked:
            self.libraryFilterMenu.actions()[-1].setChecked(False)
        else:
            self.libraryFilterMenu.actions()[-1].setChecked(True)

        self.lib_ref.update_proxymodels()

    def toggle_distraction_free(self):
        self.settings['show_bars'] = not self.settings['show_bars']

        self.statusBar.setVisible(
            not self.statusBar.isVisible())
        self.tabWidget.tabBar().setVisible(
            not self.tabWidget.tabBar().isVisible())

        current_tab = self.tabWidget.currentIndex()
        if current_tab == 0:
            self.libraryToolBar.setVisible(
                not self.libraryToolBar.isVisible())
        else:
            self.bookToolBar.setVisible(
                not self.bookToolBar.isVisible())

    def closeEvent(self, event=None):
        if event:
            event.ignore()

        self.hide()
        self.metadataDialog.hide()
        self.settingsDialog.hide()
        self.definitionDialog.hide()
        self.temp_dir.remove()

        self.settings['last_open_books'] = []
        if self.tabWidget.count() > 1:

            # All tabs must be iterated upon here
            all_metadata = []
            for i in range(1, self.tabWidget.count()):
                tab_metadata = self.tabWidget.widget(i).metadata
                all_metadata.append(tab_metadata)

                if self.settings['remember_files']:
                    self.settings['last_open_books'].append(tab_metadata['path'])

            Settings(self).save_settings()
            self.thread = BackGroundTabUpdate(
                self.database_path, all_metadata)
            self.thread.finished.connect(self.database_care)
            self.thread.start()

        else:
            Settings(self).save_settings()
            self.database_care()

    def database_care(self):
        database.DatabaseFunctions(self.database_path).vacuum_database()
        QtWidgets.qApp.exit()