class HeaderView(QHeaderView): # {{{ def __init__(self, *args): QHeaderView.__init__(self, *args) self.hover = -1 self.current_font = QFont(self.font()) self.current_font.setBold(True) self.current_font.setItalic(True) def event(self, e): if e.type() in (e.HoverMove, e.HoverEnter): self.hover = self.logicalIndexAt(e.pos()) elif e.type() in (e.Leave, e.HoverLeave): self.hover = -1 return QHeaderView.event(self, e) def paintSection(self, painter, rect, logical_index): opt = QStyleOptionHeader() self.initStyleOption(opt) opt.rect = rect opt.section = logical_index opt.orientation = self.orientation() opt.textAlignment = Qt.AlignHCenter | Qt.AlignVCenter model = self.parent().model() opt.text = unicode( model.headerData(logical_index, opt.orientation, Qt.DisplayRole) or '') if self.isSortIndicatorShown() and self.sortIndicatorSection( ) == logical_index: opt.sortIndicator = QStyleOptionHeader.SortDown if self.sortIndicatorOrder( ) == Qt.AscendingOrder else QStyleOptionHeader.SortUp opt.text = opt.fontMetrics.elidedText(opt.text, Qt.ElideRight, rect.width() - 4) if self.isEnabled(): opt.state |= QStyle.State_Enabled if self.window().isActiveWindow(): opt.state |= QStyle.State_Active if self.hover == logical_index: opt.state |= QStyle.State_MouseOver sm = self.selectionModel() if opt.orientation == Qt.Vertical: try: opt.icon = model.headerData(logical_index, opt.orientation, Qt.DecorationRole) opt.iconAlignment = Qt.AlignVCenter except (IndexError, ValueError, TypeError): pass if sm.isRowSelected(logical_index, QModelIndex()): opt.state |= QStyle.State_Sunken painter.save() if ((opt.orientation == Qt.Horizontal and sm.currentIndex().column() == logical_index) or (opt.orientation == Qt.Vertical and sm.currentIndex().row() == logical_index)): painter.setFont(self.current_font) self.style().drawControl(QStyle.CE_Header, opt, painter, self) painter.restore()
def process_duplicates(self, db, duplicates): ta = _('%(title)s by %(author)s [%(formats)s]') bf = QFont(self.dup_list.font()) bf.setBold(True) itf = QFont(self.dup_list.font()) itf.setItalic(True) for mi, cover, formats in duplicates: # formats is a list of file paths # Grab just the extension and display to the user # Based only off the file name, no file type tests are done. incoming_formats = ', '.join( os.path.splitext(path)[-1].replace('.', '').upper() for path in formats) item = QTreeWidgetItem([ ta % dict(title=mi.title, author=mi.format_field('authors')[1], formats=incoming_formats) ], 0) item.setCheckState(0, Qt.CheckState.Checked) item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsUserCheckable) item.setData(0, Qt.ItemDataRole.FontRole, bf) item.setData(0, Qt.ItemDataRole.UserRole, (mi, cover, formats)) matching_books = db.books_with_same_title(mi) def add_child(text): c = QTreeWidgetItem([text], 1) c.setFlags(Qt.ItemFlag.ItemIsEnabled) item.addChild(c) return c add_child(_('Already in calibre:')).setData( 0, Qt.ItemDataRole.FontRole, itf) author_text = {} for book_id in matching_books: author_text[book_id] = authors_to_string([ a.replace('|', ',') for a in ( db.authors(book_id, index_is_id=True) or '').split(',') ]) def key(x): return primary_sort_key(unicode_type(author_text[x])) for book_id in sorted(matching_books, key=key): add_child( ta % dict(title=db.title(book_id, index_is_id=True), author=author_text[book_id], formats=db.formats( book_id, index_is_id=True, verify_formats=False))) add_child('') yield item
def set_window_title(self): db = self.current_db restrictions = [x for x in (db.data.get_base_restriction_name(), db.data.get_search_restriction_name()) if x] restrictions = " :: ".join(restrictions) font = QFont() if restrictions: restrictions = " :: " + restrictions font.setBold(True) font.setItalic(True) self.virtual_library.setFont(font) title = u"{0} - || {1}{2} ||".format(__appname__, self.iactions["Choose Library"].library_name(), restrictions) self.setWindowTitle(title)
class HeaderView(QHeaderView): # {{{ def __init__(self, *args): QHeaderView.__init__(self, *args) self.hover = -1 self.current_font = QFont(self.font()) self.current_font.setBold(True) self.current_font.setItalic(True) def event(self, e): if e.type() in (e.HoverMove, e.HoverEnter): self.hover = self.logicalIndexAt(e.pos()) elif e.type() in (e.Leave, e.HoverLeave): self.hover = -1 return QHeaderView.event(self, e) def paintSection(self, painter, rect, logical_index): opt = QStyleOptionHeader() self.initStyleOption(opt) opt.rect = rect opt.section = logical_index opt.orientation = self.orientation() opt.textAlignment = Qt.AlignHCenter | Qt.AlignVCenter model = self.parent().model() opt.text = unicode(model.headerData(logical_index, opt.orientation, Qt.DisplayRole) or '') if self.isSortIndicatorShown() and self.sortIndicatorSection() == logical_index: opt.sortIndicator = QStyleOptionHeader.SortDown if self.sortIndicatorOrder() == Qt.AscendingOrder else QStyleOptionHeader.SortUp opt.text = opt.fontMetrics.elidedText(opt.text, Qt.ElideRight, rect.width() - 4) if self.isEnabled(): opt.state |= QStyle.State_Enabled if self.window().isActiveWindow(): opt.state |= QStyle.State_Active if self.hover == logical_index: opt.state |= QStyle.State_MouseOver sm = self.selectionModel() if opt.orientation == Qt.Vertical: try: opt.icon = model.headerData(logical_index, opt.orientation, Qt.DecorationRole) opt.iconAlignment = Qt.AlignVCenter except (IndexError, ValueError, TypeError): pass if sm.isRowSelected(logical_index, QModelIndex()): opt.state |= QStyle.State_Sunken painter.save() if ( (opt.orientation == Qt.Horizontal and sm.currentIndex().column() == logical_index) or (opt.orientation == Qt.Vertical and sm.currentIndex().row() == logical_index)): painter.setFont(self.current_font) self.style().drawControl(QStyle.CE_Header, opt, painter, self) painter.restore()
def set_window_title(self): db = self.current_db restrictions = [x for x in (db.data.get_base_restriction_name(), db.data.get_search_restriction_name()) if x] restrictions = ' :: '.join(restrictions) font = QFont() if restrictions: restrictions = ' :: ' + restrictions font.setBold(True) font.setItalic(True) self.virtual_library.setFont(font) title = '{0} - || {1}{2} ||'.format( __appname__, self.iactions['Choose Library'].library_name(), restrictions) self.setWindowTitle(title)
def set_button_style(self, attr_value_dict: dict): """ :param attr_value_dict: Available styles: 'color' - text color 'background' 'font' - font family 'font_size' 'bold' 'italic' 'underline' 'autoraise' - autoraise button 'on_remove' - handler of remove function, None if no need Example: button.set_button_style({'color':'magenta' , 'font_size': 18}) """ if not attr_value_dict: return style = '' if 'color' in attr_value_dict and attr_value_dict['color']: style += 'color: ' + attr_value_dict['color'] + ';' if 'background' in attr_value_dict: style += 'background-color: ' + attr_value_dict['background'] + ';' if style is not '': self.setStyleSheet('QToolButton {' + style + '}') font = QFont() if 'font' in attr_value_dict: font.setFamily(attr_value_dict['font']) if 'font_size' in attr_value_dict: font.setPointSize(attr_value_dict['font_size']) font.setBold(attr_value_dict.get('bold', False)) font.setItalic(attr_value_dict.get('italic', False)) font.setUnderline(attr_value_dict.get('underline', False)) self.setFont(font) self.setAutoRaise(attr_value_dict.get('autoraise', True)) self.remove = attr_value_dict.get('on_remove', None) if self.remove: # set button context menu policy self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.on_context_menu) # create context menu self.popMenu = QMenu(self) menu_action = QAction('Remove', self, triggered=self.remove) self.popMenu.addAction(menu_action)
def load_profiles(self): """ Load profiles into the list :return: """ font_default = QFont() font_default.setItalic(True) self.listOfProfiles.clear() for p in self.db.get_profiles(): item = QListWidgetItem() item.setText(p[0]) if p[2] == 1: item.setFont(font_default) self.listOfProfiles.addItem(item)
def __init__(self, toc=None): QStandardItemModel.__init__(self) self.current_query = {'text':'', 'index':-1, 'items':()} self.all_items = depth_first = [] normal_font = QApplication.instance().font() emphasis_font = QFont(normal_font) emphasis_font.setBold(True), emphasis_font.setItalic(True) if toc: for t in toc['children']: self.appendRow(TOCItem(t, 0, depth_first, normal_font, emphasis_font)) self.node_id_map = {x.node_id: x for x in self.all_items} self.currently_viewed_entry = None
def process_duplicates(self, db, duplicates): ta = _('%(title)s by %(author)s [%(formats)s]') bf = QFont(self.dup_list.font()) bf.setBold(True) itf = QFont(self.dup_list.font()) itf.setItalic(True) for mi, cover, formats in duplicates: # formats is a list of file paths # Grab just the extension and display to the user # Based only off the file name, no file type tests are done. incoming_formats = ', '.join(os.path.splitext(path)[-1].replace('.', '').upper() for path in formats) item = QTreeWidgetItem([ta%dict( title=mi.title, author=mi.format_field('authors')[1], formats=incoming_formats)] , 0) item.setCheckState(0, Qt.Checked) item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable) item.setData(0, Qt.FontRole, bf) item.setData(0, Qt.UserRole, (mi, cover, formats)) matching_books = db.books_with_same_title(mi) def add_child(text): c = QTreeWidgetItem([text], 1) c.setFlags(Qt.ItemIsEnabled) item.addChild(c) return c add_child(_('Already in calibre:')).setData(0, Qt.FontRole, itf) for book_id in matching_books: aut = [a.replace('|', ',') for a in (db.authors(book_id, index_is_id=True) or '').split(',')] add_child(ta%dict( title=db.title(book_id, index_is_id=True), author=authors_to_string(aut), formats=db.formats(book_id, index_is_id=True, verify_formats=False))) add_child('') yield item
class Highlights(QTreeWidget): jump_to_highlight = pyqtSignal(object) current_highlight_changed = pyqtSignal(object) delete_requested = pyqtSignal() edit_requested = pyqtSignal() edit_notes_requested = pyqtSignal() def __init__(self, parent=None): QTreeWidget.__init__(self, parent) self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect(self.show_context_menu) self.default_decoration = QIcon(I('blank.png')) self.setHeaderHidden(True) self.num_of_items = 0 self.setSelectionMode( QAbstractItemView.SelectionMode.ExtendedSelection) set_no_activate_on_click(self) self.itemActivated.connect(self.item_activated) self.currentItemChanged.connect(self.current_item_changed) self.uuid_map = {} self.section_font = QFont(self.font()) self.section_font.setItalic(True) def show_context_menu(self, point): index = self.indexAt(point) h = index.data(Qt.ItemDataRole.UserRole) self.context_menu = m = QMenu(self) if h is not None: m.addAction(QIcon(I('edit_input.png')), _('Modify this highlight'), self.edit_requested.emit) m.addAction(QIcon(I('edit_input.png')), _('Edit notes for this highlight'), self.edit_notes_requested.emit) m.addAction( QIcon(I('trash.png')), ngettext('Delete this highlight', 'Delete selected highlights', len(self.selectedItems())), self.delete_requested.emit) if h.get('notes'): m.addAction(QIcon(I('modified.png')), _('Edit notes for this highlight'), self.edit_notes_requested.emit) m.addSeparator() m.addAction(_('Expand all'), self.expandAll) m.addAction(_('Collapse all'), self.collapseAll) self.context_menu.popup(self.mapToGlobal(point)) return True def current_item_changed(self, current, previous): self.current_highlight_changed.emit( current.data(0, Qt.ItemDataRole.UserRole ) if current is not None else None) def load(self, highlights): s = self.style() icon_size = s.pixelMetric(QStyle.PixelMetric.PM_SmallIconSize, None, self) dpr = self.devicePixelRatioF() is_dark = is_dark_theme() self.clear() self.uuid_map = {} highlights = (h for h in highlights if not h.get('removed') and h.get('highlighted_text')) section_map = defaultdict(list) section_tt_map = {} for h in self.sorted_highlights(highlights): tfam = h.get('toc_family_titles') or () if tfam: tsec = tfam[0] lsec = tfam[-1] else: tsec = h.get('top_level_section_title') lsec = h.get('lowest_level_section_title') sec = lsec or tsec or _('Unknown') if len(tfam) > 1: lines = [] for i, node in enumerate(tfam): lines.append('\xa0\xa0' * i + '➤ ' + node) tt = ngettext('Table of Contents section:', 'Table of Contents sections:', len(lines)) tt += '\n' + '\n'.join(lines) section_tt_map[sec] = tt section_map[sec].append(h) for secnum, (sec, items) in enumerate(section_map.items()): section = QTreeWidgetItem([sec], 1) section.setFlags(Qt.ItemFlag.ItemIsEnabled) section.setFont(0, self.section_font) tt = section_tt_map.get(sec) if tt: section.setToolTip(0, tt) self.addTopLevelItem(section) section.setExpanded(True) for itemnum, h in enumerate(items): txt = h.get('highlighted_text') txt = txt.replace('\n', ' ') if h.get('notes'): txt = '•' + txt if len(txt) > 100: txt = txt[:100] + '…' item = QTreeWidgetItem(section, [txt], 2) item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemNeverHasChildren) item.setData(0, Qt.ItemDataRole.UserRole, h) try: dec = decoration_for_style(self.palette(), h.get('style') or {}, icon_size, dpr, is_dark) except Exception: import traceback traceback.print_exc() dec = None if dec is None: dec = self.default_decoration item.setData(0, Qt.ItemDataRole.DecorationRole, dec) self.uuid_map[h['uuid']] = secnum, itemnum self.num_of_items += 1 def sorted_highlights(self, highlights): defval = 999999999999999, cfi_sort_key('/99999999') def cfi_key(h): cfi = h.get('start_cfi') return (h.get('spine_index') or defval[0], cfi_sort_key(cfi)) if cfi else defval return sorted(highlights, key=cfi_key) def refresh(self, highlights): h = self.current_highlight self.load(highlights) if h is not None: idx = self.uuid_map.get(h['uuid']) if idx is not None: sec_idx, item_idx = idx self.set_current_row(sec_idx, item_idx) def iteritems(self): root = self.invisibleRootItem() for i in range(root.childCount()): sec = root.child(i) for k in range(sec.childCount()): yield sec.child(k) def count(self): return self.num_of_items def find_query(self, query): pat = query.regex items = tuple(self.iteritems()) count = len(items) cr = -1 ch = self.current_highlight if ch: q = ch['uuid'] for i, item in enumerate(items): h = item.data(0, Qt.ItemDataRole.UserRole) if h['uuid'] == q: cr = i if query.backwards: if cr < 0: cr = count indices = chain(range(cr - 1, -1, -1), range(count - 1, cr, -1)) else: if cr < 0: cr = -1 indices = chain(range(cr + 1, count), range(0, cr + 1)) for i in indices: h = items[i].data(0, Qt.ItemDataRole.UserRole) if pat.search(h['highlighted_text']) is not None or pat.search( h.get('notes') or '') is not None: self.set_current_row(*self.uuid_map[h['uuid']]) return True return False def find_annot_id(self, annot_id): q = self.uuid_map.get(annot_id) if q is not None: self.set_current_row(*q) return True return False def set_current_row(self, sec_idx, item_idx): sec = self.topLevelItem(sec_idx) if sec is not None: item = sec.child(item_idx) if item is not None: self.setCurrentItem( item, 0, QItemSelectionModel.SelectionFlag.ClearAndSelect) return True return False def item_activated(self, item): h = item.data(0, Qt.ItemDataRole.UserRole) if h is not None: self.jump_to_highlight.emit(h) @property def current_highlight(self): i = self.currentItem() if i is not None: return i.data(0, Qt.ItemDataRole.UserRole) @property def all_highlights(self): for item in self.iteritems(): yield item.data(0, Qt.ItemDataRole.UserRole) @property def selected_highlights(self): for item in self.selectedItems(): yield item.data(0, Qt.ItemDataRole.UserRole) def keyPressEvent(self, ev): if ev.matches(QKeySequence.StandardKey.Delete): self.delete_requested.emit() ev.accept() return if ev.key() == Qt.Key.Key_F2: self.edit_requested.emit() ev.accept() return return super().keyPressEvent(ev)
class ResultsList(QTreeWidget): current_result_changed = pyqtSignal(object) open_annotation = pyqtSignal(object, object, object) def __init__(self, parent): QTreeWidget.__init__(self, parent) self.setHeaderHidden(True) self.setSelectionMode(self.ExtendedSelection) self.delegate = AnnotsResultsDelegate(self) self.setItemDelegate(self.delegate) self.section_font = QFont(self.font()) self.itemDoubleClicked.connect(self.item_activated) self.section_font.setItalic(True) self.currentItemChanged.connect(self.current_item_changed) self.number_of_results = 0 self.item_map = [] def item_activated(self, item): r = item.data(0, Qt.UserRole) if isinstance(r, dict): self.open_annotation.emit(r['book_id'], r['format'], r['annotation']) def set_results(self, results, emphasize_text): self.clear() self.delegate.emphasize_text = emphasize_text self.number_of_results = 0 self.item_map = [] book_id_map = {} db = current_db() for result in results: book_id = result['book_id'] if book_id not in book_id_map: book_id_map[book_id] = { 'title': db.field_for('title', book_id), 'matches': [] } book_id_map[book_id]['matches'].append(result) for book_id, entry in book_id_map.items(): section = QTreeWidgetItem([entry['title']], 1) section.setFlags(Qt.ItemIsEnabled) section.setFont(0, self.section_font) self.addTopLevelItem(section) section.setExpanded(True) for result in entry['matches']: item = QTreeWidgetItem(section, [' '], 2) self.item_map.append(item) item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemNeverHasChildren) item.setData(0, Qt.UserRole, result) item.setData(0, Qt.UserRole + 1, self.number_of_results) self.number_of_results += 1 if self.item_map: self.setCurrentItem(self.item_map[0]) def current_item_changed(self, current, previous): if current is not None: r = current.data(0, Qt.UserRole) if isinstance(r, dict): self.current_result_changed.emit(r) else: self.current_result_changed.emit(None) def show_next(self, backwards=False): item = self.currentItem() if item is None: return i = int(item.data(0, Qt.UserRole + 1)) i += -1 if backwards else 1 i %= self.number_of_results self.setCurrentItem(self.item_map[i]) def keyPressEvent(self, ev): key = ev.key() if key == Qt.Key_Down: self.show_next() return if key == Qt.Key_Up: self.show_next(backwards=True) return return QTreeWidget.keyPressEvent(self, ev) @property def selected_annot_ids(self): for item in self.selectedItems(): yield item.data(0, Qt.UserRole)['id'] @property def selected_annotations(self): for item in self.selectedItems(): x = item.data(0, Qt.UserRole) ans = x['annotation'].copy() for key in ('book_id', 'format'): ans[key] = x[key] yield ans
class CompareSingle(QWidget): def __init__(self, field_metadata, parent=None, revert_tooltip=None, datetime_fmt='MMMM yyyy', blank_as_equal=True, fields=('title', 'authors', 'series', 'tags', 'rating', 'publisher', 'pubdate', 'identifiers', 'languages', 'comments', 'cover'), db=None): QWidget.__init__(self, parent) self.l = l = QGridLayout() l.setContentsMargins(0, 0, 0, 0) self.setLayout(l) revert_tooltip = revert_tooltip or _('Revert %s') self.current_mi = None self.changed_font = QFont(QApplication.font()) self.changed_font.setBold(True) self.changed_font.setItalic(True) self.blank_as_equal = blank_as_equal self.widgets = OrderedDict() row = 0 for field in fields: m = field_metadata[field] dt = m['datatype'] extra = None if 'series' in {field, dt}: cls = SeriesEdit elif field == 'identifiers': cls = IdentifiersEdit elif field == 'languages': cls = LanguagesEdit elif 'comments' in {field, dt}: cls = CommentsEdit elif 'rating' in {field, dt}: cls = RatingsEdit elif dt == 'datetime': extra = datetime_fmt cls = DateEdit elif field == 'cover': cls = CoverView elif dt in {'text', 'enum'}: cls = LineEdit else: continue neww = cls(field, True, self, m, extra) neww.changed.connect(partial(self.changed, field)) if isinstance(neww, EditWithComplete): try: neww.update_items_cache(db.new_api.all_field_names(field)) except ValueError: pass # A one-one field like title if isinstance(neww, SeriesEdit): neww.set_db(db.new_api) oldw = cls(field, False, self, m, extra) newl = QLabel('&%s:' % m['name']) newl.setBuddy(neww) button = RightClickButton(self) button.setIcon(QIcon(I('back.png'))) button.clicked.connect(partial(self.revert, field)) button.setToolTip(revert_tooltip % m['name']) if field == 'identifiers': button.m = m = QMenu(button) button.setMenu(m) button.setPopupMode(QToolButton.DelayedPopup) m.addAction(button.toolTip()).triggered.connect(button.click) m.actions()[0].setIcon(button.icon()) m.addAction(_('Merge identifiers')).triggered.connect( self.merge_identifiers) m.actions()[1].setIcon(QIcon(I('merge.png'))) elif field == 'tags': button.m = m = QMenu(button) button.setMenu(m) button.setPopupMode(QToolButton.DelayedPopup) m.addAction(button.toolTip()).triggered.connect(button.click) m.actions()[0].setIcon(button.icon()) m.addAction(_('Merge tags')).triggered.connect(self.merge_tags) m.actions()[1].setIcon(QIcon(I('merge.png'))) self.widgets[field] = Widgets(neww, oldw, newl, button) for i, w in enumerate((newl, neww, button, oldw)): c = i if i < 2 else i + 1 if w is oldw: c += 1 l.addWidget(w, row, c) row += 1 self.sep = f = QFrame(self) f.setFrameShape(f.VLine) l.addWidget(f, 0, 2, row, 1) self.sep2 = f = QFrame(self) f.setFrameShape(f.VLine) l.addWidget(f, 0, 4, row, 1) if 'comments' in self.widgets and not gprefs.get( 'diff_widget_show_comments_controls', True): self.widgets['comments'].new.hide_toolbars() def save_comments_controls_state(self): if 'comments' in self.widgets: vis = self.widgets['comments'].new.toolbars_visible if vis != gprefs.get('diff_widget_show_comments_controls', True): gprefs.set('diff_widget_show_comments_controls', vis) def changed(self, field): w = self.widgets[field] if not w.new.same_as(w.old) and (not self.blank_as_equal or not w.new.is_blank): w.label.setFont(self.changed_font) else: w.label.setFont(QApplication.font()) def revert(self, field): widgets = self.widgets[field] neww, oldw = widgets[:2] neww.current_val = oldw.current_val def merge_identifiers(self): widgets = self.widgets['identifiers'] neww, oldw = widgets[:2] val = neww.as_dict val.update(oldw.as_dict) neww.as_dict = val def merge_tags(self): widgets = self.widgets['tags'] neww, oldw = widgets[:2] val = oldw.value lval = {icu_lower(x) for x in val} extra = [x for x in neww.value if icu_lower(x) not in lval] if extra: neww.value = val + extra def __call__(self, oldmi, newmi): self.current_mi = newmi self.initial_vals = {} for field, widgets in self.widgets.iteritems(): widgets.old.from_mi(oldmi) widgets.new.from_mi(newmi) self.initial_vals[field] = widgets.new.current_val def apply_changes(self): changed = False for field, widgets in self.widgets.iteritems(): val = widgets.new.current_val if val != self.initial_vals[field]: widgets.new.to_mi(self.current_mi) changed = True return changed
class CompareSingle(QWidget): def __init__( self, field_metadata, parent=None, revert_tooltip=None, datetime_fmt='MMMM yyyy', blank_as_equal=True, fields=('title', 'authors', 'series', 'tags', 'rating', 'publisher', 'pubdate', 'identifiers', 'languages', 'comments', 'cover'), db=None): QWidget.__init__(self, parent) self.l = l = QGridLayout() l.setContentsMargins(0, 0, 0, 0) self.setLayout(l) revert_tooltip = revert_tooltip or _('Revert %s') self.current_mi = None self.changed_font = QFont(QApplication.font()) self.changed_font.setBold(True) self.changed_font.setItalic(True) self.blank_as_equal = blank_as_equal self.widgets = OrderedDict() row = 0 for field in fields: m = field_metadata[field] dt = m['datatype'] extra = None if 'series' in {field, dt}: cls = SeriesEdit elif field == 'identifiers': cls = IdentifiersEdit elif field == 'languages': cls = LanguagesEdit elif 'comments' in {field, dt}: cls = CommentsEdit elif 'rating' in {field, dt}: cls = RatingsEdit elif dt == 'datetime': extra = datetime_fmt cls = DateEdit elif field == 'cover': cls = CoverView elif dt in {'text', 'enum'}: cls = LineEdit else: continue neww = cls(field, True, self, m, extra) neww.changed.connect(partial(self.changed, field)) if isinstance(neww, EditWithComplete): try: neww.update_items_cache(db.new_api.all_field_names(field)) except ValueError: pass # A one-one field like title if isinstance(neww, SeriesEdit): neww.set_db(db.new_api) oldw = cls(field, False, self, m, extra) newl = QLabel('&%s:' % m['name']) newl.setBuddy(neww) button = RightClickButton(self) button.setIcon(QIcon(I('back.png'))) button.clicked.connect(partial(self.revert, field)) button.setToolTip(revert_tooltip % m['name']) if field == 'identifiers': button.m = m = QMenu(button) button.setMenu(m) button.setPopupMode(QToolButton.DelayedPopup) m.addAction(button.toolTip()).triggered.connect(button.click) m.actions()[0].setIcon(button.icon()) m.addAction(_('Merge identifiers')).triggered.connect(self.merge_identifiers) m.actions()[1].setIcon(QIcon(I('merge.png'))) elif field == 'tags': button.m = m = QMenu(button) button.setMenu(m) button.setPopupMode(QToolButton.DelayedPopup) m.addAction(button.toolTip()).triggered.connect(button.click) m.actions()[0].setIcon(button.icon()) m.addAction(_('Merge tags')).triggered.connect(self.merge_tags) m.actions()[1].setIcon(QIcon(I('merge.png'))) self.widgets[field] = Widgets(neww, oldw, newl, button) for i, w in enumerate((newl, neww, button, oldw)): c = i if i < 2 else i + 1 if w is oldw: c += 1 l.addWidget(w, row, c) row += 1 self.sep = f = QFrame(self) f.setFrameShape(f.VLine) l.addWidget(f, 0, 2, row, 1) self.sep2 = f = QFrame(self) f.setFrameShape(f.VLine) l.addWidget(f, 0, 4, row, 1) if 'comments' in self.widgets and not gprefs.get('diff_widget_show_comments_controls', True): self.widgets['comments'].new.hide_toolbars() def save_comments_controls_state(self): if 'comments' in self.widgets: vis = self.widgets['comments'].new.toolbars_visible if vis != gprefs.get('diff_widget_show_comments_controls', True): gprefs.set('diff_widget_show_comments_controls', vis) def changed(self, field): w = self.widgets[field] if not w.new.same_as(w.old) and (not self.blank_as_equal or not w.new.is_blank): w.label.setFont(self.changed_font) else: w.label.setFont(QApplication.font()) def revert(self, field): widgets = self.widgets[field] neww, oldw = widgets[:2] neww.current_val = oldw.current_val def merge_identifiers(self): widgets = self.widgets['identifiers'] neww, oldw = widgets[:2] val = neww.as_dict val.update(oldw.as_dict) neww.as_dict = val def merge_tags(self): widgets = self.widgets['tags'] neww, oldw = widgets[:2] val = oldw.value lval = {icu_lower(x) for x in val} extra = [x for x in neww.value if icu_lower(x) not in lval] if extra: neww.value = val + extra def __call__(self, oldmi, newmi): self.current_mi = newmi self.initial_vals = {} for field, widgets in self.widgets.iteritems(): widgets.old.from_mi(oldmi) widgets.new.from_mi(newmi) self.initial_vals[field] = widgets.new.current_val def apply_changes(self): changed = False for field, widgets in self.widgets.iteritems(): val = widgets.new.current_val if val != self.initial_vals[field]: widgets.new.to_mi(self.current_mi) changed = True return changed
class TOCItem(QStandardItem): def __init__(self, spine, toc, depth, all_items, parent=None): text = toc.text if text: text = re.sub(r'\s', ' ', text) self.title = text self.parent = parent self.href = toc.href QStandardItem.__init__(self, text if text else '') self.abspath = toc.abspath if toc.href else None self.fragment = toc.fragment all_items.append(self) self.emphasis_font = QFont(self.font()) self.emphasis_font.setBold(True), self.emphasis_font.setItalic(True) self.normal_font = self.font() for t in toc: self.appendRow(TOCItem(spine, t, depth + 1, all_items, parent=self)) self.setFlags(Qt.ItemIsEnabled) self.is_current_search_result = False spos = 0 for i, si in enumerate(spine): if si == self.abspath: spos = i break am = {} if self.abspath is not None: try: am = getattr(spine[i], 'anchor_map', {}) except UnboundLocalError: # Spine was empty? pass frag = self.fragment if (self.fragment and self.fragment in am) else None self.starts_at = spos self.start_anchor = frag self.start_src_offset = am.get(frag, 0) self.depth = depth self.is_being_viewed = False @property def ancestors(self): parent = self.parent while parent is not None: yield parent parent = parent.parent @classmethod def type(cls): return QStandardItem.UserType + 10 def update_indexing_state(self, spine_index, viewport_rect, anchor_map, in_paged_mode): if in_paged_mode: self.update_indexing_state_paged(spine_index, viewport_rect, anchor_map) else: self.update_indexing_state_unpaged(spine_index, viewport_rect, anchor_map) def update_indexing_state_unpaged(self, spine_index, viewport_rect, anchor_map): is_being_viewed = False top, bottom = viewport_rect[1], viewport_rect[3] # We use bottom-25 in the checks below to account for the case where # the next entry has some invisible margin that just overlaps with the # bottom of the screen. In this case it will appear to the user that # the entry is not visible on the screen. Of course, the margin could # be larger than 25, but that's a decent compromise. Also we dont want # to count a partial line as being visible. # We only care about y position anchor_map = {k: v[1] for k, v in iteritems(anchor_map)} if spine_index >= self.starts_at and spine_index <= self.ends_at: # The position at which this anchor is present in the document start_pos = anchor_map.get(self.start_anchor, 0) psp = [] if self.ends_at == spine_index: # Anchors that could possibly indicate the start of the next # section and therefore the end of this section. # self.possible_end_anchors is a set of anchors belonging to # toc entries with depth <= self.depth that are also not # ancestors of this entry. psp = [anchor_map.get(x, 0) for x in self.possible_end_anchors] psp = [x for x in psp if x >= start_pos] # The end position. The first anchor whose pos is >= start_pos # or if the end is not in this spine item, we set it to the bottom # of the window +1 end_pos = min(psp) if psp else ( bottom + 1 if self.ends_at >= spine_index else 0) if spine_index > self.starts_at and spine_index < self.ends_at: # The entire spine item is contained in this entry is_being_viewed = True elif (spine_index == self.starts_at and bottom - 25 >= start_pos and # This spine item contains the start # The start position is before the end of the viewport (spine_index != self.ends_at or top < end_pos)): # The end position is after the start of the viewport is_being_viewed = True elif (spine_index == self.ends_at and top < end_pos and # This spine item contains the end # The end position is after the start of the viewport (spine_index != self.starts_at or bottom - 25 >= start_pos)): # The start position is before the end of the viewport is_being_viewed = True changed = is_being_viewed != self.is_being_viewed self.is_being_viewed = is_being_viewed if changed: self.setFont( self.emphasis_font if is_being_viewed else self.normal_font) def update_indexing_state_paged(self, spine_index, viewport_rect, anchor_map): is_being_viewed = False left, right = viewport_rect[0], viewport_rect[2] left, right = (left, 0), (right, -1) if spine_index >= self.starts_at and spine_index <= self.ends_at: # The position at which this anchor is present in the document start_pos = anchor_map.get(self.start_anchor, (0, 0)) psp = [] if self.ends_at == spine_index: # Anchors that could possibly indicate the start of the next # section and therefore the end of this section. # self.possible_end_anchors is a set of anchors belonging to # toc entries with depth <= self.depth that are also not # ancestors of this entry. psp = [ anchor_map.get(x, (0, 0)) for x in self.possible_end_anchors ] psp = [x for x in psp if x >= start_pos] # The end position. The first anchor whose pos is >= start_pos # or if the end is not in this spine item, we set it to the column # after the right edge of the viewport end_pos = min(psp) if psp else ( right if self.ends_at >= spine_index else (0, 0)) if spine_index > self.starts_at and spine_index < self.ends_at: # The entire spine item is contained in this entry is_being_viewed = True elif (spine_index == self.starts_at and right > start_pos and # This spine item contains the start # The start position is before the end of the viewport (spine_index != self.ends_at or left < end_pos)): # The end position is after the start of the viewport is_being_viewed = True elif (spine_index == self.ends_at and left < end_pos and # This spine item contains the end # The end position is after the start of the viewport (spine_index != self.starts_at or right > start_pos)): # The start position is before the end of the viewport is_being_viewed = True changed = is_being_viewed != self.is_being_viewed self.is_being_viewed = is_being_viewed if changed: self.setFont( self.emphasis_font if is_being_viewed else self.normal_font) def set_current_search_result(self, yes): if yes and not self.is_current_search_result: self.setText(self.text() + ' ◄') self.is_current_search_result = True elif not yes and self.is_current_search_result: self.setText(self.text()[:-2]) self.is_current_search_result = False def __repr__(self): return 'TOC Item: %s %s#%s' % (self.title, self.abspath, self.fragment) def __str__(self): return repr(self)
def mark_item_as_current(self, item): font = QFont(self.font()) font.setItalic(True) font.setBold(True) item.setData(0, Qt.FontRole, font)
class TOCItem(QStandardItem): def __init__(self, spine, toc, depth, all_items, parent=None): text = toc.text if text: text = re.sub(r'\s', ' ', text) self.title = text self.parent = parent QStandardItem.__init__(self, text if text else '') self.abspath = toc.abspath if toc.href else None self.fragment = toc.fragment all_items.append(self) self.emphasis_font = QFont(self.font()) self.emphasis_font.setBold(True), self.emphasis_font.setItalic(True) self.normal_font = self.font() for t in toc: self.appendRow(TOCItem(spine, t, depth+1, all_items, parent=self)) self.setFlags(Qt.ItemIsEnabled) self.is_current_search_result = False spos = 0 for i, si in enumerate(spine): if si == self.abspath: spos = i break am = {} if self.abspath is not None: try: am = getattr(spine[i], 'anchor_map', {}) except UnboundLocalError: # Spine was empty? pass frag = self.fragment if (self.fragment and self.fragment in am) else None self.starts_at = spos self.start_anchor = frag self.start_src_offset = am.get(frag, 0) self.depth = depth self.is_being_viewed = False @property def ancestors(self): parent = self.parent while parent is not None: yield parent parent = parent.parent @classmethod def type(cls): return QStandardItem.UserType+10 def update_indexing_state(self, spine_index, viewport_rect, anchor_map, in_paged_mode): if in_paged_mode: self.update_indexing_state_paged(spine_index, viewport_rect, anchor_map) else: self.update_indexing_state_unpaged(spine_index, viewport_rect, anchor_map) def update_indexing_state_unpaged(self, spine_index, viewport_rect, anchor_map): is_being_viewed = False top, bottom = viewport_rect[1], viewport_rect[3] # We use bottom-25 in the checks below to account for the case where # the next entry has some invisible margin that just overlaps with the # bottom of the screen. In this case it will appear to the user that # the entry is not visible on the screen. Of course, the margin could # be larger than 25, but that's a decent compromise. Also we dont want # to count a partial line as being visible. # We only care about y position anchor_map = {k:v[1] for k, v in anchor_map.iteritems()} if spine_index >= self.starts_at and spine_index <= self.ends_at: # The position at which this anchor is present in the document start_pos = anchor_map.get(self.start_anchor, 0) psp = [] if self.ends_at == spine_index: # Anchors that could possibly indicate the start of the next # section and therefore the end of this section. # self.possible_end_anchors is a set of anchors belonging to # toc entries with depth <= self.depth that are also not # ancestors of this entry. psp = [anchor_map.get(x, 0) for x in self.possible_end_anchors] psp = [x for x in psp if x >= start_pos] # The end position. The first anchor whose pos is >= start_pos # or if the end is not in this spine item, we set it to the bottom # of the window +1 end_pos = min(psp) if psp else (bottom+1 if self.ends_at >= spine_index else 0) if spine_index > self.starts_at and spine_index < self.ends_at: # The entire spine item is contained in this entry is_being_viewed = True elif (spine_index == self.starts_at and bottom-25 >= start_pos and # This spine item contains the start # The start position is before the end of the viewport (spine_index != self.ends_at or top < end_pos)): # The end position is after the start of the viewport is_being_viewed = True elif (spine_index == self.ends_at and top < end_pos and # This spine item contains the end # The end position is after the start of the viewport (spine_index != self.starts_at or bottom-25 >= start_pos)): # The start position is before the end of the viewport is_being_viewed = True changed = is_being_viewed != self.is_being_viewed self.is_being_viewed = is_being_viewed if changed: self.setFont(self.emphasis_font if is_being_viewed else self.normal_font) def update_indexing_state_paged(self, spine_index, viewport_rect, anchor_map): is_being_viewed = False left, right = viewport_rect[0], viewport_rect[2] left, right = (left, 0), (right, -1) if spine_index >= self.starts_at and spine_index <= self.ends_at: # The position at which this anchor is present in the document start_pos = anchor_map.get(self.start_anchor, (0, 0)) psp = [] if self.ends_at == spine_index: # Anchors that could possibly indicate the start of the next # section and therefore the end of this section. # self.possible_end_anchors is a set of anchors belonging to # toc entries with depth <= self.depth that are also not # ancestors of this entry. psp = [anchor_map.get(x, (0, 0)) for x in self.possible_end_anchors] psp = [x for x in psp if x >= start_pos] # The end position. The first anchor whose pos is >= start_pos # or if the end is not in this spine item, we set it to the column # after the right edge of the viewport end_pos = min(psp) if psp else (right if self.ends_at >= spine_index else (0, 0)) if spine_index > self.starts_at and spine_index < self.ends_at: # The entire spine item is contained in this entry is_being_viewed = True elif (spine_index == self.starts_at and right > start_pos and # This spine item contains the start # The start position is before the end of the viewport (spine_index != self.ends_at or left < end_pos)): # The end position is after the start of the viewport is_being_viewed = True elif (spine_index == self.ends_at and left < end_pos and # This spine item contains the end # The end position is after the start of the viewport (spine_index != self.starts_at or right > start_pos)): # The start position is before the end of the viewport is_being_viewed = True changed = is_being_viewed != self.is_being_viewed self.is_being_viewed = is_being_viewed if changed: self.setFont(self.emphasis_font if is_being_viewed else self.normal_font) def set_current_search_result(self, yes): if yes and not self.is_current_search_result: self.setText(self.text() + ' ◄') self.is_current_search_result = True elif not yes and self.is_current_search_result: self.setText(self.text()[:-2]) self.is_current_search_result = False def __repr__(self): return 'TOC Item: %s %s#%s'%(self.title, self.abspath, self.fragment) def __str__(self): return repr(self)
class ResultsList(QTreeWidget): current_result_changed = pyqtSignal(object) open_annotation = pyqtSignal(object, object, object) show_book = pyqtSignal(object, object) delete_requested = pyqtSignal() export_requested = pyqtSignal() edit_annotation = pyqtSignal(object, object) def __init__(self, parent): QTreeWidget.__init__(self, parent) self.setHeaderHidden(True) self.setSelectionMode(self.ExtendedSelection) self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect(self.show_context_menu) self.delegate = AnnotsResultsDelegate(self) self.setItemDelegate(self.delegate) self.section_font = QFont(self.font()) self.itemDoubleClicked.connect(self.item_activated) self.section_font.setItalic(True) self.currentItemChanged.connect(self.current_item_changed) self.number_of_results = 0 self.item_map = [] def show_context_menu(self, pos): item = self.itemAt(pos) if item is not None: result = item.data(0, Qt.ItemDataRole.UserRole) else: result = None items = self.selectedItems() m = QMenu(self) if isinstance(result, dict): m.addAction(_('Open in viewer'), partial(self.item_activated, item)) m.addAction(_('Show in calibre'), partial(self.show_in_calibre, item)) if result.get('annotation', {}).get('type') == 'highlight': m.addAction(_('Edit notes'), partial(self.edit_notes, item)) if items: m.addSeparator() m.addAction( ngettext('Export selected item', 'Export {} selected items', len(items)).format(len(items)), self.export_requested.emit) m.addAction( ngettext('Delete selected item', 'Delete {} selected items', len(items)).format(len(items)), self.delete_requested.emit) m.addSeparator() m.addAction(_('Expand all'), self.expandAll) m.addAction(_('Collapse all'), self.collapseAll) m.exec_(self.mapToGlobal(pos)) def edit_notes(self, item): r = item.data(0, Qt.ItemDataRole.UserRole) if isinstance(r, dict): self.edit_annotation.emit(r['id'], r['annotation']) def show_in_calibre(self, item): r = item.data(0, Qt.ItemDataRole.UserRole) if isinstance(r, dict): self.show_book.emit(r['book_id'], r['format']) def item_activated(self, item): r = item.data(0, Qt.ItemDataRole.UserRole) if isinstance(r, dict): self.open_annotation.emit(r['book_id'], r['format'], r['annotation']) def set_results(self, results, emphasize_text): self.clear() self.delegate.emphasize_text = emphasize_text self.number_of_results = 0 self.item_map = [] book_id_map = {} db = current_db() for result in results: book_id = result['book_id'] if book_id not in book_id_map: book_id_map[book_id] = { 'title': db.field_for('title', book_id), 'matches': [] } book_id_map[book_id]['matches'].append(result) for book_id, entry in book_id_map.items(): section = QTreeWidgetItem([entry['title']], 1) section.setFlags(Qt.ItemFlag.ItemIsEnabled) section.setFont(0, self.section_font) self.addTopLevelItem(section) section.setExpanded(True) for result in entry['matches']: item = QTreeWidgetItem(section, [' '], 2) self.item_map.append(item) item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemNeverHasChildren) item.setData(0, Qt.ItemDataRole.UserRole, result) item.setData(0, Qt.ItemDataRole.UserRole + 1, self.number_of_results) self.number_of_results += 1 if self.item_map: self.setCurrentItem(self.item_map[0]) def current_item_changed(self, current, previous): if current is not None: r = current.data(0, Qt.ItemDataRole.UserRole) if isinstance(r, dict): self.current_result_changed.emit(r) else: self.current_result_changed.emit(None) def show_next(self, backwards=False): item = self.currentItem() if item is None: return i = int(item.data(0, Qt.ItemDataRole.UserRole + 1)) i += -1 if backwards else 1 i %= self.number_of_results self.setCurrentItem(self.item_map[i]) @property def selected_annot_ids(self): for item in self.selectedItems(): yield item.data(0, Qt.ItemDataRole.UserRole)['id'] @property def selected_annotations(self): for item in self.selectedItems(): x = item.data(0, Qt.ItemDataRole.UserRole) ans = x['annotation'].copy() for key in ('book_id', 'format'): ans[key] = x[key] yield ans def keyPressEvent(self, ev): if ev.matches(QKeySequence.StandardKey.Delete): self.delete_requested.emit() ev.accept() return return QTreeWidget.keyPressEvent(self, ev)
class Results(QTreeWidget): # {{{ show_search_result = pyqtSignal(object) current_result_changed = pyqtSignal(object) count_changed = pyqtSignal(object) def __init__(self, parent=None): QTreeWidget.__init__(self, parent) self.setHeaderHidden(True) self.setFocusPolicy(Qt.FocusPolicy.NoFocus) self.delegate = ResultsDelegate(self) self.setItemDelegate(self.delegate) self.itemClicked.connect(self.item_activated) self.blank_icon = QIcon(I('blank.png')) self.not_found_icon = QIcon(I('dialog_warning.png')) self.currentItemChanged.connect(self.current_item_changed) self.section_font = QFont(self.font()) self.section_font.setItalic(True) self.section_map = {} self.search_results = [] self.item_map = {} def current_item_changed(self, current, previous): if current is not None: r = current.data(0, SEARCH_RESULT_ROLE) if isinstance(r, SearchResult): self.current_result_changed.emit(r) else: self.current_result_changed.emit(None) def add_result(self, result): section_title = _('Unknown') section_id = -1 toc_nodes = getattr(result, 'toc_nodes', ()) or () if toc_nodes: section_title = toc_nodes[-1].get('title') or _('Unknown') section_id = toc_nodes[-1].get('id') if section_id is None: section_id = -1 section_key = section_id section = self.section_map.get(section_key) spine_idx = getattr(result, 'spine_idx', -1) if section is None: section = QTreeWidgetItem([section_title], 1) section.setFlags(Qt.ItemFlag.ItemIsEnabled) section.setFont(0, self.section_font) section.setData(0, SPINE_IDX_ROLE, spine_idx) lines = [] for i, node in enumerate(toc_nodes): lines.append('\xa0\xa0' * i + '➤ ' + (node.get('title') or _('Unknown'))) if lines: tt = ngettext('Table of Contents section:', 'Table of Contents sections:', len(lines)) tt += '\n' + '\n'.join(lines) section.setToolTip(0, tt) self.section_map[section_key] = section for s in range(self.topLevelItemCount()): ti = self.topLevelItem(s) if ti.data(0, SPINE_IDX_ROLE) > spine_idx: self.insertTopLevelItem(s, section) break else: self.addTopLevelItem(section) section.setExpanded(True) item = QTreeWidgetItem(section, [' '], 2) item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemNeverHasChildren) item.setData(0, SEARCH_RESULT_ROLE, result) item.setData(0, RESULT_NUMBER_ROLE, len(self.search_results)) item.setData(0, SPINE_IDX_ROLE, spine_idx) if isinstance(result, SearchResult): tt = '<p>…' + escape(result.before, False) + '<b>' + escape( result.text, False) + '</b>' + escape(result.after, False) + '…' item.setData(0, Qt.ItemDataRole.ToolTipRole, tt) item.setIcon(0, self.blank_icon) self.item_map[len(self.search_results)] = item self.search_results.append(result) n = self.number_of_results self.count_changed.emit(n) def item_activated(self): i = self.currentItem() if i: sr = i.data(0, SEARCH_RESULT_ROLE) if isinstance(sr, SearchResult): if not sr.is_hidden: self.show_search_result.emit(sr) def find_next(self, previous): if self.number_of_results < 1: return item = self.currentItem() if item is None: return i = int(item.data(0, RESULT_NUMBER_ROLE)) i += -1 if previous else 1 i %= self.number_of_results self.setCurrentItem(self.item_map[i]) self.item_activated() def search_result_not_found(self, sr): for i in range(self.number_of_results): item = self.item_map[i] r = item.data(0, SEARCH_RESULT_ROLE) if r.is_result(sr): r.is_hidden = True item.setIcon(0, self.not_found_icon) break def search_result_discovered(self, sr): q = sr['result_num'] for i in range(self.number_of_results): item = self.item_map[i] r = item.data(0, SEARCH_RESULT_ROLE) if r.result_num == q: self.setCurrentItem(item) @property def current_result_is_hidden(self): item = self.currentItem() if item is not None: sr = item.data(0, SEARCH_RESULT_ROLE) if isinstance(sr, SearchResult) and sr.is_hidden: return True return False @property def number_of_results(self): return len(self.search_results) def clear_all_results(self): self.section_map = {} self.item_map = {} self.search_results = [] self.clear() self.count_changed.emit(-1) def select_first_result(self): if self.number_of_results: item = self.item_map[0] self.setCurrentItem(item) def ensure_current_result_visible(self): item = self.currentItem() if item is not None: self.scrollToItem(item)
class Results(QTreeWidget): # {{{ show_search_result = pyqtSignal(object) current_result_changed = pyqtSignal(object) count_changed = pyqtSignal(object) def __init__(self, parent=None): QTreeWidget.__init__(self, parent) self.setHeaderHidden(True) self.setFocusPolicy(Qt.NoFocus) self.delegate = ResultsDelegate(self) self.setItemDelegate(self.delegate) self.itemClicked.connect(self.item_activated) self.blank_icon = QIcon(I('blank.png')) self.not_found_icon = QIcon(I('dialog_warning.png')) self.currentItemChanged.connect(self.current_item_changed) self.section_font = QFont(self.font()) self.section_font.setItalic(True) self.section_map = {} self.search_results = [] self.item_map = {} def current_item_changed(self, current, previous): if current is not None: r = current.data(0, Qt.UserRole) if isinstance(r, SearchResult): self.current_result_changed.emit(r) else: self.current_result_changed.emit(None) def add_result(self, result): section_title = _('Unknown') section_id = -1 toc_nodes = getattr(result, 'toc_nodes', ()) or () if toc_nodes: section_title = toc_nodes[-1].get('title') or _('Unknown') section_id = toc_nodes[-1].get('id') if section_id is None: section_id = -1 section_key = section_id section = self.section_map.get(section_key) if section is None: section = QTreeWidgetItem([section_title], 1) section.setFlags(Qt.ItemIsEnabled) section.setFont(0, self.section_font) lines = [] for i, node in enumerate(toc_nodes): lines.append('\xa0\xa0' * i + '➤ ' + (node.get('title') or _('Unknown'))) if lines: tt = ngettext('Table of Contents section:', 'Table of Contents sections:', len(lines)) tt += '\n' + '\n'.join(lines) section.setToolTip(0, tt) self.section_map[section_key] = section self.addTopLevelItem(section) section.setExpanded(True) item = QTreeWidgetItem(section, [' '], 2) item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemNeverHasChildren) item.setData(0, Qt.UserRole, result) item.setData(0, Qt.UserRole + 1, len(self.search_results)) item.setIcon(0, self.blank_icon) self.item_map[len(self.search_results)] = item self.search_results.append(result) n = self.number_of_results self.count_changed.emit(n) return n def item_activated(self): i = self.currentItem() if i: sr = i.data(0, Qt.UserRole) if isinstance(sr, SearchResult): if not sr.is_hidden: self.show_search_result.emit(sr) def find_next(self, previous): if self.number_of_results < 1: return item = self.currentItem() if item is None: return i = int(item.data(0, Qt.UserRole + 1)) i += -1 if previous else 1 i %= self.number_of_results self.setCurrentItem(self.item_map[i]) self.item_activated() def search_result_not_found(self, sr): for i in range(self.number_of_results): item = self.item_map[i] r = item.data(0, Qt.UserRole) if r.is_result(sr): r.is_hidden = True item.setIcon(0, self.not_found_icon) break @property def current_result_is_hidden(self): item = self.currentItem() if item is not None: sr = item.data(0, Qt.UserRole) if isinstance(sr, SearchResult) and sr.is_hidden: return True return False @property def number_of_results(self): return len(self.search_results) def clear_all_results(self): self.section_map = {} self.item_map = {} self.search_results = [] self.clear() self.count_changed.emit(-1) def select_first_result(self): if self.number_of_results: item = self.item_map[0] self.setCurrentItem(item)
class CompareSingle(QWidget): def __init__( self, field_metadata, parent=None, revert_tooltip=None, datetime_fmt='MMMM yyyy', blank_as_equal=True, fields=('title', 'authors', 'series', 'tags', 'rating', 'publisher', 'pubdate', 'identifiers', 'languages', 'comments', 'cover')): QWidget.__init__(self, parent) self.l = l = QGridLayout() l.setContentsMargins(0, 0, 0, 0) self.setLayout(l) revert_tooltip = revert_tooltip or _('Revert %s') self.current_mi = None self.changed_font = QFont(QApplication.font()) self.changed_font.setBold(True) self.changed_font.setItalic(True) self.blank_as_equal = blank_as_equal self.widgets = OrderedDict() row = 0 for field in fields: m = field_metadata[field] dt = m['datatype'] extra = None if 'series' in {field, dt}: cls = SeriesEdit elif field == 'identifiers': cls = IdentifiersEdit elif field == 'languages': cls = LanguagesEdit elif 'comments' in {field, dt}: cls = CommentsEdit elif 'rating' in {field, dt}: cls = RatingsEdit elif dt == 'datetime': extra = datetime_fmt cls = DateEdit elif field == 'cover': cls = CoverView elif dt in {'text', 'enum'}: cls = LineEdit else: continue neww = cls(field, True, self, m, extra) neww.changed.connect(partial(self.changed, field)) oldw = cls(field, False, self, m, extra) newl = QLabel('&%s:' % m['name']) newl.setBuddy(neww) button = QToolButton(self) button.setIcon(QIcon(I('back.png'))) button.clicked.connect(partial(self.revert, field)) button.setToolTip(revert_tooltip % m['name']) self.widgets[field] = Widgets(neww, oldw, newl, button) for i, w in enumerate((newl, neww, button, oldw)): c = i if i < 2 else i + 1 if w is oldw: c += 1 l.addWidget(w, row, c) row += 1 self.sep = f = QFrame(self) f.setFrameShape(f.VLine) l.addWidget(f, 0, 2, row, 1) self.sep2 = f = QFrame(self) f.setFrameShape(f.VLine) l.addWidget(f, 0, 4, row, 1) if 'comments' in self.widgets and not gprefs.get('diff_widget_show_comments_controls', True): self.widgets['comments'].new.hide_toolbars() def save_comments_controls_state(self): if 'comments' in self.widgets: vis = self.widgets['comments'].new.toolbars_visible if vis != gprefs.get('diff_widget_show_comments_controls', True): gprefs.set('diff_widget_show_comments_controls', vis) def changed(self, field): w = self.widgets[field] if not w.new.same_as(w.old) and (not self.blank_as_equal or not w.new.is_blank): w.label.setFont(self.changed_font) else: w.label.setFont(QApplication.font()) def revert(self, field): widgets = self.widgets[field] neww, oldw = widgets[:2] neww.current_val = oldw.current_val def __call__(self, oldmi, newmi): self.current_mi = newmi self.initial_vals = {} for field, widgets in self.widgets.iteritems(): widgets.old.from_mi(oldmi) widgets.new.from_mi(newmi) self.initial_vals[field] = widgets.new.current_val def apply_changes(self): changed = False for field, widgets in self.widgets.iteritems(): val = widgets.new.current_val if val != self.initial_vals[field]: widgets.new.to_mi(self.current_mi) changed = True return changed
class CompareSingle(QWidget): def __init__(self, field_metadata, parent=None, revert_tooltip=None, datetime_fmt='MMMM yyyy', blank_as_equal=True, fields=('title', 'authors', 'series', 'tags', 'rating', 'publisher', 'pubdate', 'identifiers', 'languages', 'comments', 'cover')): QWidget.__init__(self, parent) self.l = l = QGridLayout() l.setContentsMargins(0, 0, 0, 0) self.setLayout(l) revert_tooltip = revert_tooltip or _('Revert %s') self.current_mi = None self.changed_font = QFont(QApplication.font()) self.changed_font.setBold(True) self.changed_font.setItalic(True) self.blank_as_equal = blank_as_equal self.widgets = OrderedDict() row = 0 for field in fields: m = field_metadata[field] dt = m['datatype'] extra = None if 'series' in {field, dt}: cls = SeriesEdit elif field == 'identifiers': cls = IdentifiersEdit elif field == 'languages': cls = LanguagesEdit elif 'comments' in {field, dt}: cls = CommentsEdit elif 'rating' in {field, dt}: cls = RatingsEdit elif dt == 'datetime': extra = datetime_fmt cls = DateEdit elif field == 'cover': cls = CoverView elif dt in {'text', 'enum'}: cls = LineEdit else: continue neww = cls(field, True, self, m, extra) neww.changed.connect(partial(self.changed, field)) oldw = cls(field, False, self, m, extra) newl = QLabel('&%s:' % m['name']) newl.setBuddy(neww) button = QToolButton(self) button.setIcon(QIcon(I('back.png'))) button.clicked.connect(partial(self.revert, field)) button.setToolTip(revert_tooltip % m['name']) self.widgets[field] = Widgets(neww, oldw, newl, button) for i, w in enumerate((newl, neww, button, oldw)): c = i if i < 2 else i + 1 if w is oldw: c += 1 l.addWidget(w, row, c) row += 1 self.sep = f = QFrame(self) f.setFrameShape(f.VLine) l.addWidget(f, 0, 2, row, 1) self.sep2 = f = QFrame(self) f.setFrameShape(f.VLine) l.addWidget(f, 0, 4, row, 1) if 'comments' in self.widgets and not gprefs.get( 'diff_widget_show_comments_controls', True): self.widgets['comments'].new.hide_toolbars() def save_comments_controls_state(self): if 'comments' in self.widgets: vis = self.widgets['comments'].new.toolbars_visible if vis != gprefs.get('diff_widget_show_comments_controls', True): gprefs.set('diff_widget_show_comments_controls', vis) def changed(self, field): w = self.widgets[field] if not w.new.same_as(w.old) and (not self.blank_as_equal or not w.new.is_blank): w.label.setFont(self.changed_font) else: w.label.setFont(QApplication.font()) def revert(self, field): widgets = self.widgets[field] neww, oldw = widgets[:2] neww.current_val = oldw.current_val def __call__(self, oldmi, newmi): self.current_mi = newmi self.initial_vals = {} for field, widgets in self.widgets.iteritems(): widgets.old.from_mi(oldmi) widgets.new.from_mi(newmi) self.initial_vals[field] = widgets.new.current_val def apply_changes(self): changed = False for field, widgets in self.widgets.iteritems(): val = widgets.new.current_val if val != self.initial_vals[field]: widgets.new.to_mi(self.current_mi) changed = True return changed