# This file is a part of Lector, a Qt based ebook reader # Copyright (C) 2017-2019 BasioMeusPuga # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # TODO # Get Cancel working with the file system model import os import copy import logging import pathlib from PyQt5 import QtWidgets, QtCore, QtGui from lector import database from lector.annotations import AnnotationsUI from lector.models import MostExcellentFileSystemModel from lector.threaded import BackGroundBookSearch, BackGroundBookAddition from lector.resources import settingswindow from lector.settings import Settings from lector.logger import logger_filename, VERSION logger = logging.getLogger(__name__) class SettingsUI(QtWidgets.QDialog, settingswindow.Ui_Dialog): def __init__(self, parent=None): super(SettingsUI, self).__init__() self.setupUi(self) self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) self._translate = QtCore.QCoreApplication.translate self.main_window = parent self.database_path = self.main_window.database_path self.image_factory = self.main_window.QImageFactory # The annotation dialog will use the settings dialog as its parent self.annotationsDialog = AnnotationsUI(self) self.resize(self.main_window.settings['settings_dialog_size']) self.move(self.main_window.settings['settings_dialog_position']) install_dir = os.path.realpath(__file__) install_dir = pathlib.Path(install_dir).parents[1] aboutfile_path = os.path.join(install_dir, 'lector', 'resources', 'about.html') with open(aboutfile_path) as about_html: html = about_html.readlines() html.insert( 8, f'

v{VERSION}

