예제 #1
0
파일: model.py 프로젝트: smdx023/calibre
class NewsCategory(NewsTreeItem):
    def __init__(self, category, builtin, custom, scheduler_config, parent):
        NewsTreeItem.__init__(self, builtin, custom, scheduler_config, parent)
        self.category = category
        self.cdata = get_language(self.category)
        if self.category == _('Scheduled'):
            self.sortq = 0, ''
        elif self.category == _('Custom'):
            self.sortq = 1, ''
        else:
            self.sortq = 2, self.cdata
        self.bold_font = QFont()
        self.bold_font.setBold(True)
        self.bold_font = (self.bold_font)

    def data(self, role):
        if role == Qt.ItemDataRole.DisplayRole:
            return (self.cdata + ' [%d]' % len(self.children))
        elif role == Qt.ItemDataRole.FontRole:
            return self.bold_font
        elif role == Qt.ItemDataRole.ForegroundRole and self.category == _(
                'Scheduled'):
            return QApplication.instance().palette().color(
                QPalette.ColorRole.Link)
        elif role == Qt.ItemDataRole.UserRole:
            return '::category::{}'.format(self.sortq[0])
        return None

    def flags(self):
        return Qt.ItemFlag.ItemIsEnabled

    def __eq__(self, other):
        return self.cdata == other.cdata

    def __lt__(self, other):
        return self.sortq < getattr(other, 'sortq', (3, ''))
예제 #2
0
    def paint_line_numbers(self, ev):
        painter = QPainter(self.line_number_area)
        painter.fillRect(
            ev.rect(), self.line_number_palette.color(QPalette.ColorRole.Base))

        block = self.firstVisibleBlock()
        num = block.blockNumber()
        top = int(
            self.blockBoundingGeometry(block).translated(
                self.contentOffset()).top())
        bottom = top + int(self.blockBoundingRect(block).height())
        current = self.textCursor().block().blockNumber()
        painter.setPen(self.line_number_palette.color(QPalette.ColorRole.Text))

        while block.isValid() and top <= ev.rect().bottom():
            if block.isVisible() and bottom >= ev.rect().top():
                if current == num:
                    painter.save()
                    painter.setPen(
                        self.line_number_palette.color(
                            QPalette.ColorRole.BrightText))
                    f = QFont(self.font())
                    f.setBold(True)
                    painter.setFont(f)
                    self.last_current_lnum = (top, bottom - top)
                painter.drawText(0, top,
                                 self.line_number_area.width() - 5,
                                 self.fontMetrics().height(),
                                 Qt.AlignmentFlag.AlignRight,
                                 unicode_type(num + 1))
                if current == num:
                    painter.restore()
            block = block.next()
            top = bottom
            bottom = top + int(self.blockBoundingRect(block).height())
            num += 1
예제 #3
0
 def __init__(self, parent=None, is_half_star=False):
     QComboBox.__init__(self, parent)
     self.addItem(_('Not rated'))
     if is_half_star:
         [self.addItem(stars(x, True)) for x in range(1, 11)]
     else:
         [self.addItem(stars(x)) for x in (2, 4, 6, 8, 10)]
     self.rating_font = QFont(rating_font())
     self.undo_stack = QUndoStack(self)
     self.undo, self.redo = self.undo_stack.undo, self.undo_stack.redo
     self.allow_undo = False
     self.is_half_star = is_half_star
     self.delegate = RatingItemDelegate(self)
     self.view().setItemDelegate(self.delegate)
     self.view().setStyleSheet('QListView { background: palette(window) }\nQListView::item { padding: 6px }')
     self.setMaxVisibleItems(self.count())
     self.currentIndexChanged.connect(self.update_font)
예제 #4
0
    def do_paint(self, painter, option, index):
        text = str(index.data(Qt.ItemDataRole.DisplayRole) or '')
        font = QFont(option.font)
        font.setPointSizeF(QFontInfo(font).pointSize() * 1.5)
        font2 = QFont(font)
        font2.setFamily(text)

        system, has_latin = writing_system_for_font(font2)
        if has_latin:
            font = font2

        r = option.rect
        color = option.palette.text()

        if option.state & QStyle.StateFlag.State_Selected:
            color = option.palette.highlightedText()
        painter.setPen(QPen(color, 0))

        if (option.direction == Qt.LayoutDirection.RightToLeft):
            r.setRight(r.right() - 4)
        else:
            r.setLeft(r.left() + 4)

        painter.setFont(font)
        painter.drawText(
            r, Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeading
            | Qt.TextFlag.TextSingleLine, text)

        if (system != QFontDatabase.WritingSystem.Any):
            w = painter.fontMetrics().width(text + "  ")
            painter.setFont(font2)
            sample = QFontDatabase().writingSystemSample(system)
            if (option.direction == Qt.LayoutDirection.RightToLeft):
                r.setRight(r.right() - w)
            else:
                r.setLeft(r.left() + w)
            painter.drawText(
                r, Qt.AlignmentFlag.AlignVCenter
                | Qt.AlignmentFlag.AlignLeading | Qt.TextFlag.TextSingleLine,
                sample)
예제 #5
0
 def set_value(self, g, val):
     from calibre.gui2.convert.xpath_wizard import XPathEdit
     from calibre.gui2.convert.regex_builder import RegexEdit
     from calibre.gui2.widgets import EncodingComboBox
     if self.set_value_handler(g, val):
         return
     if hasattr(g, 'set_value_for_config'):
         g.set_value_for_config = val
         return
     if isinstance(g, (QSpinBox, QDoubleSpinBox)):
         g.setValue(val)
     elif isinstance(g, (QLineEdit, QTextEdit, QPlainTextEdit)):
         if not val:
             val = ''
         getattr(g, 'setPlainText', getattr(g, 'setText', None))(val)
         getattr(g, 'setCursorPosition', lambda x: x)(0)
     elif isinstance(g, QFontComboBox):
         g.setCurrentFont(QFont(val or ''))
     elif isinstance(g, FontFamilyChooser):
         g.font_family = val
     elif isinstance(g, EncodingComboBox):
         if val:
             g.setEditText(val)
         else:
             g.setCurrentIndex(0)
     elif isinstance(g, QComboBox) and val:
         idx = g.findText(val, Qt.MatchFlag.MatchFixedString)
         if idx < 0:
             g.addItem(val)
             idx = g.findText(val, Qt.MatchFlag.MatchFixedString)
         g.setCurrentIndex(idx)
     elif isinstance(g, QCheckBox):
         g.setCheckState(Qt.CheckState.Checked if bool(val) else Qt.
                         CheckState.Unchecked)
     elif isinstance(g, (XPathEdit, RegexEdit)):
         g.edit.setText(val if val else '')
     else:
         raise Exception('Can\'t set value %s in %s' %
                         (repr(val), unicode_type(g.objectName())))
     self.post_set_value(g, val)
    def initialize_formats(self):
        font_name = gprefs.get('gpm_template_editor_font', None)
        size = gprefs['gpm_template_editor_font_size']
        if font_name is None:
            font = QFont()
            font.setFixedPitch(True)
            font.setPointSize(size)
            font_name = font.family()
        config = self.Config = {}
        config["fontfamily"] = font_name
        app_palette = QApplication.instance().palette()
        for name, color, bold, italic in (
            ("normal", None, False, False),
            ("keyword", app_palette.color(QPalette.ColorRole.Link).name(),
             True, False), ("builtin",
                            app_palette.color(QPalette.ColorRole.Link).name(),
                            False, False), ("identifier", None, False, True),
            ("comment", "#007F00", False, True), ("string", "#808000", False,
                                                  False), ("number", "#924900",
                                                           False, False),
            ("lparen", None, True, True), ("rparen", None, True, True)):
            config["%sfontcolor" % name] = color
            config["%sfontbold" % name] = bold
            config["%sfontitalic" % name] = italic
        base_format = QTextCharFormat()
        base_format.setFontFamily(config["fontfamily"])
        config["fontsize"] = size
        base_format.setFontPointSize(config["fontsize"])

        self.Formats = {}
        for name in ("normal", "keyword", "builtin", "comment", "identifier",
                     "string", "number", "lparen", "rparen"):
            format_ = QTextCharFormat(base_format)
            color = config["%sfontcolor" % name]
            if color:
                format_.setForeground(QColor(color))
            if config["%sfontbold" % name]:
                format_.setFontWeight(QFont.Weight.Bold)
            format_.setFontItalic(config["%sfontitalic" % name])
            self.Formats[name] = format_
