Example #1
0
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()
Example #2
0
    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
Example #3
0
File: ui.py Project: GRiker/calibre
 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)
Example #4
0
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()
Example #5
0
File: ui.py Project: zyhong/calibre
 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)
Example #6
0
    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)
Example #7
0
    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)
Example #8
0
 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
Example #9
0
    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
Example #10
0
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)
Example #11
0
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
Example #12
0
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
Example #13
0
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
Example #14
0
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)
Example #15
0
 def mark_item_as_current(self, item):
     font = QFont(self.font())
     font.setItalic(True)
     font.setBold(True)
     item.setData(0, Qt.FontRole, font)
Example #16
0
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)
Example #17
0
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)
Example #18
0
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)
Example #19
0
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)
Example #20
0
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
Example #21
0
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