\n') self.aboutBox.setHtml(''.join(html)) self.paths = None self.thread = None self.filesystemModel = None self.tag_data_copy = None english_string = self._translate('SettingsUI', 'English') spanish_string = self._translate('SettingsUI', 'Spanish') hindi_string = self._translate('SettingsUI', 'Hindi') languages = [english_string, spanish_string, hindi_string] self.languageBox.addItems(languages) current_language = self.main_window.settings['dictionary_language'] if current_language == 'en': self.languageBox.setCurrentIndex(0) elif current_language == 'es': self.languageBox.setCurrentIndex(1) else: self.languageBox.setCurrentIndex(2) self.languageBox.activated.connect(self.change_dictionary_language) self.okButton.setToolTip( self._translate('SettingsUI', 'Save changes and start library scan')) self.okButton.clicked.connect(self.start_library_scan) self.cancelButton.clicked.connect(self.cancel_pressed) # Radio buttons if self.main_window.settings['icon_theme'] == 'DarkIcons': self.darkIconsRadio.setChecked(True) else: self.lightIconsRadio.setChecked(True) self.darkIconsRadio.clicked.connect(self.change_icon_theme) self.lightIconsRadio.clicked.connect(self.change_icon_theme) # Check boxes self.autoTags.setChecked(self.main_window.settings['auto_tags']) self.coverShadows.setChecked(self.main_window.settings['cover_shadows']) self.refreshLibrary.setChecked(self.main_window.settings['scan_library']) self.fileRemember.setChecked(self.main_window.settings['remember_files']) self.performCulling.setChecked(self.main_window.settings['perform_culling']) self.cachingEnabled.setChecked(self.main_window.settings['caching_enabled']) self.hideScrollBars.setChecked(self.main_window.settings['hide_scrollbars']) self.attenuateTitles.setChecked(self.main_window.settings['attenuate_titles']) self.autoCover.setChecked(self.main_window.settings['auto_cover']) self.scrollSpeedSlider.setValue(self.main_window.settings['scroll_speed']) self.readAtPercent.setValue(self.main_window.settings['consider_read_at']) self.smallIncrementBox.setValue(self.main_window.settings['small_increment']) self.largeIncrementBox.setValue(self.main_window.settings['large_increment']) self.autoTags.clicked.connect(self.manage_checkboxes) self.coverShadows.clicked.connect(self.manage_checkboxes) self.refreshLibrary.clicked.connect(self.manage_checkboxes) self.fileRemember.clicked.connect(self.manage_checkboxes) self.performCulling.clicked.connect(self.manage_checkboxes) self.cachingEnabled.clicked.connect(self.manage_checkboxes) self.hideScrollBars.clicked.connect(self.manage_checkboxes) self.attenuateTitles.clicked.connect(self.manage_checkboxes) self.autoCover.clicked.connect(self.manage_checkboxes) self.scrollSpeedSlider.valueChanged.connect(self.change_scroll_speed) self.readAtPercent.valueChanged.connect(self.change_read_at) self.smallIncrementBox.valueChanged.connect(self.change_increment) self.largeIncrementBox.valueChanged.connect(self.change_increment) # Generate the QStandardItemModel for the listView self.listModel = QtGui.QStandardItemModel(self.listView) library_string = self._translate('SettingsUI', 'Library') switches_string = self._translate('SettingsUI', 'Switches') annotations_string = self._translate('SettingsUI', 'Annotations') about_string = self._translate('SettingsUI', 'About') list_options = [ library_string, switches_string, annotations_string, about_string] icon_dict = { 0: 'view-readermode', 1: 'switches', 2: 'annotate', 3: 'about'} for count, i in enumerate(list_options): item = QtGui.QStandardItem() item.setText(i) this_icon = icon_dict[count] item.setIcon( self.main_window.QImageFactory.get_image(this_icon)) self.listModel.appendRow(item) self.listView.setModel(self.listModel) # Custom signal to account for page changes self.listView.newIndexSignal.connect(self.list_index_changed) # Annotation related buttons # Icon names self.newAnnotation.setIcon(self.image_factory.get_image('add')) self.deleteAnnotation.setIcon(self.image_factory.get_image('remove')) self.editAnnotation.setIcon(self.image_factory.get_image('edit-rename')) self.moveUp.setIcon(self.image_factory.get_image('arrow-up')) self.moveDown.setIcon(self.image_factory.get_image('arrow-down')) # Icon sizes self.newAnnotation.setIconSize(QtCore.QSize(24, 24)) self.deleteAnnotation.setIconSize(QtCore.QSize(24, 24)) self.editAnnotation.setIconSize(QtCore.QSize(24, 24)) self.moveUp.setIconSize(QtCore.QSize(24, 24)) self.moveDown.setIconSize(QtCore.QSize(24, 24)) self.annotationsList.clicked.connect(self.load_annotation) self.annotationsList.doubleClicked.connect(self.editAnnotation.click) self.newAnnotation.clicked.connect(self.add_annotation) self.deleteAnnotation.clicked.connect(self.delete_annotation) self.editAnnotation.clicked.connect(self.load_annotation) self.moveUp.clicked.connect(self.move_annotation) self.moveDown.clicked.connect(self.move_annotation) # Generate annotation settings self.annotationModel = QtGui.QStandardItemModel() self.generate_annotations() # Generate the filesystem treeView self.generate_tree() # About... About self.aboutTabWidget.setDocumentMode(True) self.aboutTabWidget.setContentsMargins(0, 0, 0, 0) self.logBox.setReadOnly(True) # About buttons self.resetButton.clicked.connect(self.delete_database) self.clearLogButton.clicked.connect(self.clear_log) # Hide the image annotation tab # TODO # Maybe get off your lazy ass and write something for this self.tabWidget.setContentsMargins(0, 0, 0, 0) self.tabWidget.tabBar().setVisible(False) def list_index_changed(self, index): switch_to = index.row() self.stackedWidget.setCurrentIndex(switch_to) valid_buttons = { 0: (self.okButton,), 3: (self.resetButton, self.clearLogButton),} for i in valid_buttons: if i == switch_to: for j in valid_buttons[i]: j.setVisible(True) else: for j in valid_buttons[i]: j.setVisible(False) def generate_tree(self): # Fetch all directories in the database paths = database.DatabaseFunctions( self.database_path).fetch_data( ('Path', 'Name', 'Tags', 'CheckState'), 'directories', {'Path': ''}, 'LIKE') self.main_window.generate_library_filter_menu(paths) directory_data = {} if not paths: logger.warning('No book paths saved') else: # Convert to the dictionary format that is # to be fed into the QFileSystemModel for i in paths: directory_data[i[0]] = { 'name': i[1], 'tags': i[2], 'check_state': i[3]} self.filesystemModel = MostExcellentFileSystemModel(directory_data) self.filesystemModel.setFilter( QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Dirs) self.treeView.setModel(self.filesystemModel) # TODO # This here might break on them pestilent non unixy OSes # Check and see root_directory = QtCore.QDir().rootPath() self.treeView.setRootIndex( self.filesystemModel.setRootPath(root_directory)) # Set the treeView and QFileSystemModel to its desired state selected_paths = [ i for i in directory_data if directory_data[i]['check_state'] == QtCore.Qt.Checked] expand_paths = set() for i in selected_paths: # Recursively grind down parent paths for expansion this_path = i while True: parent_path = os.path.dirname(this_path) if parent_path == this_path: break expand_paths.add(parent_path) this_path = parent_path # Expand all the parent paths derived from the selected path if root_directory in expand_paths: expand_paths.remove(root_directory) for i in expand_paths: this_index = self.filesystemModel.index(i) self.treeView.expand(this_index) header_sizes = self.main_window.settings['settings_dialog_headers'] if header_sizes: for count, i in enumerate((0, 4)): self.treeView.setColumnWidth(i, int(header_sizes[count])) # TODO # Set a QSortFilterProxy model on top of the existing QFileSystem model # self.filesystem_proxy_model = FileSystemProxyModel() # self.filesystem_proxy_model.setSourceModel(self.filesystem_model) # self.treeView.setModel(self.filesystem_proxy_model) for i in range(1, 4): self.treeView.hideColumn(i) def start_library_scan(self): self.hide() data_pairs = [] for i in self.filesystemModel.tag_data.items(): data_pairs.append([ i[0], i[1]['name'], i[1]['tags'], i[1]['check_state'] ]) database.DatabaseFunctions( self.database_path).set_library_paths(data_pairs) if not data_pairs: logger.error('Can\'t scan - No book paths saved') try: if self.sender().objectName() == 'reloadLibrary': self.show() treeViewIndex = self.listModel.index(0, 0) self.listView.setCurrentIndex(treeViewIndex) return except AttributeError: pass database.DatabaseFunctions( self.database_path).delete_from_database('*', '*') self.main_window.lib_ref.generate_model('build') self.main_window.lib_ref.generate_proxymodels() self.main_window.generate_library_filter_menu() return # Update the main window library filter menu self.main_window.generate_library_filter_menu(data_pairs) self.main_window.set_library_filter() # Disallow rechecking until the first check completes self.okButton.setEnabled(False) self.main_window.libraryToolBar.reloadLibraryButton.setEnabled(False) self.okButton.setToolTip( self._translate('SettingsUI', 'Library scan in progress...')) # Traverse directories looking for files self.main_window.statusMessage.setText( self._translate('SettingsUI', 'Checking library folders')) self.thread = BackGroundBookSearch(data_pairs) self.thread.finished.connect(self.finished_iterating) self.thread.start() def finished_iterating(self): # The books the search thread has found # are now in self.thread.valid_files if not self.thread.valid_files: self.main_window.move_on() return # Hey, messaging is important, okay? self.main_window.statusBar.setVisible(True) self.main_window.sorterProgress.setVisible(True) self.main_window.statusMessage.setText( self._translate('SettingsUI', 'Parsing files')) # We now create a new thread to put those files into the database self.thread = BackGroundBookAddition( self.thread.valid_files, self.database_path, 'automatic', self.main_window) self.thread.finished.connect( lambda: self.main_window.move_on(self.thread.errors)) self.thread.start() def cancel_pressed(self): self.filesystemModel.tag_data = copy.deepcopy(self.tag_data_copy) self.hide() def hideEvent(self, event): self.no_more_settings() event.accept() def showEvent(self, event): # Load log into the plainTextEdit with open(logger_filename) as infile: log_text = infile.read() self.logBox.setPlainText(log_text) # Annotation preview self.format_preview() # Make copy of tags in case of a nope.jpg self.tag_data_copy = copy.deepcopy(self.filesystemModel.tag_data) event.accept() def no_more_settings(self): self.main_window.libraryToolBar.settingsButton.setChecked(False) self.gather_annotations() Settings(self.main_window).save_settings() Settings(self.main_window).read_settings() self.main_window.settings['last_open_tab'] = None # Needed to allow focus change # to newly opened book self.resizeEvent() def resizeEvent(self, event=None): self.main_window.settings['settings_dialog_size'] = self.size() self.main_window.settings['settings_dialog_position'] = self.pos() table_headers = [] for i in [0, 4]: table_headers.append(self.treeView.columnWidth(i)) self.main_window.settings['settings_dialog_headers'] = table_headers def change_icon_theme(self): if self.sender() == self.darkIconsRadio: self.main_window.settings['icon_theme'] = 'DarkIcons' else: self.main_window.settings['icon_theme'] = 'LightIcons' def change_dictionary_language(self, event): language_dict = { 0: 'en', 1: 'es', 2: 'hi'} self.main_window.settings[ 'dictionary_language'] = language_dict[self.languageBox.currentIndex()] def change_scroll_speed(self, event=None): self.main_window.settings['scroll_speed'] = self.scrollSpeedSlider.value() def change_read_at(self, event=None): self.main_window.settings['consider_read_at'] = self.readAtPercent.value() def change_increment(self, event=None): self.main_window.settings['small_increment'] = self.smallIncrementBox.value() self.main_window.settings['large_increment'] = self.largeIncrementBox.value() def manage_checkboxes(self, event=None): sender = self.sender().objectName() sender_dict = { 'coverShadows': 'cover_shadows', 'autoTags': 'auto_tags', 'refreshLibrary': 'scan_library', 'fileRemember': 'remember_files', 'performCulling': 'perform_culling', 'cachingEnabled': 'caching_enabled', 'hideScrollBars': 'hide_scrollbars', 'attenuateTitles': 'attenuate_titles', 'autoCover': 'auto_cover'} self.main_window.settings[ sender_dict[sender]] = not self.main_window.settings[sender_dict[sender]] if not self.performCulling.isChecked(): self.main_window.cover_functions.load_all_covers() def generate_annotations(self): saved_annotations = self.main_window.settings['annotations'] for i in saved_annotations: item = QtGui.QStandardItem() item.setText(i['name']) item.setData(i, QtCore.Qt.UserRole) self.annotationModel.appendRow(item) self.annotationsList.setModel(self.annotationModel) def format_preview(self): # Needed to clear the preview of annotation ickiness cursor = QtGui.QTextCursor() self.previewView.setTextCursor(cursor) self.previewView.setText('Vidistine nuper imagines moventes bonas?') profile_index = self.main_window.bookToolBar.profileBox.currentIndex() current_profile = self.main_window.bookToolBar.profileBox.itemData( profile_index, QtCore.Qt.UserRole) if not current_profile: return font = current_profile['font'] self.foreground = current_profile['foreground'] background = current_profile['background'] font_size = current_profile['font_size'] self.previewView.setStyleSheet( "QTextEdit {{font-family: {0}; font-size: {1}px; color: {2}; background-color: {3}}}".format( font, font_size, self.foreground.name(), background.name())) block_format = QtGui.QTextBlockFormat() block_format.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter) cursor = self.previewView.textCursor() while True: old_position = cursor.position() cursor.mergeBlockFormat(block_format) cursor.movePosition(QtGui.QTextCursor.NextBlock, 0, 1) new_position = cursor.position() if old_position == new_position: break def add_annotation(self): self.annotationsDialog.show_dialog('add') def delete_annotation(self): selected_index = self.annotationsList.currentIndex() if not selected_index.isValid(): return self.annotationModel.removeRow( self.annotationsList.currentIndex().row()) self.format_preview() self.annotationsList.clearSelection() def load_annotation(self): selected_index = self.annotationsList.currentIndex() if not selected_index.isValid(): return if self.sender() == self.annotationsList: self.annotationsDialog.show_dialog('preview', selected_index) elif self.sender() == self.editAnnotation: self.annotationsDialog.show_dialog('edit', selected_index) def move_annotation(self): current_row = self.annotationsList.currentIndex().row() if self.sender() == self.moveUp: new_row = current_row - 1 if new_row < 0: return elif self.sender() == self.moveDown: new_row = current_row + 1 if new_row == self.annotationModel.rowCount(): return row_out = self.annotationModel.takeRow(current_row) self.annotationModel.insertRow(new_row, row_out) new_index = self.annotationModel.index(new_row, 0) self.annotationsList.setCurrentIndex(new_index) def gather_annotations(self): annotations_out = [] for i in range(self.annotationModel.rowCount()): annotation_item = self.annotationModel.item(i, 0) annotation_data = annotation_item.data(QtCore.Qt.UserRole) annotations_out.append(annotation_data) self.main_window.settings['annotations'] = annotations_out def delete_database(self): def ifcontinue(box_button): if box_button.text() != '&Yes': return database_filename = os.path.join( self.main_window.database_path, 'Lector.db') os.remove(database_filename) QtWidgets.qApp.exit() # Generate a message box to confirm deletion confirm_deletion = QtWidgets.QMessageBox() deletion_prompt = self._translate( 'SettingsUI', f'Delete database and exit?') confirm_deletion.setText(deletion_prompt) confirm_deletion.setIcon(QtWidgets.QMessageBox.Critical) confirm_deletion.setWindowTitle(self._translate('SettingsUI', 'Confirm')) confirm_deletion.setStandardButtons( QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) confirm_deletion.buttonClicked.connect(ifcontinue) confirm_deletion.show() confirm_deletion.exec_() def clear_log(self): self.logBox.clear() open(logger_filename, 'w').close()