예제 #7
0
    def paint_line_numbers(self, ev):
        painter = QPainter(self.line_number_area)
        painter.fillRect(
            ev.rect(), self.line_number_palette.color(QPalette.ColorRole.Base))

        block = self.firstVisibleBlock()
        num = block.blockNumber()
        top = int(
            self.blockBoundingGeometry(block).translated(
                self.contentOffset()).top())
        bottom = top + int(self.blockBoundingRect(block).height())
        current = self.textCursor().block().blockNumber()
        painter.setPen(self.line_number_palette.color(QPalette.ColorRole.Text))

        while block.isValid() and top <= ev.rect().bottom():
            if block.isVisible() and bottom >= ev.rect().top():
                set_bold = False
                set_italic = False
                if current == num:
                    set_bold = True
                if num + 1 in self.clicked_line_numbers:
                    set_italic = True
                painter.save()
                if set_bold or set_italic:
                    f = QFont(self.font())
                    if set_bold:
                        f.setBold(set_bold)
                        painter.setPen(
                            self.line_number_palette.color(
                                QPalette.ColorRole.BrightText))
                    f.setItalic(set_italic)
                    painter.setFont(f)
                else:
                    painter.setFont(self.font())
                painter.drawText(0, top,
                                 self.line_number_area.width() - 5,
                                 self.fontMetrics().height(),
                                 Qt.AlignmentFlag.AlignRight, str(num + 1))
                painter.restore()
            block = block.next()
            top = bottom
            bottom = top + int(self.blockBoundingRect(block).height())
            num += 1
