def __init__(self, cover_thread_count=2, detail_thread_count=4): QAbstractItemModel.__init__(self) self.DRM_LOCKED_ICON = QPixmap(I('drm-locked.png')).scaledToHeight( 64, Qt.SmoothTransformation) self.DRM_UNLOCKED_ICON = QPixmap(I('drm-unlocked.png')).scaledToHeight( 64, Qt.SmoothTransformation) self.DRM_UNKNOWN_ICON = QPixmap( I('dialog_question.png')).scaledToHeight(64, Qt.SmoothTransformation) self.DONATE_ICON = QPixmap(I('donate.png')).scaledToHeight( 16, Qt.SmoothTransformation) self.DOWNLOAD_ICON = QPixmap(I('arrow-down.png')).scaledToHeight( 16, Qt.SmoothTransformation) # All matches. Used to determine the order to display # self.matches because the SearchFilter returns # matches unordered. self.all_matches = [] # Only the showing matches. self.matches = [] self.query = '' self.filterable_query = False self.search_filter = SearchFilter() self.cover_pool = CoverThreadPool(cover_thread_count) self.details_pool = DetailsThreadPool(detail_thread_count) self.filter_results_dispatcher = FunctionDispatcher( self.filter_results) self.got_result_details_dispatcher = FunctionDispatcher( self.got_result_details) self.sort_col = 2 self.sort_order = Qt.AscendingOrder
def __init__(self, cover_thread_count=2, detail_thread_count=4): QAbstractItemModel.__init__(self) self.DRM_LOCKED_ICON = QPixmap(I('drm-locked.png')).scaledToHeight(64, Qt.SmoothTransformation) self.DRM_UNLOCKED_ICON = QPixmap(I('drm-unlocked.png')).scaledToHeight(64, Qt.SmoothTransformation) self.DRM_UNKNOWN_ICON = QPixmap(I('dialog_question.png')).scaledToHeight(64, Qt.SmoothTransformation) self.DONATE_ICON = QPixmap(I('donate.png')).scaledToHeight(16, Qt.SmoothTransformation) self.DOWNLOAD_ICON = QPixmap(I('arrow-down.png')).scaledToHeight(16, Qt.SmoothTransformation) # All matches. Used to determine the order to display # self.matches because the SearchFilter returns # matches unordered. self.all_matches = [] # Only the showing matches. self.matches = [] self.query = '' self.filterable_query = False self.search_filter = SearchFilter() self.cover_pool = CoverThreadPool(cover_thread_count) self.details_pool = DetailsThreadPool(detail_thread_count) self.filter_results_dispatcher = FunctionDispatcher(self.filter_results) self.got_result_details_dispatcher = FunctionDispatcher(self.got_result_details) self.sort_col = 2 self.sort_order = Qt.AscendingOrder
class Matches(QAbstractItemModel): total_changed = pyqtSignal(int) HEADERS = [ _('Cover'), _('Title'), _('Price'), _('DRM'), _('Store'), _('Download'), _('Affiliate') ] HTML_COLS = (1, 4) def __init__(self, cover_thread_count=2, detail_thread_count=4): QAbstractItemModel.__init__(self) self.DRM_LOCKED_ICON = QPixmap(I('drm-locked.png')).scaledToHeight( 64, Qt.SmoothTransformation) self.DRM_UNLOCKED_ICON = QPixmap(I('drm-unlocked.png')).scaledToHeight( 64, Qt.SmoothTransformation) self.DRM_UNKNOWN_ICON = QPixmap( I('dialog_question.png')).scaledToHeight(64, Qt.SmoothTransformation) self.DONATE_ICON = QPixmap(I('donate.png')).scaledToHeight( 16, Qt.SmoothTransformation) self.DOWNLOAD_ICON = QPixmap(I('arrow-down.png')).scaledToHeight( 16, Qt.SmoothTransformation) # All matches. Used to determine the order to display # self.matches because the SearchFilter returns # matches unordered. self.all_matches = [] # Only the showing matches. self.matches = [] self.query = '' self.filterable_query = False self.search_filter = SearchFilter() self.cover_pool = CoverThreadPool(cover_thread_count) self.details_pool = DetailsThreadPool(detail_thread_count) self.filter_results_dispatcher = FunctionDispatcher( self.filter_results) self.got_result_details_dispatcher = FunctionDispatcher( self.got_result_details) self.sort_col = 2 self.sort_order = Qt.AscendingOrder def closing(self): self.cover_pool.abort() self.details_pool.abort() def clear_results(self): self.all_matches = [] self.matches = [] self.all_matches = [] self.search_filter.clear_search_results() self.query = '' self.filterable_query = False self.cover_pool.abort() self.details_pool.abort() self.total_changed.emit(self.rowCount()) self.beginResetModel(), self.endResetModel() def add_result(self, result, store_plugin): if result not in self.all_matches: self.layoutAboutToBeChanged.emit() self.all_matches.append(result) self.search_filter.add_search_result(result) if result.cover_url: result.cover_queued = True self.cover_pool.add_task(result, self.filter_results_dispatcher) else: result.cover_queued = False self.details_pool.add_task(result, store_plugin, self.got_result_details_dispatcher) self.filter_results() self.layoutChanged.emit() def get_result(self, index): row = index.row() if row < len(self.matches): return self.matches[row] else: return None def has_results(self): return len(self.matches) > 0 def filter_results(self): self.layoutAboutToBeChanged.emit() # Only use the search filter's filtered results when there is a query # and it is a filterable query. This allows for the stores best guess # matches to come though. if self.query and self.filterable_query: self.matches = list(self.search_filter.parse(self.query)) else: self.matches = list(self.search_filter.universal_set()) self.total_changed.emit(self.rowCount()) self.sort(self.sort_col, self.sort_order, False) self.layoutChanged.emit() def got_result_details(self, result): if not result.cover_queued and result.cover_url: result.cover_queued = True self.cover_pool.add_task(result, self.filter_results_dispatcher) if result in self.matches: row = self.matches.index(result) self.dataChanged.emit(self.index(row, 0), self.index(row, self.columnCount() - 1)) if result.drm not in (SearchResult.DRM_LOCKED, SearchResult.DRM_UNLOCKED, SearchResult.DRM_UNKNOWN): result.drm = SearchResult.DRM_UNKNOWN self.filter_results() def set_query(self, query): self.query = query self.filterable_query = self.is_filterable_query(query) def is_filterable_query(self, query): # Remove control modifiers. query = query.replace('\\', '') query = query.replace('!', '') query = query.replace('=', '') query = query.replace('~', '') query = query.replace('>', '') query = query.replace('<', '') # Store the query at this point for comparision later mod_query = query # Remove filter identifiers # Remove the prefix. for loc in ('all', 'author', 'author2', 'authors', 'title', 'title2'): query = re.sub(r'%s:"(?P<a>[^\s"]+)"' % loc, '\g<a>', query) query = query.replace('%s:' % loc, '') # Remove the prefix and search text. for loc in ('cover', 'download', 'downloads', 'drm', 'format', 'formats', 'price', 'store'): query = re.sub(r'%s:"[^"]"' % loc, '', query) query = re.sub(r'%s:[^\s]*' % loc, '', query) # Remove whitespace query = re.sub('\s', '', query) mod_query = re.sub('\s', '', mod_query) # If mod_query and query are the same then there were no filter modifiers # so this isn't a filterable query. if mod_query == query: return False return True def index(self, row, column, parent=QModelIndex()): return self.createIndex(row, column) def parent(self, index): if not index.isValid() or index.internalId() == 0: return QModelIndex() return self.createIndex(0, 0) def rowCount(self, *args): return len(self.matches) def columnCount(self, *args): return len(self.HEADERS) def headerData(self, section, orientation, role): if role != Qt.DisplayRole: return None text = '' if orientation == Qt.Horizontal: if section < len(self.HEADERS): text = self.HEADERS[section] return (text) else: return (section + 1) def data(self, index, role): row, col = index.row(), index.column() if row >= len(self.matches): return None result = self.matches[row] if role == Qt.DisplayRole: if col == 1: t = result.title if result.title else _('Unknown') a = result.author if result.author else '' return ('<b>%s</b><br><i>%s</i>' % (t, a)) elif col == 2: return (result.price) elif col == 4: return ('%s<br>%s' % (result.store_name, result.formats)) return None elif role == Qt.DecorationRole: if col == 0 and result.cover_data: p = QPixmap() p.loadFromData(result.cover_data) return (p) if col == 3: if result.drm == SearchResult.DRM_LOCKED: return (self.DRM_LOCKED_ICON) elif result.drm == SearchResult.DRM_UNLOCKED: return (self.DRM_UNLOCKED_ICON) elif result.drm == SearchResult.DRM_UNKNOWN: return (self.DRM_UNKNOWN_ICON) if col == 5: if result.downloads: return (self.DOWNLOAD_ICON) if col == 6: if result.affiliate: return (self.DONATE_ICON) elif role == Qt.ToolTipRole: if col == 1: return ('<p>%s</p>' % result.title) elif col == 2: return ('<p>' + _( 'Detected price as: %s. Check with the store before making a purchase to verify this price is correct. This price often does not include promotions the store may be running.' ) % result.price + '</p>') # noqa elif col == 3: if result.drm == SearchResult.DRM_LOCKED: return ('<p>' + _( 'This book as been detected as having DRM restrictions. This book may not work with your reader and you will have limitations placed upon you as to what you can do with this book. Check with the store before making any purchases to ensure you can actually read this book.' ) + '</p>') # noqa elif result.drm == SearchResult.DRM_UNLOCKED: return ('<p>' + _( 'This book has been detected as being DRM Free. You should be able to use this book on any device provided it is in a format calibre supports for conversion. However, before making a purchase double check the DRM status with the store. The store may not be disclosing the use of DRM.' ) + '</p>') # noqa else: return ('<p>' + _( 'The DRM status of this book could not be determined. There is a very high likelihood that this book is actually DRM restricted.' ) + '</p>') # noqa elif col == 4: return ('<p>%s</p>' % result.formats) elif col == 5: if result.downloads: return ('<p>' + _( 'The following formats can be downloaded directly: %s.' ) % ', '.join(result.downloads.keys()) + '</p>') elif col == 6: if result.affiliate: return ('<p>' + _( 'Buying from this store supports the calibre developer: %s.' ) % result.plugin_author + '</p>') elif role == Qt.SizeHintRole: return QSize(64, 64) return None def data_as_text(self, result, col): text = '' if col == 1: text = result.title elif col == 2: text = comparable_price(result.price) elif col == 3: if result.drm == SearchResult.DRM_UNLOCKED: text = 'a' if result.drm == SearchResult.DRM_LOCKED: text = 'b' else: text = 'c' elif col == 4: text = result.store_name elif col == 5: if result.downloads: text = 'a' else: text = 'b' elif col == 6: if result.affiliate: text = 'a' else: text = 'b' return text def sort(self, col, order, reset=True): self.sort_col = col self.sort_order = order if not self.matches: return descending = order == Qt.DescendingOrder self.all_matches.sort( None, lambda x: sort_key(unicode(self.data_as_text(x, col))), descending) self.reorder_matches() if reset: self.beginResetModel(), self.endResetModel() def reorder_matches(self): def keygen(x): try: return self.all_matches.index(x) except: return 100000 self.matches = sorted(self.matches, key=keygen)
class Matches(QAbstractItemModel): total_changed = pyqtSignal(int) HEADERS = [_('Cover'), _('Title'), _('Price'), _('DRM'), _('Store'), _('Download'), _('Affiliate')] HTML_COLS = (1, 4) def __init__(self, cover_thread_count=2, detail_thread_count=4): QAbstractItemModel.__init__(self) self.DRM_LOCKED_ICON = QPixmap(I('drm-locked.png')).scaledToHeight(64, Qt.SmoothTransformation) self.DRM_UNLOCKED_ICON = QPixmap(I('drm-unlocked.png')).scaledToHeight(64, Qt.SmoothTransformation) self.DRM_UNKNOWN_ICON = QPixmap(I('dialog_question.png')).scaledToHeight(64, Qt.SmoothTransformation) self.DONATE_ICON = QPixmap(I('donate.png')).scaledToHeight(16, Qt.SmoothTransformation) self.DOWNLOAD_ICON = QPixmap(I('arrow-down.png')).scaledToHeight(16, Qt.SmoothTransformation) # All matches. Used to determine the order to display # self.matches because the SearchFilter returns # matches unordered. self.all_matches = [] # Only the showing matches. self.matches = [] self.query = '' self.filterable_query = False self.search_filter = SearchFilter() self.cover_pool = CoverThreadPool(cover_thread_count) self.details_pool = DetailsThreadPool(detail_thread_count) self.filter_results_dispatcher = FunctionDispatcher(self.filter_results) self.got_result_details_dispatcher = FunctionDispatcher(self.got_result_details) self.sort_col = 2 self.sort_order = Qt.AscendingOrder def closing(self): self.cover_pool.abort() self.details_pool.abort() def clear_results(self): self.all_matches = [] self.matches = [] self.all_matches = [] self.search_filter.clear_search_results() self.query = '' self.filterable_query = False self.cover_pool.abort() self.details_pool.abort() self.total_changed.emit(self.rowCount()) self.reset() def add_result(self, result, store_plugin): if result not in self.all_matches: self.layoutAboutToBeChanged.emit() self.all_matches.append(result) self.search_filter.add_search_result(result) if result.cover_url: result.cover_queued = True self.cover_pool.add_task(result, self.filter_results_dispatcher) else: result.cover_queued = False self.details_pool.add_task(result, store_plugin, self.got_result_details_dispatcher) self.filter_results() self.layoutChanged.emit() def get_result(self, index): row = index.row() if row < len(self.matches): return self.matches[row] else: return None def has_results(self): return len(self.matches) > 0 def filter_results(self): self.layoutAboutToBeChanged.emit() # Only use the search filter's filtered results when there is a query # and it is a filterable query. This allows for the stores best guess # matches to come though. if self.query and self.filterable_query: self.matches = list(self.search_filter.parse(self.query)) else: self.matches = list(self.search_filter.universal_set()) self.total_changed.emit(self.rowCount()) self.sort(self.sort_col, self.sort_order, False) self.layoutChanged.emit() def got_result_details(self, result): if not result.cover_queued and result.cover_url: result.cover_queued = True self.cover_pool.add_task(result, self.filter_results_dispatcher) if result in self.matches: row = self.matches.index(result) self.dataChanged.emit(self.index(row, 0), self.index(row, self.columnCount() - 1)) if result.drm not in (SearchResult.DRM_LOCKED, SearchResult.DRM_UNLOCKED, SearchResult.DRM_UNKNOWN): result.drm = SearchResult.DRM_UNKNOWN self.filter_results() def set_query(self, query): self.query = query self.filterable_query = self.is_filterable_query(query) def is_filterable_query(self, query): # Remove control modifiers. query = query.replace('\\', '') query = query.replace('!', '') query = query.replace('=', '') query = query.replace('~', '') query = query.replace('>', '') query = query.replace('<', '') # Store the query at this point for comparision later mod_query = query # Remove filter identifiers # Remove the prefix. for loc in ('all', 'author', 'authors', 'title'): query = re.sub(r'%s:"(?P<a>[^\s"]+)"' % loc, '\g<a>', query) query = query.replace('%s:' % loc, '') # Remove the prefix and search text. for loc in ('cover', 'download', 'downloads', 'drm', 'format', 'formats', 'price', 'store'): query = re.sub(r'%s:"[^"]"' % loc, '', query) query = re.sub(r'%s:[^\s]*' % loc, '', query) # Remove whitespace query = re.sub('\s', '', query) mod_query = re.sub('\s', '', mod_query) # If mod_query and query are the same then there were no filter modifiers # so this isn't a filterable query. if mod_query == query: return False return True def index(self, row, column, parent=QModelIndex()): return self.createIndex(row, column) def parent(self, index): if not index.isValid() or index.internalId() == 0: return QModelIndex() return self.createIndex(0, 0) def rowCount(self, *args): return len(self.matches) def columnCount(self, *args): return len(self.HEADERS) def headerData(self, section, orientation, role): if role != Qt.DisplayRole: return NONE text = '' if orientation == Qt.Horizontal: if section < len(self.HEADERS): text = self.HEADERS[section] return QVariant(text) else: return QVariant(section+1) def data(self, index, role): row, col = index.row(), index.column() if row >= len(self.matches): return NONE result = self.matches[row] if role == Qt.DisplayRole: if col == 1: t = result.title if result.title else _('Unknown') a = result.author if result.author else '' return QVariant('<b>%s</b><br><i>%s</i>' % (t, a)) elif col == 2: return QVariant(result.price) elif col == 4: return QVariant('%s<br>%s' % (result.store_name, result.formats)) return NONE elif role == Qt.DecorationRole: if col == 0 and result.cover_data: p = QPixmap() p.loadFromData(result.cover_data) return QVariant(p) if col == 3: if result.drm == SearchResult.DRM_LOCKED: return QVariant(self.DRM_LOCKED_ICON) elif result.drm == SearchResult.DRM_UNLOCKED: return QVariant(self.DRM_UNLOCKED_ICON) elif result.drm == SearchResult.DRM_UNKNOWN: return QVariant(self.DRM_UNKNOWN_ICON) if col == 5: if result.downloads: return QVariant(self.DOWNLOAD_ICON) if col == 6: if result.affiliate: return QVariant(self.DONATE_ICON) elif role == Qt.ToolTipRole: if col == 1: return QVariant('<p>%s</p>' % result.title) elif col == 2: return QVariant('<p>' + _('Detected price as: %s. Check with the store before making a purchase to verify this price is correct. This price often does not include promotions the store may be running.') % result.price + '</p>') elif col == 3: if result.drm == SearchResult.DRM_LOCKED: return QVariant('<p>' + _('This book as been detected as having DRM restrictions. This book may not work with your reader and you will have limitations placed upon you as to what you can do with this book. Check with the store before making any purchases to ensure you can actually read this book.') + '</p>') elif result.drm == SearchResult.DRM_UNLOCKED: return QVariant('<p>' + _('This book has been detected as being DRM Free. You should be able to use this book on any device provided it is in a format calibre supports for conversion. However, before making a purchase double check the DRM status with the store. The store may not be disclosing the use of DRM.') + '</p>') else: return QVariant('<p>' + _('The DRM status of this book could not be determined. There is a very high likelihood that this book is actually DRM restricted.') + '</p>') elif col == 4: return QVariant('<p>%s</p>' % result.formats) elif col == 5: if result.downloads: return QVariant('<p>' + _('The following formats can be downloaded directly: %s.') % ', '.join(result.downloads.keys()) + '</p>') elif col == 6: if result.affiliate: return QVariant('<p>' + _('Buying from this store supports the calibre developer: %s.') % result.plugin_author + '</p>') elif role == Qt.SizeHintRole: return QSize(64, 64) return NONE def data_as_text(self, result, col): text = '' if col == 1: text = result.title elif col == 2: text = comparable_price(result.price) elif col == 3: if result.drm == SearchResult.DRM_UNLOCKED: text = 'a' if result.drm == SearchResult.DRM_LOCKED: text = 'b' else: text = 'c' elif col == 4: text = result.store_name elif col == 5: if result.downloads: text = 'a' else: text = 'b' elif col == 6: if result.affiliate: text = 'a' else: text = 'b' return text def sort(self, col, order, reset=True): self.sort_col = col self.sort_order = order if not self.matches: return descending = order == Qt.DescendingOrder self.all_matches.sort(None, lambda x: sort_key(unicode(self.data_as_text(x, col))), descending) self.reorder_matches() if reset: self.reset() def reorder_matches(self): def keygen(x): try: return self.all_matches.index(x) except: return 100000 self.matches = sorted(self.matches, key=keygen)