# 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 <http://www.gnu.org/licenses/>. import logging import pathlib from PyQt5 import QtCore, QtWidgets from lector.resources import pie_chart logger = logging.getLogger(__name__) class BookmarkProxyModel(QtCore.QSortFilterProxyModel): def __init__(self, parent=None): super(BookmarkProxyModel, self).__init__(parent) self.parent = parent self.parentTab = self.parent.parent self.filter_text = None def setFilterParams(self, filter_text): self.filter_text = filter_text def setData(self, index, value, role): if role == QtCore.Qt.EditRole: source_index = self.mapToSource(index) identifier = self.sourceModel().data(source_index, QtCore.Qt.UserRole + 2) self.sourceModel().setData(source_index, value, QtCore.Qt.DisplayRole) self.parentTab.metadata['bookmarks'][identifier]['description'] = value return True class ItemProxyModel(QtCore.QSortFilterProxyModel): def __init__(self, parent=None): super(ItemProxyModel, self).__init__(parent) self.filter_text = None self.active_library_filters = None self.sorting_box_position = None self.common_functions = ProxyModelsCommonFunctions(self) def setFilterParams(self, filter_text, active_library_filters, sorting_box_position): self.common_functions.setFilterParams( filter_text, active_library_filters, sorting_box_position) def filterAcceptsRow(self, row, parent): output = self.common_functions.filterAcceptsRow(row, parent) return output class TableProxyModel(QtCore.QSortFilterProxyModel): def __init__(self, temp_dir, tableViewHeader, consider_read_at, parent=None): super(TableProxyModel, self).__init__(parent) self.tableViewHeader = tableViewHeader self.consider_read_at = consider_read_at self._translate = QtCore.QCoreApplication.translate title_string = self._translate('TableProxyModel', 'Title') author_string = self._translate('TableProxyModel', 'Author') year_string = self._translate('TableProxyModel', 'Year') lastread_string = self._translate('TableProxyModel', 'Last Read') tags_string = self._translate('TableProxyModel', 'Tags') self.header_data = [ None, title_string, author_string, year_string, lastread_string, '%', tags_string] self.temp_dir = temp_dir self.filter_text = None self.active_library_filters = None self.sorting_box_position = None self.role_dictionary = { 1: QtCore.Qt.UserRole, # Title 2: QtCore.Qt.UserRole + 1, # Author 3: QtCore.Qt.UserRole + 2, # Year 4: QtCore.Qt.UserRole + 12, # Last read 5: QtCore.Qt.UserRole + 7, # Position percentage 6: QtCore.Qt.UserRole + 4} # Tags self.common_functions = ProxyModelsCommonFunctions(self) def columnCount(self, parent): return 7 def headerData(self, column, orientation, role): if role == QtCore.Qt.DisplayRole: try: return self.header_data[column] except IndexError: logger.error( 'Table proxy model: Can\'t find header for column' + str(column)) # The column will be called IndexError. Not a typo. return 'IndexError' def flags(self, index): # Tag editing will take place by way of a right click menu # These tags denote clickable and that's about it return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable def data(self, index, role): source_index = self.mapToSource(index) item = self.sourceModel().item(source_index.row(), 0) if role == QtCore.Qt.TextAlignmentRole: if index.column() in (3, 4): return QtCore.Qt.AlignHCenter if role == QtCore.Qt.DecorationRole: if index.column() == 5: return_pixmap = None file_exists = item.data(QtCore.Qt.UserRole + 5) position_percent = item.data(QtCore.Qt.UserRole + 7) if not file_exists: return pie_chart.pixmapper( -1, None, -1, QtCore.Qt.SizeHintRole + 10) if position_percent: return_pixmap = pie_chart.pixmapper( position_percent, self.temp_dir, self.consider_read_at, QtCore.Qt.SizeHintRole + 10) return return_pixmap elif role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: if index.column() in (0, 5): # Cover and Status return QtCore.QVariant() if index.column() == 4: last_accessed = item.data(self.role_dictionary[index.column()]) if last_accessed: right_now = QtCore.QDateTime().currentDateTime() time_diff = last_accessed.msecsTo(right_now) return self.time_convert(time_diff // 1000) return item.data(self.role_dictionary[index.column()]) else: return QtCore.QVariant() def setFilterParams(self, filter_text, active_library_filters, sorting_box_position): self.common_functions.setFilterParams( filter_text, active_library_filters, sorting_box_position) def filterAcceptsRow(self, row, parent): output = self.common_functions.filterAcceptsRow(row, parent) return output def sort_table_columns(self, column=None): column = self.tableViewHeader.sortIndicatorSection() sorting_order = self.tableViewHeader.sortIndicatorOrder() self.sort(0, sorting_order) if column != 0: self.setSortRole(self.role_dictionary[column]) def time_convert(self, seconds): seconds = int(seconds) m, s = divmod(seconds, 60) h, m = divmod(m, 60) d, h = divmod(h, 24) if d > 0: return f'{d}d' if h > 0: return f'{h}h' if m > 0: return f'{m}m' else: return '<1m' class ProxyModelsCommonFunctions: def __init__(self, parent_model): self.parent_model = parent_model def setFilterParams(self, filter_text, active_library_filters, sorting_box_position): self.parent_model.filter_text = filter_text self.parent_model.active_library_filters = [i.lower() for i in active_library_filters] self.parent_model.sorting_box_position = sorting_box_position def filterAcceptsRow(self, row, parent): model = self.parent_model.sourceModel() this_index = model.index(row, 0) title = model.data(this_index, QtCore.Qt.UserRole) author = model.data(this_index, QtCore.Qt.UserRole + 1) tags = model.data(this_index, QtCore.Qt.UserRole + 4) progress = model.data(this_index, QtCore.Qt.UserRole + 7) directory_name = model.data(this_index, QtCore.Qt.UserRole + 10) directory_tags = model.data(this_index, QtCore.Qt.UserRole + 11) last_accessed = model.data(this_index, QtCore.Qt.UserRole + 12) file_path = model.data(this_index, QtCore.Qt.UserRole + 13) # Hide untouched files when sorting by last accessed if self.parent_model.sorting_box_position == 4 and not last_accessed: return False # Hide untouched files when sorting by progress if self.parent_model.sorting_box_position == 5 and not progress: return False if self.parent_model.active_library_filters: if directory_name not in self.parent_model.active_library_filters: return False else: return False if not self.parent_model.filter_text: return True else: valid_data = [ i.lower() for i in ( title, author, tags, directory_name, directory_tags, file_path) if i is not None] for i in valid_data: if self.parent_model.filter_text.lower() in i: return True return False class MostExcellentFileSystemModel(QtWidgets.QFileSystemModel): # Directories are tracked on the basis of their paths # Poll the tag_data dictionary to get User selection def __init__(self, tag_data, parent=None): super(MostExcellentFileSystemModel, self).__init__(parent) self.tag_data = tag_data self.field_dict = { 0: 'check_state', 4: 'name', 5: 'tags'} def columnCount(self, parent): # The QFileSystemModel returns 4 columns by default # Columns 1, 2, 3 will be present but hidden return 6 def headerData(self, col, orientation, role): # Columns not mentioned here will be hidden if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: column_dict = { 0: 'Path', 4: 'Name', 5: 'Tags'} try: return column_dict[col] except KeyError: pass def data(self, index, role): if (index.column() in (4, 5) and (role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole)): read_field = self.field_dict[index.column()] try: return self.tag_data[self.filePath(index)][read_field] except KeyError: return QtCore.QVariant() if role == QtCore.Qt.CheckStateRole and index.column() == 0: return self.checkState(index) return QtWidgets.QFileSystemModel.data(self, index, role) def flags(self, index): if index.column() in (4, 5): return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable else: return QtWidgets.QFileSystemModel.flags(self, index) | QtCore.Qt.ItemIsUserCheckable def checkState(self, index): while index.isValid(): index_path = self.filePath(index) if index_path in self.tag_data: return self.tag_data[index_path]['check_state'] index = index.parent() return QtCore.Qt.Unchecked def setData(self, index, value, role): if (role == QtCore.Qt.EditRole or role == QtCore.Qt.CheckStateRole) and index.isValid(): write_field = self.field_dict[index.column()] self.layoutAboutToBeChanged.emit() this_path = self.filePath(index) if this_path not in self.tag_data: self.populate_dictionary(this_path) self.tag_data[this_path][write_field] = value self.depopulate_dictionary() self.layoutChanged.emit() return True def populate_dictionary(self, path): self.tag_data[path] = {} self.tag_data[path]['name'] = None self.tag_data[path]['tags'] = None self.tag_data[path]['check_state'] = QtCore.Qt.Checked def depopulate_dictionary(self): # This keeps the tag_data dictionary manageable as well as preventing # weird ass behaviour when something is deselected and its tags are cleared deletable = set() for i in self.tag_data.items(): all_data = [j[1] for j in i[1].items()] filtered_down = list(filter(lambda x: x is not None and x != 0, all_data)) if not filtered_down: deletable.add(i[0]) # Get untagged subdirectories too all_dirs = [i for i in self.tag_data] all_dirs.sort() def is_child(this_dir): this_path = pathlib.Path(this_dir) for i in all_dirs: if pathlib.Path(i) in this_path.parents: # If a parent folder has tags, we only want the deletion # to kick in in case the parent is also checked if self.tag_data[i]['check_state'] == QtCore.Qt.Checked: return True return False for i in all_dirs: if is_child(i): dir_tags = (self.tag_data[i]['name'], self.tag_data[i]['tags']) filtered_down = list(filter(lambda x: x is not None and x != '', dir_tags)) if not filtered_down: deletable.add(i) for i in deletable: del self.tag_data[i]