예제 #8
0
파일: diff.py 프로젝트: sazaed/calibre
    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.setObjectName(field)
            connect_lambda(
                neww.changed, self,
                lambda self: self.changed(self.sender().objectName()))
            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.setObjectName(field)
            connect_lambda(
                button.clicked, self,
                lambda self: self.revert(self.sender().objectName()))
            button.setToolTip(revert_tooltip % m['name'])
            if field == 'identifiers':
                button.m = m = QMenu(button)
                button.setMenu(m)
                button.setPopupMode(
                    QToolButton.ToolButtonPopupMode.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.ToolButtonPopupMode.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

        if 'comments' in self.widgets and not gprefs.get(
                'diff_widget_show_comments_controls', True):
            self.widgets['comments'].new.hide_toolbars()
예제 #9
0
파일: diff.py 프로젝트: sazaed/calibre
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.setObjectName(field)
            connect_lambda(
                neww.changed, self,
                lambda self: self.changed(self.sender().objectName()))
            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.setObjectName(field)
            connect_lambda(
                button.clicked, self,
                lambda self: self.revert(self.sender().objectName()))
            button.setToolTip(revert_tooltip % m['name'])
            if field == 'identifiers':
                button.m = m = QMenu(button)
                button.setMenu(m)
                button.setPopupMode(
                    QToolButton.ToolButtonPopupMode.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.ToolButtonPopupMode.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

        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]
        if hasattr(neww, 'set_undoable'):
            neww.set_undoable(oldw.current_val)
        else:
            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 iteritems(self.widgets):
            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 iteritems(self.widgets):
            val = widgets.new.current_val
            if val != self.initial_vals[field]:
                widgets.new.to_mi(self.current_mi)
                changed = True
        return changed
예제 #10
0
class EmailAccounts(QAbstractTableModel):  # {{{
    def __init__(self, accounts, subjects, aliases={}, tags={}):
        QAbstractTableModel.__init__(self)
        self.accounts = accounts
        self.subjects = subjects
        self.aliases = aliases
        self.tags = tags
        self.sorted_on = (0, True)
        self.account_order = list(self.accounts)
        self.do_sort()
        self.headers = [
            _('Email'),
            _('Formats'),
            _('Subject'),
            _('Auto send'),
            _('Alias'),
            _('Auto send only tags')
        ]
        self.default_font = QFont()
        self.default_font.setBold(True)
        self.default_font = (self.default_font)
        self.tooltips = [None] + list(
            map(textwrap.fill, [
                _('Formats to email. The first matching format will be sent.'),
                _('Subject of the email to use when sending. When left blank '
                  'the title will be used for the subject. Also, the same '
                  'templates used for "Save to disk" such as {title} and '
                  '{author_sort} can be used here.'), '<p>' +
                _('If checked, downloaded news will be automatically '
                  'mailed to this email address '
                  '(provided it is in one of the listed formats and has not been filtered by tags).'
                  ),
                _('Friendly name to use for this email address'),
                _('If specified, only news with one of these tags will be sent to'
                  ' this email address. All news downloads have their title as a'
                  ' tag, so you can use this to easily control which news downloads'
                  ' are sent to this email address.')
            ]))

    def do_sort(self):
        col = self.sorted_on[0]
        if col == 0:

            def key(account_key):
                return numeric_sort_key(account_key)
        elif col == 1:

            def key(account_key):
                return numeric_sort_key(self.accounts[account_key][0] or '')
        elif col == 2:

            def key(account_key):
                return numeric_sort_key(self.subjects.get(account_key) or '')
        elif col == 3:

            def key(account_key):
                return numeric_sort_key(
                    as_unicode(self.accounts[account_key][0]) or '')
        elif col == 4:

            def key(account_key):
                return numeric_sort_key(self.aliases.get(account_key) or '')
        elif col == 5:

            def key(account_key):
                return numeric_sort_key(self.tags.get(account_key) or '')

        self.account_order.sort(key=key, reverse=not self.sorted_on[1])

    def sort(self, column, order=Qt.SortOrder.AscendingOrder):
        nsort = (column, order == Qt.SortOrder.AscendingOrder)
        if nsort != self.sorted_on:
            self.sorted_on = nsort
            self.beginResetModel()
            try:
                self.do_sort()
            finally:
                self.endResetModel()

    def rowCount(self, *args):
        return len(self.account_order)

    def columnCount(self, *args):
        return len(self.headers)

    def headerData(self, section, orientation, role):
        if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
            return self.headers[section]
        return None

    def data(self, index, role):
        row, col = index.row(), index.column()
        if row < 0 or row >= self.rowCount():
            return None
        account = self.account_order[row]
        if account not in self.accounts:
            return None
        if role == Qt.ItemDataRole.UserRole:
            return (account, self.accounts[account])
        if role == Qt.ItemDataRole.ToolTipRole:
            return self.tooltips[col]
        if role in [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole]:
            if col == 0:
                return (account)
            if col == 1:
                return ', '.join(
                    x.strip()
                    for x in (self.accounts[account][0] or '').split(','))
            if col == 2:
                return (self.subjects.get(account, ''))
            if col == 4:
                return (self.aliases.get(account, ''))
            if col == 5:
                return (self.tags.get(account, ''))
        if role == Qt.ItemDataRole.FontRole and self.accounts[account][2]:
            return self.default_font
        if role == Qt.ItemDataRole.CheckStateRole and col == 3:
            return (Qt.CheckState.Checked
                    if self.accounts[account][1] else Qt.CheckState.Unchecked)
        return None

    def flags(self, index):
        if index.column() == 3:
            return QAbstractTableModel.flags(
                self, index) | Qt.ItemFlag.ItemIsUserCheckable
        else:
            return QAbstractTableModel.flags(
                self, index) | Qt.ItemFlag.ItemIsEditable

    def setData(self, index, value, role):
        if not index.isValid():
            return False
        row, col = index.row(), index.column()
        account = self.account_order[row]
        if col == 3:
            self.accounts[account][1] ^= True
        elif col == 2:
            self.subjects[account] = as_unicode(value or '')
        elif col == 4:
            self.aliases.pop(account, None)
            aval = as_unicode(value or '').strip()
            if aval:
                self.aliases[account] = aval
        elif col == 5:
            self.tags.pop(account, None)
            aval = as_unicode(value or '').strip()
            if aval:
                self.tags[account] = aval
        elif col == 1:
            self.accounts[account][0] = re.sub(
                ',+', ',', re.sub(r'\s+', ',',
                                  as_unicode(value or '').upper()))
        elif col == 0:
            na = as_unicode(value or '')
            from email.utils import parseaddr
            addr = parseaddr(na)[-1]
            if not addr or '@' not in na:
                return False
            self.accounts[na] = self.accounts.pop(account)
            self.account_order[row] = na
            if '@kindle.com' in addr:
                self.accounts[na][0] = 'AZW, MOBI, TPZ, PRC, AZW1'

        self.dataChanged.emit(self.index(index.row(), 0),
                              self.index(index.row(), 3))
        return True

    def make_default(self, index):
        if index.isValid():
            self.beginResetModel()
            row = index.row()
            for x in self.accounts.values():
                x[2] = False
            self.accounts[self.account_order[row]][2] = True
            self.endResetModel()

    def add(self):
        x = _('new email address')
        y = x
        c = 0
        while y in self.accounts:
            c += 1
            y = x + str(c)
        auto_send = len(self.accounts) < 1
        self.beginResetModel()
        self.accounts[y] = [
            'MOBI, EPUB', auto_send,
            len(self.account_order) == 0
        ]
        self.account_order = list(self.accounts)
        self.do_sort()
        self.endResetModel()
        return self.index(self.account_order.index(y), 0)

    def remove_rows(self, *rows):
        for row in sorted(rows, reverse=True):
            try:
                account = self.account_order[row]
            except Exception:
                continue
            self.accounts.pop(account)
        self.account_order = sorted(self.accounts)
        has_default = False
        for account in self.account_order:
            if self.accounts[account][2]:
                has_default = True
                break
        if not has_default and self.account_order:
            self.accounts[self.account_order[0]][2] = True

        self.beginResetModel()
        self.endResetModel()
        self.do_sort()

    def remove(self, index):
        if index.isValid():
            self.remove(index.row())
예제 #11
0
class TextEdit(PlainTextEdit):

    link_clicked = pyqtSignal(object)
    class_clicked = pyqtSignal(object)
    smart_highlighting_updated = pyqtSignal()

    def __init__(self, parent=None, expected_geometry=(100, 50)):
        PlainTextEdit.__init__(self, parent)
        self.snippet_manager = SnippetManager(self)
        self.completion_popup = CompletionPopup(self)
        self.request_completion = self.completion_doc_name = None
        self.clear_completion_cache_timer = t = QTimer(self)
        t.setInterval(5000), t.timeout.connect(self.clear_completion_cache), t.setSingleShot(True)
        self.textChanged.connect(t.start)
        self.last_completion_request = -1
        self.gutter_width = 0
        self.tw = 2
        self.expected_geometry = expected_geometry
        self.saved_matches = {}
        self.syntax = None
        self.smarts = NullSmarts(self)
        self.current_cursor_line = None
        self.current_search_mark = None
        self.smarts_highlight_timer = t = QTimer()
        t.setInterval(750), t.setSingleShot(True), t.timeout.connect(self.update_extra_selections)
        self.highlighter = SyntaxHighlighter()
        self.line_number_area = LineNumbers(self)
        self.apply_settings()
        self.setMouseTracking(True)
        self.cursorPositionChanged.connect(self.highlight_cursor_line)
        self.blockCountChanged[int].connect(self.update_line_number_area_width)
        self.updateRequest.connect(self.update_line_number_area)

    def get_droppable_files(self, md):

        def is_mt_ok(mt):
            return self.syntax == 'html' and (
                mt in OEB_DOCS or mt in OEB_STYLES or mt.startswith('image/')
            )

        if md.hasFormat(CONTAINER_DND_MIMETYPE):
            for line in as_unicode(bytes(md.data(CONTAINER_DND_MIMETYPE))).splitlines():
                mt = current_container().mime_map.get(line, 'application/octet-stream')
                if is_mt_ok(mt):
                    yield line, mt, True
            return
        for qurl in md.urls():
            if qurl.isLocalFile() and os.access(qurl.toLocalFile(), os.R_OK):
                path = qurl.toLocalFile()
                mt = guess_type(path)
                if is_mt_ok(mt):
                    yield path, mt, False

    def canInsertFromMimeData(self, md):
        if md.hasText() or (md.hasHtml() and self.syntax == 'html') or md.hasImage():
            return True
        elif tuple(self.get_droppable_files(md)):
            return True
        return False

    def insertFromMimeData(self, md):
        files = tuple(self.get_droppable_files(md))
        base = self.highlighter.doc_name or None

        def get_name(name):
            folder = get_recommended_folders(current_container(), (name,))[name] or ''
            if folder:
                folder += '/'
            return folder + name

        def get_href(name):
            return current_container().name_to_href(name, base)

        def insert_text(text):
            c = self.textCursor()
            c.insertText(text)
            self.setTextCursor(c)
            self.ensureCursorVisible()

        def add_file(name, data, mt=None):
            from calibre.gui2.tweak_book.boss import get_boss
            name = current_container().add_file(name, data, media_type=mt, modify_name_if_needed=True)
            get_boss().refresh_file_list()
            return name

        if files:
            for path, mt, is_name in files:
                if is_name:
                    name = path
                else:
                    name = get_name(os.path.basename(path))
                    with lopen(path, 'rb') as f:
                        name = add_file(name, f.read(), mt)
                href = get_href(name)
                if mt.startswith('image/'):
                    self.insert_image(href)
                elif mt in OEB_STYLES:
                    insert_text('<link href="{}" rel="stylesheet" type="text/css"/>'.format(href))
                elif mt in OEB_DOCS:
                    self.insert_hyperlink(href, name)
            self.ensureCursorVisible()
            return
        if md.hasImage():
            img = md.imageData()
            if img is not None and not img.isNull():
                data = image_to_data(img, fmt='PNG')
                name = add_file(get_name('dropped_image.png'), data)
                self.insert_image(get_href(name))
                self.ensureCursorVisible()
                return
        if md.hasText():
            return insert_text(md.text())
        if md.hasHtml():
            insert_text(md.html())
            return

    @property
    def is_modified(self):
        ''' True if the document has been modified since it was loaded or since
        the last time is_modified was set to False. '''
        return self.document().isModified()

    @is_modified.setter
    def is_modified(self, val):
        self.document().setModified(bool(val))

    def sizeHint(self):
        return self.size_hint

    def apply_settings(self, prefs=None, dictionaries_changed=False):  # {{{
        prefs = prefs or tprefs
        self.setAcceptDrops(prefs.get('editor_accepts_drops', True))
        self.setLineWrapMode(QPlainTextEdit.LineWrapMode.WidgetWidth if prefs['editor_line_wrap'] else QPlainTextEdit.LineWrapMode.NoWrap)
        theme = get_theme(prefs['editor_theme'])
        self.apply_theme(theme)
        w = self.fontMetrics()
        self.space_width = w.width(' ')
        self.tw = self.smarts.override_tab_stop_width if self.smarts.override_tab_stop_width is not None else prefs['editor_tab_stop_width']
        self.setTabStopWidth(self.tw * self.space_width)
        if dictionaries_changed:
            self.highlighter.rehighlight()

    def apply_theme(self, theme):
        self.theme = theme
        pal = self.palette()
        pal.setColor(QPalette.ColorRole.Base, theme_color(theme, 'Normal', 'bg'))
        pal.setColor(QPalette.ColorRole.AlternateBase, theme_color(theme, 'CursorLine', 'bg'))
        pal.setColor(QPalette.ColorRole.Text, theme_color(theme, 'Normal', 'fg'))
        pal.setColor(QPalette.ColorRole.Highlight, theme_color(theme, 'Visual', 'bg'))
        pal.setColor(QPalette.ColorRole.HighlightedText, theme_color(theme, 'Visual', 'fg'))
        self.setPalette(pal)
        self.tooltip_palette = pal = QPalette()
        pal.setColor(QPalette.ColorRole.ToolTipBase, theme_color(theme, 'Tooltip', 'bg'))
        pal.setColor(QPalette.ColorRole.ToolTipText, theme_color(theme, 'Tooltip', 'fg'))
        self.line_number_palette = pal = QPalette()
        pal.setColor(QPalette.ColorRole.Base, theme_color(theme, 'LineNr', 'bg'))
        pal.setColor(QPalette.ColorRole.Text, theme_color(theme, 'LineNr', 'fg'))
        pal.setColor(QPalette.ColorRole.BrightText, theme_color(theme, 'LineNrC', 'fg'))
        self.match_paren_format = theme_format(theme, 'MatchParen')
        font = self.font()
        ff = tprefs['editor_font_family']
        if ff is None:
            ff = default_font_family()
        font.setFamily(ff)
        font.setPointSize(tprefs['editor_font_size'])
        self.tooltip_font = QFont(font)
        self.tooltip_font.setPointSize(font.pointSize() - 1)
        self.setFont(font)
        self.highlighter.apply_theme(theme)
        w = self.fontMetrics()
        self.number_width = max(map(lambda x:w.width(unicode_type(x)), range(10)))
        self.size_hint = QSize(self.expected_geometry[0] * w.averageCharWidth(), self.expected_geometry[1] * w.height())
        self.highlight_color = theme_color(theme, 'HighlightRegion', 'bg')
        self.highlight_cursor_line()
        self.completion_popup.clear_caches(), self.completion_popup.update()
    # }}}

    def load_text(self, text, syntax='html', process_template=False, doc_name=None):
        self.syntax = syntax
        self.highlighter = get_highlighter(syntax)()
        self.highlighter.apply_theme(self.theme)
        self.highlighter.set_document(self.document(), doc_name=doc_name)
        sclass = get_smarts(syntax)
        if sclass is not None:
            self.smarts = sclass(self)
            if self.smarts.override_tab_stop_width is not None:
                self.tw = self.smarts.override_tab_stop_width
                self.setTabStopWidth(self.tw * self.space_width)
        if isinstance(text, bytes):
            text = text.decode('utf-8', 'replace')
        self.setPlainText(unicodedata.normalize('NFC', unicode_type(text)))
        if process_template and QPlainTextEdit.find(self, '%CURSOR%'):
            c = self.textCursor()
            c.insertText('')

    def change_document_name(self, newname):
        self.highlighter.doc_name = newname
        self.highlighter.rehighlight()  # Ensure links are checked w.r.t. the new name correctly

    def replace_text(self, text):
        c = self.textCursor()
        pos = c.position()
        c.beginEditBlock()
        c.clearSelection()
        c.select(QTextCursor.SelectionType.Document)
        c.insertText(unicodedata.normalize('NFC', text))
        c.endEditBlock()
        c.setPosition(min(pos, len(text)))
        self.setTextCursor(c)
        self.ensureCursorVisible()

    def simple_replace(self, text, cursor=None):
        c = cursor or self.textCursor()
        c.insertText(unicodedata.normalize('NFC', text))
        self.setTextCursor(c)

    def go_to_line(self, lnum, col=None):
        lnum = max(1, min(self.blockCount(), lnum))
        c = self.textCursor()
        c.clearSelection()
        c.movePosition(QTextCursor.MoveOperation.Start)
        c.movePosition(QTextCursor.MoveOperation.NextBlock, n=lnum - 1)
        c.movePosition(QTextCursor.MoveOperation.StartOfLine)
        c.movePosition(QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor)
        text = unicode_type(c.selectedText()).rstrip('\0')
        if col is None:
            c.movePosition(QTextCursor.MoveOperation.StartOfLine)
            lt = text.lstrip()
            if text and lt and lt != text:
                c.movePosition(QTextCursor.MoveOperation.NextWord)
        else:
            c.setPosition(c.block().position() + col)
            if c.blockNumber() + 1 > lnum:
                # We have moved past the end of the line
                c.setPosition(c.block().position())
                c.movePosition(QTextCursor.MoveOperation.EndOfBlock)
        self.setTextCursor(c)
        self.ensureCursorVisible()

    def update_extra_selections(self, instant=True):
        sel = []
        if self.current_cursor_line is not None:
            sel.append(self.current_cursor_line)
        if self.current_search_mark is not None:
            sel.append(self.current_search_mark)
        if instant and not self.highlighter.has_requests and self.smarts is not None:
            sel.extend(self.smarts.get_extra_selections(self))
            self.smart_highlighting_updated.emit()
        else:
            self.smarts_highlight_timer.start()
        self.setExtraSelections(sel)

    # Search and replace {{{
    def mark_selected_text(self):
        sel = QTextEdit.ExtraSelection()
        sel.format.setBackground(self.highlight_color)
        sel.cursor = self.textCursor()
        if sel.cursor.hasSelection():
            self.current_search_mark = sel
            c = self.textCursor()
            c.clearSelection()
            self.setTextCursor(c)
        else:
            self.current_search_mark = None
        self.update_extra_selections()

    def find_in_marked(self, pat, wrap=False, save_match=None):
        if self.current_search_mark is None:
            return False
        csm = self.current_search_mark.cursor
        reverse = pat.flags & regex.REVERSE
        c = self.textCursor()
        c.clearSelection()
        m_start = min(csm.position(), csm.anchor())
        m_end = max(csm.position(), csm.anchor())
        if c.position() < m_start:
            c.setPosition(m_start)
        if c.position() > m_end:
            c.setPosition(m_end)
        pos = m_start if reverse else m_end
        if wrap:
            pos = m_end if reverse else m_start
        c.setPosition(pos, QTextCursor.MoveMode.KeepAnchor)
        raw = unicode_type(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0')
        m = pat.search(raw)
        if m is None:
            return False
        start, end = m.span()
        if start == end:
            return False
        if wrap:
            if reverse:
                textpos = c.anchor()
                start, end = textpos + end, textpos + start
            else:
                start, end = m_start + start, m_start + end
        else:
            if reverse:
                start, end = m_start + end, m_start + start
            else:
                start, end = c.anchor() + start, c.anchor() + end

        c.clearSelection()
        c.setPosition(start)
        c.setPosition(end, QTextCursor.MoveMode.KeepAnchor)
        self.setTextCursor(c)
        # Center search result on screen
        self.centerCursor()
        if save_match is not None:
            self.saved_matches[save_match] = (pat, m)
        return True

    def all_in_marked(self, pat, template=None):
        if self.current_search_mark is None:
            return 0
        c = self.current_search_mark.cursor
        raw = unicode_type(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0')
        if template is None:
            count = len(pat.findall(raw))
        else:
            from calibre.gui2.tweak_book.function_replace import Function
            repl_is_func = isinstance(template, Function)
            if repl_is_func:
                template.init_env()
            raw, count = pat.subn(template, raw)
            if repl_is_func:
                from calibre.gui2.tweak_book.search import show_function_debug_output
                if getattr(template.func, 'append_final_output_to_marked', False):
                    retval = template.end()
                    if retval:
                        raw += unicode_type(retval)
                else:
                    template.end()
                show_function_debug_output(template)
            if count > 0:
                start_pos = min(c.anchor(), c.position())
                c.insertText(raw)
                end_pos = max(c.anchor(), c.position())
                c.setPosition(start_pos), c.setPosition(end_pos, QTextCursor.MoveMode.KeepAnchor)
                self.update_extra_selections()
        return count

    def smart_comment(self):
        from calibre.gui2.tweak_book.editor.comments import smart_comment
        smart_comment(self, self.syntax)

    def sort_css(self):
        from calibre.gui2.dialogs.confirm_delete import confirm
        if confirm(_('Sorting CSS rules can in rare cases change the effective styles applied to the book.'
                     ' Are you sure you want to proceed?'), 'edit-book-confirm-sort-css', parent=self, config_set=tprefs):
            c = self.textCursor()
            c.beginEditBlock()
            c.movePosition(QTextCursor.MoveOperation.Start), c.movePosition(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.KeepAnchor)
            text = unicode_type(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0')
            from calibre.ebooks.oeb.polish.css import sort_sheet
            text = css_text(sort_sheet(current_container(), text))
            c.insertText(text)
            c.movePosition(QTextCursor.MoveOperation.Start)
            c.endEditBlock()
            self.setTextCursor(c)

    def find(self, pat, wrap=False, marked=False, complete=False, save_match=None):
        if marked:
            return self.find_in_marked(pat, wrap=wrap, save_match=save_match)
        reverse = pat.flags & regex.REVERSE
        c = self.textCursor()
        c.clearSelection()
        if complete:
            # Search the entire text
            c.movePosition(QTextCursor.MoveOperation.End if reverse else QTextCursor.MoveOperation.Start)
        pos = QTextCursor.MoveOperation.Start if reverse else QTextCursor.MoveOperation.End
        if wrap and not complete:
            pos = QTextCursor.MoveOperation.End if reverse else QTextCursor.MoveOperation.Start
        c.movePosition(pos, QTextCursor.MoveMode.KeepAnchor)
        raw = unicode_type(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0')
        m = pat.search(raw)
        if m is None:
            return False
        start, end = m.span()
        if start == end:
            return False
        if wrap and not complete:
            if reverse:
                textpos = c.anchor()
                start, end = textpos + end, textpos + start
        else:
            if reverse:
                # Put the cursor at the start of the match
                start, end = end, start
            else:
                textpos = c.anchor()
                start, end = textpos + start, textpos + end
        c.clearSelection()
        c.setPosition(start)
        c.setPosition(end, QTextCursor.MoveMode.KeepAnchor)
        self.setTextCursor(c)
        # Center search result on screen
        self.centerCursor()
        if save_match is not None:
            self.saved_matches[save_match] = (pat, m)
        return True

    def find_text(self, pat, wrap=False, complete=False):
        reverse = pat.flags & regex.REVERSE
        c = self.textCursor()
        c.clearSelection()
        if complete:
            # Search the entire text
            c.movePosition(QTextCursor.MoveOperation.End if reverse else QTextCursor.MoveOperation.Start)
        pos = QTextCursor.MoveOperation.Start if reverse else QTextCursor.MoveOperation.End
        if wrap and not complete:
            pos = QTextCursor.MoveOperation.End if reverse else QTextCursor.MoveOperation.Start
        c.movePosition(pos, QTextCursor.MoveMode.KeepAnchor)
        if hasattr(self.smarts, 'find_text'):
            self.highlighter.join()
            found, start, end = self.smarts.find_text(pat, c, reverse)
            if not found:
                return False
        else:
            raw = unicode_type(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0')
            m = pat.search(raw)
            if m is None:
                return False
            start, end = m.span()
            if start == end:
                return False
        if reverse:
            start, end = end, start
        c.clearSelection()
        c.setPosition(start)
        c.setPosition(end, QTextCursor.MoveMode.KeepAnchor)
        self.setTextCursor(c)
        # Center search result on screen
        self.centerCursor()
        return True

    def find_spell_word(self, original_words, lang, from_cursor=True, center_on_cursor=True):
        c = self.textCursor()
        c.setPosition(c.position())
        if not from_cursor:
            c.movePosition(QTextCursor.MoveOperation.Start)
        c.movePosition(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.KeepAnchor)

        def find_first_word(haystack):
            match_pos, match_word = -1, None
            for w in original_words:
                idx = index_of(w, haystack, lang=lang)
                if idx > -1 and (match_pos == -1 or match_pos > idx):
                    match_pos, match_word = idx, w
            return match_pos, match_word

        while True:
            text = unicode_type(c.selectedText()).rstrip('\0')
            idx, word = find_first_word(text)
            if idx == -1:
                return False
            c.setPosition(c.anchor() + idx)
            c.setPosition(c.position() + string_length(word), QTextCursor.MoveMode.KeepAnchor)
            if self.smarts.verify_for_spellcheck(c, self.highlighter):
                self.highlighter.join()  # Ensure highlighting is finished
                locale = self.spellcheck_locale_for_cursor(c)
                if not lang or not locale or (locale and lang == locale.langcode):
                    self.setTextCursor(c)
                    if center_on_cursor:
                        self.centerCursor()
                    return True
            c.setPosition(c.position())
            c.movePosition(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.KeepAnchor)

        return False

    def find_next_spell_error(self, from_cursor=True):
        c = self.textCursor()
        if not from_cursor:
            c.movePosition(QTextCursor.MoveOperation.Start)
        block = c.block()
        while block.isValid():
            for r in block.layout().additionalFormats():
                if r.format.property(SPELL_PROPERTY):
                    if not from_cursor or block.position() + r.start + r.length > c.position():
                        c.setPosition(block.position() + r.start)
                        c.setPosition(c.position() + r.length, QTextCursor.MoveMode.KeepAnchor)
                        self.setTextCursor(c)
                        return True
            block = block.next()
        return False

    def replace(self, pat, template, saved_match='gui'):
        c = self.textCursor()
        raw = unicode_type(c.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0')
        m = pat.fullmatch(raw)
        if m is None:
            # This can happen if either the user changed the selected text or
            # the search expression uses lookahead/lookbehind operators. See if
            # the saved match matches the currently selected text and
            # use it, if so.
            if saved_match is not None and saved_match in self.saved_matches:
                saved_pat, saved = self.saved_matches.pop(saved_match)
                if saved_pat == pat and saved.group() == raw:
                    m = saved
        if m is None:
            return False
        if callable(template):
            text = template(m)
        else:
            text = m.expand(template)
        c.insertText(text)
        return True

    def go_to_anchor(self, anchor):
        if anchor is TOP:
            c = self.textCursor()
            c.movePosition(QTextCursor.MoveOperation.Start)
            self.setTextCursor(c)
            return True
        base = r'''%%s\s*=\s*['"]{0,1}%s''' % regex.escape(anchor)
        raw = unicode_type(self.toPlainText())
        m = regex.search(base % 'id', raw)
        if m is None:
            m = regex.search(base % 'name', raw)
        if m is not None:
            c = self.textCursor()
            c.setPosition(m.start())
            self.setTextCursor(c)
            return True
        return False

    # }}}

    # Line numbers and cursor line {{{
    def highlight_cursor_line(self):
        sel = QTextEdit.ExtraSelection()
        sel.format.setBackground(self.palette().alternateBase())
        sel.format.setProperty(QTextFormat.Property.FullWidthSelection, True)
        sel.cursor = self.textCursor()
        sel.cursor.clearSelection()
        self.current_cursor_line = sel
        self.update_extra_selections(instant=False)
        # Update the cursor line's line number in the line number area
        try:
            self.line_number_area.update(0, self.last_current_lnum[0], self.line_number_area.width(), self.last_current_lnum[1])
        except AttributeError:
            pass
        block = self.textCursor().block()
        top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
        height = int(self.blockBoundingRect(block).height())
        self.line_number_area.update(0, top, self.line_number_area.width(), height)

    def update_line_number_area_width(self, block_count=0):
        self.gutter_width = self.line_number_area_width()
        self.setViewportMargins(self.gutter_width, 0, 0, 0)

    def line_number_area_width(self):
        digits = 1
        limit = max(1, self.blockCount())
        while limit >= 10:
            limit /= 10
            digits += 1

        return 8 + self.number_width * digits

    def update_line_number_area(self, rect, dy):
        if dy:
            self.line_number_area.scroll(0, dy)
        else:
            self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height())
        if rect.contains(self.viewport().rect()):
            self.update_line_number_area_width()

    def resizeEvent(self, ev):
        QPlainTextEdit.resizeEvent(self, ev)
        cr = self.contentsRect()
        self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height()))

    def paint_line_numbers(self, ev):
        painter = QPainter(self.line_number_area)
        painter.fillRect(ev.rect(), self.line_number_palette.color(QPalette.ColorRole.Base))

        block = self.firstVisibleBlock()
        num = block.blockNumber()
        top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
        bottom = top + int(self.blockBoundingRect(block).height())
        current = self.textCursor().block().blockNumber()
        painter.setPen(self.line_number_palette.color(QPalette.ColorRole.Text))

        while block.isValid() and top <= ev.rect().bottom():
            if block.isVisible() and bottom >= ev.rect().top():
                if current == num:
                    painter.save()
                    painter.setPen(self.line_number_palette.color(QPalette.ColorRole.BrightText))
                    f = QFont(self.font())
                    f.setBold(True)
                    painter.setFont(f)
                    self.last_current_lnum = (top, bottom - top)
                painter.drawText(0, top, self.line_number_area.width() - 5, self.fontMetrics().height(),
                              Qt.AlignmentFlag.AlignRight, unicode_type(num + 1))
                if current == num:
                    painter.restore()
            block = block.next()
            top = bottom
            bottom = top + int(self.blockBoundingRect(block).height())
            num += 1
    # }}}

    def override_shortcut(self, ev):
        # Let the global cut/copy/paste/undo/redo shortcuts work, this avoids the nbsp
        # problem as well, since they use the overridden createMimeDataFromSelection() method
        # instead of the one from Qt (which makes copy() work), and allows proper customization
        # of the shortcuts
        if ev in (
            QKeySequence.StandardKey.Copy, QKeySequence.StandardKey.Cut, QKeySequence.StandardKey.Paste,
            QKeySequence.StandardKey.Undo, QKeySequence.StandardKey.Redo
        ):
            ev.ignore()
            return True
        # This is used to convert typed hex codes into unicode
        # characters
        if ev.key() == Qt.Key.Key_X and ev.modifiers() == Qt.KeyboardModifier.AltModifier:
            ev.accept()
            return True
        return PlainTextEdit.override_shortcut(self, ev)

    def text_for_range(self, block, r):
        c = self.textCursor()
        c.setPosition(block.position() + r.start)
        c.setPosition(c.position() + r.length, QTextCursor.MoveMode.KeepAnchor)
        return unicode_type(c.selectedText())

    def spellcheck_locale_for_cursor(self, c):
        with store_locale:
            formats = self.highlighter.parse_single_block(c.block())[0]
        pos = c.positionInBlock()
        for r in formats:
            if r.start <= pos <= r.start + r.length and r.format.property(SPELL_PROPERTY):
                return r.format.property(SPELL_LOCALE_PROPERTY)

    def recheck_word(self, word, locale):
        c = self.textCursor()
        c.movePosition(QTextCursor.MoveOperation.Start)
        block = c.block()
        while block.isValid():
            for r in block.layout().additionalFormats():
                if r.format.property(SPELL_PROPERTY) and self.text_for_range(block, r) == word:
                    self.highlighter.reformat_block(block)
                    break
            block = block.next()

    # Tooltips {{{
    def syntax_range_for_cursor(self, cursor):
        if cursor.isNull():
            return
        pos = cursor.positionInBlock()
        for r in cursor.block().layout().additionalFormats():
            if r.start <= pos <= r.start + r.length and r.format.property(SYNTAX_PROPERTY):
                return r

    def show_tooltip(self, ev):
        c = self.cursorForPosition(ev.pos())
        fmt_range = self.syntax_range_for_cursor(c)
        fmt = getattr(fmt_range, 'format', None)
        if fmt is not None:
            tt = unicode_type(fmt.toolTip())
            if tt:
                QToolTip.setFont(self.tooltip_font)
                QToolTip.setPalette(self.tooltip_palette)
                QToolTip.showText(ev.globalPos(), textwrap.fill(tt))
                return
        QToolTip.hideText()
        ev.ignore()
    # }}}

    def link_for_position(self, pos):
        c = self.cursorForPosition(pos)
        r = self.syntax_range_for_cursor(c)
        if r is not None and r.format.property(LINK_PROPERTY):
            return self.text_for_range(c.block(), r)

    def class_for_position(self, pos):
        c = self.cursorForPosition(pos)
        r = self.syntax_range_for_cursor(c)
        if r is not None and r.format.property(CLASS_ATTRIBUTE_PROPERTY):
            c.select(QTextCursor.SelectionType.WordUnderCursor)
            class_name = c.selectedText()
            if class_name:
                tags = self.current_tag(for_position_sync=False, cursor=c)
                return {'class': class_name, 'sourceline_address': tags}

    def mousePressEvent(self, ev):
        if self.completion_popup.isVisible() and not self.completion_popup.rect().contains(ev.pos()):
            # For some reason using eventFilter for this does not work, so we
            # implement it here
            self.completion_popup.abort()
        if ev.modifiers() & Qt.Modifier.CTRL:
            url = self.link_for_position(ev.pos())
            if url is not None:
                ev.accept()
                self.link_clicked.emit(url)
                return
            class_data = self.class_for_position(ev.pos())
            if class_data is not None:
                ev.accept()
                self.class_clicked.emit(class_data)
                return
        return PlainTextEdit.mousePressEvent(self, ev)

    def get_range_inside_tag(self):
        c = self.textCursor()
        left = min(c.anchor(), c.position())
        right = max(c.anchor(), c.position())
        # For speed we use QPlainTextEdit's toPlainText as we dont care about
        # spaces in this context
        raw = unicode_type(QPlainTextEdit.toPlainText(self))
        # Make sure the left edge is not within a <>
        gtpos = raw.find('>', left)
        ltpos = raw.find('<', left)
        if gtpos < ltpos:
            left = gtpos + 1 if gtpos > -1 else left
        right = max(left, right)
        if right != left:
            gtpos = raw.find('>', right)
            ltpos = raw.find('<', right)
            if ltpos > gtpos:
                ltpos = raw.rfind('<', left, right+1)
                right = max(ltpos, left)
        return left, right

    def format_text(self, formatting):
        if self.syntax != 'html':
            return
        if formatting.startswith('justify_'):
            return self.smarts.set_text_alignment(self, formatting.partition('_')[-1])
        color = 'currentColor'
        if formatting in {'color', 'background-color'}:
            color = QColorDialog.getColor(
                QColor(Qt.GlobalColor.black if formatting == 'color' else Qt.GlobalColor.white),
                self, _('Choose color'), QColorDialog.ColorDialogOption.ShowAlphaChannel)
            if not color.isValid():
                return
            r, g, b, a = color.getRgb()
            if a == 255:
                color = 'rgb(%d, %d, %d)' % (r, g, b)
            else:
                color = 'rgba(%d, %d, %d, %.2g)' % (r, g, b, a/255)
        prefix, suffix = {
            'bold': ('<b>', '</b>'),
            'italic': ('<i>', '</i>'),
            'underline': ('<u>', '</u>'),
            'strikethrough': ('<strike>', '</strike>'),
            'superscript': ('<sup>', '</sup>'),
            'subscript': ('<sub>', '</sub>'),
            'color': ('<span style="color: %s">' % color, '</span>'),
            'background-color': ('<span style="background-color: %s">' % color, '</span>'),
        }[formatting]
        left, right = self.get_range_inside_tag()
        c = self.textCursor()
        c.setPosition(left)
        c.setPosition(right, QTextCursor.MoveMode.KeepAnchor)
        prev_text = unicode_type(c.selectedText()).rstrip('\0')
        c.insertText(prefix + prev_text + suffix)
        if prev_text:
            right = c.position()
            c.setPosition(left)
            c.setPosition(right, QTextCursor.MoveMode.KeepAnchor)
        else:
            c.setPosition(c.position() - len(suffix))
        self.setTextCursor(c)

    def insert_image(self, href, fullpage=False, preserve_aspect_ratio=False, width=-1, height=-1):
        if width <= 0:
            width = 1200
        if height <= 0:
            height = 1600
        c = self.textCursor()
        template, alt = 'url(%s)', ''
        left = min(c.position(), c.anchor())
        if self.syntax == 'html':
            left, right = self.get_range_inside_tag()
            c.setPosition(left)
            c.setPosition(right, QTextCursor.MoveMode.KeepAnchor)
            href = prepare_string_for_xml(href, True)
            if fullpage:
                template =  '''\
<div style="page-break-before:always; page-break-after:always; page-break-inside:avoid">\
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" \
version="1.1" width="100%%" height="100%%" viewBox="0 0 {w} {h}" preserveAspectRatio="{a}">\
<image width="{w}" height="{h}" xlink:href="%s"/>\
</svg></div>'''.format(w=width, h=height, a='xMidYMid meet' if preserve_aspect_ratio else 'none')
            else:
                alt = _('Image')
                template = '<img alt="{0}" src="%s" />'.format(alt)
        text = template % href
        c.insertText(text)
        if self.syntax == 'html' and not fullpage:
            c.setPosition(left + 10)
            c.setPosition(c.position() + len(alt), QTextCursor.MoveMode.KeepAnchor)
        else:
            c.setPosition(left)
            c.setPosition(left + len(text), QTextCursor.MoveMode.KeepAnchor)
        self.setTextCursor(c)

    def insert_hyperlink(self, target, text, template=None):
        if hasattr(self.smarts, 'insert_hyperlink'):
            self.smarts.insert_hyperlink(self, target, text, template=template)

    def insert_tag(self, tag):
        if hasattr(self.smarts, 'insert_tag'):
            self.smarts.insert_tag(self, tag)

    def remove_tag(self):
        if hasattr(self.smarts, 'remove_tag'):
            self.smarts.remove_tag(self)

    def split_tag(self):
        if hasattr(self.smarts, 'split_tag'):
            self.smarts.split_tag(self)

    def keyPressEvent(self, ev):
        if ev.key() == Qt.Key.Key_X and ev.modifiers() == Qt.KeyboardModifier.AltModifier:
            if self.replace_possible_unicode_sequence():
                ev.accept()
                return
        if ev.key() == Qt.Key.Key_Insert:
            self.setOverwriteMode(self.overwriteMode() ^ True)
            ev.accept()
            return
        if self.snippet_manager.handle_key_press(ev):
            self.completion_popup.hide()
            return
        if self.smarts.handle_key_press(ev, self):
            self.handle_keypress_completion(ev)
            return
        QPlainTextEdit.keyPressEvent(self, ev)
        self.handle_keypress_completion(ev)

    def handle_keypress_completion(self, ev):
        if self.request_completion is None:
            return
        code = ev.key()
        if code in (
            0, Qt.Key.Key_unknown, Qt.Key.Key_Shift, Qt.Key.Key_Control, Qt.Key.Key_Alt,
            Qt.Key.Key_Meta, Qt.Key.Key_AltGr, Qt.Key.Key_CapsLock, Qt.Key.Key_NumLock,
            Qt.Key.Key_ScrollLock, Qt.Key.Key_Up, Qt.Key.Key_Down):
            # We ignore up/down arrow so as to not break scrolling through the
            # text with the arrow keys
            return
        result = self.smarts.get_completion_data(self, ev)
        if result is None:
            self.last_completion_request += 1
        else:
            self.last_completion_request = self.request_completion(*result)
        self.completion_popup.mark_completion(self, None if result is None else result[-1])

    def handle_completion_result(self, result):
        if result.request_id[0] >= self.last_completion_request:
            self.completion_popup.handle_result(result)

    def clear_completion_cache(self):
        if self.request_completion is not None and self.completion_doc_name:
            self.request_completion(None, 'file:' + self.completion_doc_name)

    def replace_possible_unicode_sequence(self):
        c = self.textCursor()
        has_selection = c.hasSelection()
        if has_selection:
            text = unicode_type(c.selectedText()).rstrip('\0')
        else:
            c.setPosition(c.position() - min(c.positionInBlock(), 6), QTextCursor.MoveMode.KeepAnchor)
            text = unicode_type(c.selectedText()).rstrip('\0')
        m = re.search(r'[a-fA-F0-9]{2,6}$', text)
        if m is None:
            return False
        text = m.group()
        try:
            num = int(text, 16)
        except ValueError:
            return False
        if num > 0x10ffff or num < 1:
            return False
        end_pos = max(c.anchor(), c.position())
        c.setPosition(end_pos - len(text)), c.setPosition(end_pos, QTextCursor.MoveMode.KeepAnchor)
        c.insertText(safe_chr(num))
        return True

    def select_all(self):
        c = self.textCursor()
        c.clearSelection()
        c.setPosition(0)
        c.movePosition(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.KeepAnchor)
        self.setTextCursor(c)

    def rename_block_tag(self, new_name):
        if hasattr(self.smarts, 'rename_block_tag'):
            self.smarts.rename_block_tag(self, new_name)

    def current_tag(self, for_position_sync=True, cursor=None):
        use_matched_tag = False
        if cursor is None:
            use_matched_tag = True
            cursor = self.textCursor()
        return self.smarts.cursor_position_with_sourceline(cursor, for_position_sync=for_position_sync, use_matched_tag=use_matched_tag)

    def goto_sourceline(self, sourceline, tags, attribute=None):
        return self.smarts.goto_sourceline(self, sourceline, tags, attribute=attribute)

    def get_tag_contents(self):
        c = self.smarts.get_inner_HTML(self)
        if c is not None:
            return self.selected_text_from_cursor(c)

    def goto_css_rule(self, rule_address, sourceline_address=None):
        from calibre.gui2.tweak_book.editor.smarts.css import find_rule
        block = None
        if self.syntax == 'css':
            raw = unicode_type(self.toPlainText())
            line, col = find_rule(raw, rule_address)
            if line is not None:
                block = self.document().findBlockByNumber(line - 1)
        elif sourceline_address is not None:
            sourceline, tags = sourceline_address
            if self.goto_sourceline(sourceline, tags):
                c = self.textCursor()
                c.setPosition(c.position() + 1)
                self.setTextCursor(c)
                raw = self.get_tag_contents()
                line, col = find_rule(raw, rule_address)
                if line is not None:
                    block = self.document().findBlockByNumber(c.blockNumber() + line - 1)

        if block is not None and block.isValid():
            c = self.textCursor()
            c.setPosition(block.position() + col)
            self.setTextCursor(c)

    def change_case(self, action, cursor=None):
        cursor = cursor or self.textCursor()
        text = self.selected_text_from_cursor(cursor)
        text = {'lower':lower, 'upper':upper, 'capitalize':capitalize, 'title':titlecase, 'swap':swapcase}[action](text)
        cursor.insertText(text)
        self.setTextCursor(cursor)
예제 #12
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 = {}
        self.gesture_manager = GestureManager(self)
        self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)

    def viewportEvent(self, ev):
        try:
            ret = self.gesture_manager.handle_event(ev)
        except AttributeError:
            ret = None
        if ret is not None:
            return ret
        return super().viewportEvent(ev)

    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)
예제 #13
0
 def family_setter(which, w, val):
     w.setCurrentFont(QFont(val or default_font(which)))
예제 #14
0
def layout_text(prefs, img, title, subtitle, footer, max_height, style):
    width = img.width() - 2 * style.hmargin
    title, subtitle, footer = title, subtitle, footer
    title_font = QFont(prefs.title_font_family or 'Liberation Serif')
    title_font.setPixelSize(prefs.title_font_size)
    title_font.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)
    title_block = Block(title, width, title_font, img, max_height, style.TITLE_ALIGN)
    title_block.position = style.hmargin, style.vmargin
    subtitle_block = Block()
    if subtitle:
        subtitle_font = QFont(prefs.subtitle_font_family or 'Liberation Sans')
        subtitle_font.setPixelSize(prefs.subtitle_font_size)
        subtitle_font.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)
        gap = 2 * title_block.leading
        mh = max_height - title_block.height - gap
        subtitle_block = Block(subtitle, width, subtitle_font, img, mh, style.SUBTITLE_ALIGN)
        subtitle_block.position = style.hmargin, title_block.position.y + title_block.height + gap

    footer_font = QFont(prefs.footer_font_family or 'Liberation Serif')
    footer_font.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)
    footer_font.setPixelSize(prefs.footer_font_size)
    footer_block = Block(footer, width, footer_font, img, max_height, style.FOOTER_ALIGN)
    footer_block.position = style.hmargin, img.height() - style.vmargin - footer_block.height

    return title_block, subtitle_block, footer_block
