class BlockingBusy(QDialog): def __init__(self, msg, parent=None, window_title=_('Working')): QDialog.__init__(self, parent) self._layout = QVBoxLayout() self.setLayout(self._layout) self.msg = QLabel(msg) # self.msg.setWordWrap(True) self.font = QFont() self.font.setPointSize(self.font.pointSize() + 8) self.msg.setFont(self.font) self.pi = ProgressIndicator(self) self.pi.setDisplaySize(QSize(100, 100)) self._layout.addWidget(self.pi, 0, Qt.AlignmentFlag.AlignHCenter) self._layout.addSpacing(15) self._layout.addWidget(self.msg, 0, Qt.AlignmentFlag.AlignHCenter) self.start() self.setWindowTitle(window_title) self.resize(self.sizeHint()) def start(self): self.pi.startAnimation() def stop(self): self.pi.stopAnimation() def accept(self): self.stop() return QDialog.accept(self) def reject(self): pass # Cannot cancel this dialog
def do_paint(self, painter, option, index): text = str(index.data(Qt.ItemDataRole.DisplayRole) or '') font = QFont(option.font) font.setPointSize(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 if option.state & QStyle.StateFlag.State_Selected: painter.setPen(QPen(option.palette.highlightedText(), 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)
def get_current_font(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) else: font = QFont(font_name, pointSize=size) return font
def set_up_font_boxes(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) else: font = QFont(font_name, pointSize=size) self.font_box.setWritingSystem(QFontDatabase.Latin) self.font_box.setCurrentFont(font) self.font_box.setEditable(False) gprefs['gpm_template_editor_font'] = unicode_type(font.family()) self.font_size_box.setValue(size) self.font_box.currentFontChanged.connect(self.font_changed) self.font_size_box.valueChanged.connect(self.font_size_changed) self.highlighter.initializeFormats() self.highlighter.rehighlight()
def __init__(self, parent, icon_name, title): QHBoxLayout.__init__(self) title_font = QFont() title_font.setPointSize(16) title_image_label = QLabel(parent) pixmap = QPixmap() pixmap.load(I(icon_name)) if pixmap is None: error_dialog(parent, _('Restart required'), _('You must restart calibre before using this plugin!'), show=True) else: title_image_label.setPixmap(pixmap) title_image_label.setMaximumSize(32, 32) title_image_label.setScaledContents(True) self.addWidget(title_image_label) shelf_label = QLabel(title, parent) shelf_label.setFont(title_font) self.addWidget(shelf_label) self.insertStretch(-1)
class RatingDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ def __init__(self, *args, **kwargs): QStyledItemDelegate.__init__(self, *args) self.is_half_star = kwargs.get('is_half_star', False) self.table_widget = args[0] self.rf = QFont(rating_font()) self.em = Qt.TextElideMode.ElideMiddle delta = 0 if iswindows and sys.getwindowsversion().major >= 6: delta = 2 self.rf.setPointSize( QFontInfo(QApplication.font()).pointSize() + delta) def get_required_width(self, editor, style, fm): return editor.sizeHint().width() def displayText(self, value, locale): return rating_to_stars(value, self.is_half_star) def createEditor(self, parent, option, index): return RatingEditor(parent, is_half_star=self.is_half_star) def setEditorData(self, editor, index): if check_key_modifier(Qt.KeyboardModifier.ControlModifier): val = 0 else: val = index.data(Qt.ItemDataRole.EditRole) editor.rating_value = val def setModelData(self, editor, model, index): val = editor.rating_value model.setData(index, val, Qt.ItemDataRole.EditRole) def sizeHint(self, option, index): option.font = self.rf option.textElideMode = self.em return QStyledItemDelegate.sizeHint(self, option, index) def paint(self, painter, option, index): option.font = self.rf option.textElideMode = self.em return QStyledItemDelegate.paint(self, painter, option, index)
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_
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)
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())