예제 #15
0
 def do_size_hint(self, option, index):
     text = index.data(Qt.ItemDataRole.DisplayRole) or ''
     font = QFont(option.font)
     font.setPointSize(QFontInfo(font).pointSize() * 1.5)
     m = QFontMetrics(font)
     return QSize(m.width(text), m.height())
예제 #16
0
 def clear_button(self, which):
     b = getattr(self, 'button%d' % which)
     s = getattr(self, 'shortcut%d' % which, None)
     b.setText(_('None') if s is None else s.toString(QKeySequence.SequenceFormat.NativeText))
     b.setFont(QFont())
예제 #17
0
 def mark_item_as_current(self, item):
     font = QFont(self.font())
     font.setItalic(True)
     font.setBold(True)
     item.setData(0, Qt.ItemDataRole.FontRole, font)
예제 #18
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)
        self.gesture_manager = GestureManager(self)
        self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)

    def viewportEvent(self, ev):
        try:
            ret = self.gesture_manager.handle_event(ev)
        except AttributeError:
            ret = None
        if ret is not None:
            return ret
        return super().viewportEvent(ev)

    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('modified.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)
        m.addSeparator()
        m.addAction(QIcon.ic('plus.png'), _('Expand all'), self.expandAll)
        m.addAction(QIcon.ic('minus.png'), _('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, preserve_state=False):
        s = self.style()
        expanded_chapters = set()
        if preserve_state:
            root = self.invisibleRootItem()
            for i in range(root.childCount()):
                chapter = root.child(i)
                if chapter.isExpanded():
                    expanded_chapters.add(chapter.data(0, Qt.ItemDataRole.DisplayRole))
        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(not preserve_state or sec in expanded_chapters)
            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):
        def_idx = 999999999999999
        defval = def_idx, cfi_sort_key('/99999999')

        def cfi_key(h):
            cfi = h.get('start_cfi')
            si = h.get('spine_index', def_idx)
            return (si, 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, preserve_state=True)
        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)
예제 #19
0
    def font(self, text_style):
        device_font = text_style.fontfacename in LIBERATION_FONT_MAP
        try:
            if device_font:
                face = self.font_map[text_style.fontfacename]
            else:
                face = self.face_map[text_style.fontfacename]
        except KeyError:  # Bad fontfacename field in LRF
            face = self.font_map['Dutch801 Rm BT Roman']

        sz = text_style.fontsize
        wt = text_style.fontweight
        style = text_style.fontstyle
        font = (
            face,
            wt,
            style,
            sz,
        )
        if font in self.cache:
            rfont = self.cache[font]
        else:
            italic = font[2] == QFont.Style.StyleItalic
            rfont = QFont(font[0], font[3], font[1], italic)
            rfont.setPixelSize(font[3])
            rfont.setBold(wt >= 69)
            self.cache[font] = rfont
        qfont = rfont
        if text_style.emplinetype != 'none':
            qfont = QFont(rfont)
            qfont.setOverline(text_style.emplineposition == 'before')
            qfont.setUnderline(text_style.emplineposition == 'after')
        return qfont
예제 #20
0
 def set_subtitle_font(self, for_ratings=True):
     if for_ratings:
         self.setSubtitleFont(QFont(rating_font()))
     else:
         self.setSubtitleFont(self.font())
예제 #21
0
 def build_font_obj(self):
     font_info = qt_app.original_font if self.current_font is None else self.current_font
     font = QFont(*(font_info[:4]))
     font.setStretch(font_info[4])
     return font
예제 #22
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(
            QAbstractItemView.SelectionMode.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)
            section.setData(0, Qt.ItemDataRole.UserRole, book_id)
            self.addTopLevelItem(section)
            section.setExpanded(True)
            for result in sorted_items(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
        if ev.key() == Qt.Key.Key_F2:
            item = self.currentItem()
            if item:
                self.edit_notes(item)
                ev.accept()
                return
        return QTreeWidget.keyPressEvent(self, ev)

    @property
    def tree_state(self):
        ans = {'closed': set()}
        item = self.currentItem()
        if item is not None:
            ans['current'] = item.data(0, Qt.ItemDataRole.UserRole)
        for item in (self.topLevelItem(i)
                     for i in range(self.topLevelItemCount())):
            if not item.isExpanded():
                ans['closed'].add(item.data(0, Qt.ItemDataRole.UserRole))
        return ans

    @tree_state.setter
    def tree_state(self, state):
        closed = state['closed']
        for item in (self.topLevelItem(i)
                     for i in range(self.topLevelItemCount())):
            if item.data(0, Qt.ItemDataRole.UserRole) in closed:
                item.setExpanded(False)

        cur = state.get('current')
        if cur is not None:
            for item in self.item_map:
                if item.data(0, Qt.ItemDataRole.UserRole) == cur:
                    self.setCurrentItem(item)
                    break