def in_image(): c = QTextCursor(doc.text) c.movePosition(QTextCursor.Start) while c.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor): if doc.in_image(): break
def __init__(self, config): super(Doc, self).__init__() self._text = QTextDocument() self._cfg = config self._text_edit_cursor = QTextCursor(self._text) self._text.setIndentWidth(self._cfg.get("TextEditor/IndentWidth", 24)) self.set_default_font()
def in_table(): c = QTextCursor(doc.text) c.movePosition(QTextCursor.Start) while c.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor): if doc.in_table(): cc = doc.table().cellAt(2, 1) cc.firstCursorPosition().insertText("Hello") doc.change(cc.firstCursorPosition())
def in_table(): c = QTextCursor(doc.text) c.movePosition(QTextCursor.Start) while c.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor): if doc.in_table(): doc.text_align(Qt.AlignRight, QTextCharFormat.AlignBottom) a = doc._text_edit_cursor.charFormat().verticalAlignment() self.assertEqual(a, QTextCharFormat.AlignBottom) break
def in_table(): # is exists table in document ? c = QTextCursor(doc.text) c.movePosition(QTextCursor.Start) _in_table = False while c.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor): if doc.in_table(): _in_table = True break return _in_table
def createDocument(self, rootFrame): # Create empty document self.document = QTextDocument() self.document.setUndoRedoEnabled(False) self.document.setIndentWidth(20) # Register a renderer for custom text objects mo = CustomObjectRenderer() mo.setParent(self.document) self.document.documentLayout().registerHandler(QTextCharFormat.UserObject+1, mo); self.cursor = QTextCursor(self.document) self.listLevel = 0 self.paraFormat = None # add all root paragraphs for n in rootFrame.children: self.addNode(n) # Clean up the first paragraph if document is not empty self.cursor.movePosition(QTextCursor.Start) b = self.cursor.block() if b.length() == 1: cursor = QTextCursor(self.document.findBlockByLineNumber(0)) cursor.select(QTextCursor.BlockUnderCursor) cursor.deleteChar() return self.document
def do_test(self): selections = [] self.match_locs = [] if self.regex_valid(): text = unicode_type(self.preview.toPlainText()) regex = unicode_type(self.regex.text()) cursor = QTextCursor(self.preview.document()) extsel = QTextEdit.ExtraSelection() extsel.cursor = cursor extsel.format.setBackground(QBrush(Qt.yellow)) try: for match in compile_regular_expression(regex).finditer(text): es = QTextEdit.ExtraSelection(extsel) es.cursor.setPosition(match.start(), QTextCursor.MoveAnchor) es.cursor.setPosition(match.end(), QTextCursor.KeepAnchor) selections.append(es) self.match_locs.append((match.start(), match.end())) except: pass self.preview.setExtraSelections(selections) if self.match_locs: self.next.setEnabled(True) self.previous.setEnabled(True) self.occurrences.setText(unicode_type(len(self.match_locs)))
def reformat_blocks(self, position, removed, added): doc = self.doc if doc is None or self.ignore_requests or not hasattr(self, 'state_map'): return block = doc.findBlock(position) if not block.isValid(): return start_cursor = QTextCursor(block) last_block = doc.findBlock(position + added + (1 if removed > 0 else 0)) if not last_block.isValid(): last_block = doc.lastBlock() end_cursor = QTextCursor(last_block) end_cursor.movePosition(end_cursor.EndOfBlock) self.requests.append((start_cursor, end_cursor)) QTimer.singleShot(0, self.do_one_block)
def in_table(): # is exists table in document ? c = QTextCursor(doc.text) c.movePosition(QTextCursor.Start) _in_table = False while c.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor): if doc.in_table(): _in_table = True doc.background_color("blue") cc = doc._text_edit_cursor cell = cc.currentTable().cellAt(cc).format() clr = cell.background().color().name() self.assertEqual(clr, QColor("blue").name()) break return _in_table
def set_document(self, doc, doc_name=None): old_doc = self.doc if old_doc is not None: old_doc.contentsChange.disconnect(self.reformat_blocks) c = QTextCursor(old_doc) c.beginEditBlock() blk = old_doc.begin() while blk.isValid(): blk.layout().clearAdditionalFormats() blk = blk.next() c.endEditBlock() self.doc = self.doc_name = None if doc is not None: self.doc = doc self.doc_name = doc_name doc.contentsChange.connect(self.reformat_blocks) self.rehighlight()
def copy_lines(self, lo, hi, cursor): ''' Copy specified lines from the syntax highlighted buffer into the destination cursor, preserving all formatting created by the syntax highlighter. ''' self.highlighter.join() num = hi - lo if num > 0: block = self.findBlockByNumber(lo) while num > 0: num -= 1 cursor.insertText(block.text()) dest_block = cursor.block() c = QTextCursor(dest_block) try: afs = block.layout().additionalFormats() except AttributeError: afs = () for af in afs: start = dest_block.position() + af.start c.setPosition(start), c.setPosition( start + af.length, c.KeepAnchor) c.setCharFormat(af.format) cursor.insertBlock() cursor.setCharFormat(NULL_FMT) block = next(block)
def search_log_area(self): """ Search a word in log area. Keep last cursor and word used for iteration in the document. """ cursor: QTextCursor doc: QTextDocument text: str search_format: QTextCharFormat text = self.log_filters.filter_line.text() doc = self.log_area.document() search_format = QTextCharFormat() color = QColor("yellow") color.setAlpha(255) search_format.setBackground(color) # remove the previous search highlight if not self.last_search_cursor.isNull(): self.last_search_cursor.setCharFormat(QTextCharFormat()) # if search word changed or erased, reset cursor and last_search if len(text) == 0 or text != self.last_search: self.last_search_cursor = QTextCursor() self.last_search = text self.log_area.unsetCursor() # if has no word to search, just return if len(text) == 0: return cursor = self.last_search_cursor if not self.last_search_cursor.isNull( ) else QTextCursor(doc) cursor = doc.find(text, cursor) if not cursor.isNull(): self.log_area.setTextCursor(cursor) cursor.setCharFormat(search_format) else: self.log_area.unsetCursor() self.last_search_cursor = cursor
def __init__(self): super(MainWindow, self).__init__() self.log_area = None # :type QPlainTextEdit self.main_document = None # :type QTextDocument self.highlight_options_dialog = None # :type HighlightOptionsDialog self.serial_dialog = None # :type SerialDialog self.serial_menu = None # :type QMenu self.highlight_options_menu = None # :type QMenu self.help_menu = None # :type QMenu self.log_filter_widget = None # :type QWidget self.log_filters = None # :type FilterPanel self.last_search = str() self.last_search_cursor = QTextCursor() container = QWidget() self.setup_menu() self.setup_text_area() self.setup_footer_panel() self.setup_serial_dialog() self.setup_highlight_options_dialog() self.first_search_flag = False self.serial_worker = SerialListenerWorker() self.serial_worker.signal.connect(self.add_line_to_log_area) self.log_area_layout = QVBoxLayout() self.log_area_layout.setContentsMargins(0, 0, 0, 0) self.log_area_layout.addWidget(self.log_area) self.log_area_layout.addWidget(self.log_filter_widget) container.setLayout(self.log_area_layout) self.setCentralWidget(container) self.setWindowTitle(APP_NAME) self.setGeometry(QRect(100, 100, 800, 600))
def goto_loc(self, loc, operation=QTextCursor.Right, mode=QTextCursor.KeepAnchor, n=0): cursor = QTextCursor(self.preview.document()) cursor.setPosition(loc) if n: cursor.movePosition(operation, mode, n) self.preview.setTextCursor(cursor)
def in_table(): c = QTextCursor(doc.text) c.movePosition(QTextCursor.Start) while c.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor): if doc.in_table(): cc = doc.table().cellAt(2, 2) cc.firstCursorPosition().insertText("Hello1") cc = doc.table().cellAt(2, 1) cc.firstCursorPosition().insertText("Hello") c.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) break
def copy_lines(self, lo, hi, cursor): ''' Copy specified lines from the syntax highlighted buffer into the destination cursor, preserving all formatting created by the syntax highlighter. ''' num = hi - lo if num > 0: block = self.findBlockByNumber(lo) while num > 0: num -= 1 cursor.insertText(block.text()) dest_block = cursor.block() c = QTextCursor(dest_block) for af in block.layout().additionalFormats(): start = dest_block.position() + af.start c.setPosition(start), c.setPosition(start + af.length, c.KeepAnchor) c.setCharFormat(af.format) cursor.insertBlock() cursor.setCharFormat(NULL_FMT) block = block.next()
def find_text(self, pat, cursor): from calibre.gui2.tweak_book.text_search import find_text_in_chunks chunks = [] cstart = min(cursor.position(), cursor.anchor()) cend = max(cursor.position(), cursor.anchor()) c = QTextCursor(cursor) c.setPosition(cstart) block = c.block() in_text = find_tag_definition(block, 0)[0] is None def append(text, start): after = start + len(text) if start <= cend and cstart < after: extra = after - (cend + 1) if extra > 0: text = text[:-extra] extra = cstart - start if extra > 0: text = text[extra:] chunks.append((text, start + max(extra, 0))) while block.isValid() and block.position() <= cend: boundaries = sorted(block.userData().tags, key=get_offset) if not boundaries: # Add the whole line if in_text: text = block.text() if text: append(text, block.position()) else: start = block.position() c.setPosition(start) for b in boundaries: if in_text: c.setPosition(start + b.offset, c.KeepAnchor) if c.hasSelection(): append(c.selectedText(), c.anchor()) in_text = not b.is_start c.setPosition(start + b.offset + 1) if in_text: # Add remaining text in block c.setPosition(block.position() + boundaries[-1].offset + 1) c.movePosition(c.EndOfBlock, c.KeepAnchor) if c.hasSelection(): append(c.selectedText(), c.anchor()) block = block.next() s, e = find_text_in_chunks(pat, chunks) return s != -1 and e != -1, s, e
def resized(self): ' Resize images to fit in new view size and adjust all line number references accordingly ' for v in (self.left, self.right): changes = [] for i, (top, bot, kind) in enumerate(v.changes): if top in v.images: img, oldw, oldlines = v.images[top] lines, w = self.get_lines_for_image(img, v) if lines != oldlines: changes.append((i, lines, lines - oldlines, img, w)) for i, lines, delta, img, w in changes: top, bot, kind = v.changes[i] c = QTextCursor(v.document().findBlockByNumber(top+1)) c.beginEditBlock() c.movePosition(c.StartOfBlock) if delta > 0: for _ in xrange(delta): c.insertBlock() else: c.movePosition(c.NextBlock, c.KeepAnchor, -delta) c.removeSelectedText() c.endEditBlock() v.images[top] = (img, w, lines) def mapnum(x): return x if x <= top else x + delta lnm = LineNumberMap() lnm.max_width = v.line_number_map.max_width for x, val in v.line_number_map.iteritems(): dict.__setitem__(lnm, mapnum(x), val) v.line_number_map = lnm v.changes = [(mapnum(t), mapnum(b), k) for t, b, k in v.changes] v.headers = [(mapnum(x), name) for x, name in v.headers] v.images = OrderedDict((mapnum(x), v) for x, v in v.images.iteritems()) v.viewport().update()
def search(self, query, reverse=False): ''' Search for query, also searching the headers. Matches in headers are not highlighted as managing the highlight is too much of a pain.''' if not query.strip(): return c = self.textCursor() lnum = c.block().blockNumber() cpos = c.positionInBlock() headers = dict(self.headers) if lnum in headers: cpos = self.search_header_pos lines = unicode(self.toPlainText()).splitlines() for hn, text in self.headers: lines[hn] = text prefix, postfix = lines[lnum][:cpos], lines[lnum][cpos:] before, after = enumerate(lines[0:lnum]), ((lnum+1+i, x) for i, x in enumerate(lines[lnum+1:])) if reverse: sl = chain([(lnum, prefix)], reversed(tuple(before)), reversed(tuple(after)), [(lnum, postfix)]) else: sl = chain([(lnum, postfix)], after, before, [(lnum, prefix)]) flags = regex.REVERSE if reverse else 0 pat = regex.compile(regex.escape(query, special_only=True), flags=regex.UNICODE|regex.IGNORECASE|flags) for num, text in sl: try: m = next(pat.finditer(text)) except StopIteration: continue start, end = m.span() length = end - start if text is postfix: start += cpos c = QTextCursor(self.document().findBlockByNumber(num)) c.setPosition(c.position() + start) if num in headers: self.search_header_pos = start + length else: c.setPosition(c.position() + length, c.KeepAnchor) self.search_header_pos = 0 if reverse: pos, anchor = c.position(), c.anchor() c.setPosition(pos), c.setPosition(anchor, c.KeepAnchor) self.setTextCursor(c) self.centerCursor() self.scrolled.emit() break else: info_dialog(self, _('No matches found'), _( 'No matches found for query: %s' % query), show=True)
class MainWindow(QMainWindow): """ Create the main Create the main window and setup all widgets needed. """ log_area: QPlainTextEdit main_document: QTextDocument serial_dialog: SerialDialog serial_menu: QMenu help_menu: QMenu log_filter_widget: QWidget log_filters: FilterPanel def __init__(self): super(MainWindow, self).__init__() self.log_area = None # :type QPlainTextEdit self.main_document = None # :type QTextDocument self.highlight_options_dialog = None # :type HighlightOptionsDialog self.serial_dialog = None # :type SerialDialog self.serial_menu = None # :type QMenu self.highlight_options_menu = None # :type QMenu self.help_menu = None # :type QMenu self.log_filter_widget = None # :type QWidget self.log_filters = None # :type FilterPanel self.last_search = str() self.last_search_cursor = QTextCursor() container = QWidget() self.setup_menu() self.setup_text_area() self.setup_footer_panel() self.setup_serial_dialog() self.setup_highlight_options_dialog() self.first_search_flag = False self.serial_worker = SerialListenerWorker() self.serial_worker.signal.connect(self.add_line_to_log_area) self.log_area_layout = QVBoxLayout() self.log_area_layout.setContentsMargins(0, 0, 0, 0) self.log_area_layout.addWidget(self.log_area) self.log_area_layout.addWidget(self.log_filter_widget) container.setLayout(self.log_area_layout) self.setCentralWidget(container) self.setWindowTitle(APP_NAME) self.setGeometry(QRect(100, 100, 800, 600)) def closeEvent(self, event): self.serial_dialog.serial_handler.close() del self.serial_worker def setup_serial_dialog(self): """Setup the Serial Dialog interface""" self.serial_dialog = SerialDialog(self) def setup_highlight_options_dialog(self): """Setup the highlighting options dialog interface""" self.highlight_options_dialog = HighlightOptionsDialog(self) def setup_menu(self): """Setup the menu""" self.serial_menu = self.menuBar().addMenu("Serial") self.highlight_options_menu = self.menuBar().addMenu( "Highlight Options") self.help_menu = self.menuBar().addMenu("Help") action_serial_setup = QAction("Setup", self) action_serial_setup.triggered.connect(self.serial_setup) self.serial_menu.addAction(action_serial_setup) action_highlight_options = QAction("Highlight Options", self) action_highlight_options.triggered.connect( self.highlight_options_setup) self.highlight_options_menu.addAction(action_highlight_options) action_about_box = QAction("About", self) action_about_box.triggered.connect(self.about_box_show) self.help_menu.addAction(action_about_box) def setup_text_area(self): """Setup the log text area and the highlighter class""" self.log_area = QPlainTextEdit() self.log_area.setReadOnly(True) self.log_area.centerOnScroll() self.main_document = self.log_area.document() HighlighterTag(self.log_area.document()) def setup_footer_panel(self): """Setup the footer area""" self.log_filter_widget = QWidget() self.log_filters = FilterPanel(self.log_filter_widget) self.log_filters.filter_changed.connect(self.filter_document) self.log_filters.filter_clear_button.clicked.connect( self.clear_log_area) self.log_filters.filter_search_button.clicked.connect( self.search_log_area) @pyqtSlot(str) def filter_document(self, msg: str): """Filter the log area with the selected tag. The log area will show only the lines with the selected tag or untagged lines. :type msg: str (ALL|DEBUG|INFO|ERROR|CRITICAL|FATAL) """ tag = find_tag_by_name(msg) log_filter_by_tag(self.main_document, tag) self.update() def highlight_options_setup(self): apply = self.highlight_options_dialog.exec() def serial_setup(self): """ Execute the serial dialog interface. If the the result (:type dialog_result : int)== 0 and the connection is successfully done, start the serial_worker thread If the the result (:type dialog_result : int) == 1, disconnection was requested, the worker is closed and the serial connection are closed """ dialog_result = self.serial_dialog.exec() logger.debug("SerialDialog result : " + str(dialog_result)) if dialog_result == 0: if self.serial_dialog.serial_handler.isOpen(): self.serial_worker.serial_handler = self.serial_dialog.serial_handler self.serial_worker.start() elif dialog_result == 1: self.serial_worker.terminate() self.serial_dialog.serial_handler.close() def about_box_show(self): about_msg_box = QMessageBox(self) about_msg_box.setGeometry(QRect(100, 100, 240, 360)) about_msg_box.addButton(QPushButton("OK"), QMessageBox.YesRole) about_msg_box.setWindowTitle(APP_NAME) about_msg_box.setText("{0}\n" "Verion : {1}\n" "Author : {2}\n" "Email : {3}".format(APP_NAME, APP_VERSION, APP_AUTHOR, APP_AUTHOR_EMAIL)) about_msg_box.exec() @pyqtSlot(str) def add_line_to_log_area(self, log): """ Add a string to the log area :param log: str """ self.log_area.appendPlainText(log) def clear_log_area(self): """ clear the log area """ self.log_area.clear() def search_log_area(self): """ Search a word in log area. Keep last cursor and word used for iteration in the document. """ cursor: QTextCursor doc: QTextDocument text: str search_format: QTextCharFormat text = self.log_filters.filter_line.text() doc = self.log_area.document() search_format = QTextCharFormat() color = QColor("yellow") color.setAlpha(255) search_format.setBackground(color) # remove the previous search highlight if not self.last_search_cursor.isNull(): self.last_search_cursor.setCharFormat(QTextCharFormat()) # if search word changed or erased, reset cursor and last_search if len(text) == 0 or text != self.last_search: self.last_search_cursor = QTextCursor() self.last_search = text self.log_area.unsetCursor() # if has no word to search, just return if len(text) == 0: return cursor = self.last_search_cursor if not self.last_search_cursor.isNull( ) else QTextCursor(doc) cursor = doc.find(text, cursor) if not cursor.isNull(): self.log_area.setTextCursor(cursor) cursor.setCharFormat(search_format) else: self.log_area.unsetCursor() self.last_search_cursor = cursor
def show_context_menu(self, pos): m = QMenu(self) a = m.addAction c = self.editor.cursorForPosition(pos) origc = QTextCursor(c) current_cursor = self.editor.textCursor() r = origr = self.editor.syntax_range_for_cursor(c) if (r is None or not r.format.property(SPELL_PROPERTY)) and c.positionInBlock() > 0 and not current_cursor.hasSelection(): c.setPosition(c.position() - 1) r = self.editor.syntax_range_for_cursor(c) if r is not None and r.format.property(SPELL_PROPERTY): word = self.editor.text_for_range(c.block(), r) locale = self.editor.spellcheck_locale_for_cursor(c) orig_pos = c.position() c.setPosition(orig_pos - utf16_length(word)) found = False self.editor.setTextCursor(c) if self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False): found = True fc = self.editor.textCursor() if fc.position() < c.position(): self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False) spell_cursor = self.editor.textCursor() if current_cursor.hasSelection(): # Restore the current cursor so that any selection is preserved # for the change case actions self.editor.setTextCursor(current_cursor) if found: suggestions = dictionaries.suggestions(word, locale)[:7] if suggestions: for suggestion in suggestions: ac = m.addAction(suggestion, partial(self.editor.simple_replace, suggestion, cursor=spell_cursor)) f = ac.font() f.setBold(True), ac.setFont(f) m.addSeparator() m.addAction(actions['spell-next']) m.addAction(_('Ignore this word'), partial(self._nuke_word, None, word, locale)) dics = dictionaries.active_user_dictionaries if len(dics) > 0: if len(dics) == 1: m.addAction(_('Add this word to the dictionary: {0}').format(dics[0].name), partial( self._nuke_word, dics[0].name, word, locale)) else: ac = m.addAction(_('Add this word to the dictionary')) dmenu = QMenu(m) ac.setMenu(dmenu) for dic in dics: dmenu.addAction(dic.name, partial(self._nuke_word, dic.name, word, locale)) m.addSeparator() if origr is not None and origr.format.property(LINK_PROPERTY): href = self.editor.text_for_range(origc.block(), origr) m.addAction(_('Open %s') % href, partial(self.link_clicked.emit, href)) if origr is not None and (origr.format.property(TAG_NAME_PROPERTY) or origr.format.property(CSS_PROPERTY)): word = self.editor.text_for_range(origc.block(), origr) item_type = 'tag_name' if origr.format.property(TAG_NAME_PROPERTY) else 'css_property' url = help_url(word, item_type, self.editor.highlighter.doc_name, extra_data=current_container().opf_version) if url is not None: m.addAction(_('Show help for: %s') % word, partial(open_url, url)) for x in ('undo', 'redo'): ac = actions['editor-%s' % x] if ac.isEnabled(): a(ac) m.addSeparator() for x in ('cut', 'copy', 'paste'): ac = actions['editor-' + x] if ac.isEnabled(): a(ac) m.addSeparator() m.addAction(_('&Select all'), self.editor.select_all) if self.selected_text or self.has_marked_text: update_mark_text_action(self) m.addAction(actions['mark-selected-text']) if self.syntax != 'css' and actions['editor-cut'].isEnabled(): cm = QMenu(_('Change &case'), m) for ac in 'upper lower swap title capitalize'.split(): cm.addAction(actions['transform-case-' + ac]) m.addMenu(cm) if self.syntax == 'html': m.addAction(actions['multisplit']) m.exec_(self.editor.viewport().mapToGlobal(pos))
def find_text(self, pat, cursor): from calibre.gui2.tweak_book.text_search import find_text_in_chunks chunks = [] cstart = min(cursor.position(), cursor.anchor()) cend = max(cursor.position(), cursor.anchor()) c = QTextCursor(cursor) c.setPosition(cstart) block = c.block() in_text = find_tag_definition(block, 0)[0] is None if in_text: # Check if we are in comment/PI/etc. pb = block.previous() while pb.isValid(): boundaries = pb.userData().non_tag_structures if boundaries: if boundaries[-1].is_start: in_text = False break pb = pb.previous() def append(text, start): text = text.replace(PARAGRAPH_SEPARATOR, '\n') after = start + len(text) if start <= cend and cstart < after: extra = after - (cend + 1) if extra > 0: text = text[:-extra] extra = cstart - start if extra > 0: text = text[extra:] chunks.append((text, start + max(extra, 0))) while block.isValid() and block.position() <= cend: ud = block.userData() boundaries = sorted(chain(ud.tags, ud.non_tag_structures), key=get_offset) if not boundaries: # Add the whole line if in_text: text = block.text() + '\n' append(text, block.position()) else: start = block.position() c.setPosition(start) for b in boundaries: if in_text: c.setPosition(start + b.offset, c.KeepAnchor) if c.hasSelection(): append(c.selectedText(), c.anchor()) in_text = not b.is_start c.setPosition(start + b.offset + 1) if in_text: # Add remaining text in block c.setPosition(block.position() + boundaries[-1].offset + 1) c.movePosition(c.EndOfBlock, c.KeepAnchor) if c.hasSelection(): append(c.selectedText() + '\n', c.anchor()) block = block.next() s, e = find_text_in_chunks(pat, chunks) return s != -1 and e != -1, s, e
class Doc(QObject): changed_status = pyqtSignal(dict) # for update the status bar changed_bold = pyqtSignal(bool) # for update button "Bold" enabled_save = pyqtSignal(bool) # for update button "Save" # ------------------------------------------------------------------------- def __init__(self, config): super(Doc, self).__init__() self._text = QTextDocument() self._cfg = config self._text_edit_cursor = QTextCursor(self._text) self._text.setIndentWidth(self._cfg.get("TextEditor/IndentWidth", 24)) self.set_default_font() # ------------------------------------------------------------------------- @property def text(self): return self._text # ------------------------------------------------------------------------- def is_modified(self): return self._text.isModified() # ------------------------------------------------------------------------- def in_table(self): return bool(self._text_edit_cursor.currentTable()) # ------------------------------------------------------------------------- def set_default_font(self, set_modified=False): font_name = self._cfg.get("TextEditor/Font", "Mono", system=True) sz = self._cfg.get("TextEditor/FontSize", 10, system=True) font = QFont(font_name, sz) self._text.setDefaultFont(font) if set_modified: self._text.setModified(True) self.change(self._text_edit_cursor) # ------------------------------------------------------------------------- def change(self, text_cursor: QTextCursor = None): """ Called (from QTextEdit event) with changed text or position of cursor """ if text_cursor: self._text_edit_cursor = text_cursor # refresh data on statusbar y = self._text_edit_cursor.blockNumber() + 1 cnt = self._text.lineCount() x = self._text_edit_cursor.columnNumber() + 1 chg = (self.tr("The document is not saved") if self._text.isModified() else "") xy = f"{y} : {x} [{cnt}]" self.changed_status.emit({"left": chg, "right": xy}) self.enabled_save.emit(self.is_modified()) # ------------------------------------------------------------------------- def bold(self): """ Set/unset bold font """ fmt = self._text_edit_cursor.charFormat() typ = QFont.Normal if fmt.fontWeight() == QFont.Bold else QFont.Bold fmt.setFontWeight(typ) self._text_edit_cursor.setCharFormat(fmt) self.change(self._text_edit_cursor) self.changed_bold.emit(typ == QFont.Bold) # ------------------------------------------------------------------------- def color(self, color): fmt = self._text_edit_cursor.charFormat() fmt.setForeground(QColor(color)) self._text_edit_cursor.setCharFormat(fmt) # ------------------------------------------------------------------------- def background_color(self, color): fmt = self._text_edit_cursor.charFormat() if self.in_table(): table = self._text_edit_cursor.currentTable() cell = table.cellAt(self._text_edit_cursor) cell_format = cell.format() cell_format.setBackground(QColor(color)) cell.setFormat(cell_format) else: fmt.setBackground(QColor(color)) self._text_edit_cursor.setCharFormat(fmt) # ------------------------------------------------------------------------- def font(self, font): fmt = self._text_edit_cursor.charFormat() fmt.setFont(font) self._text_edit_cursor.setCharFormat(fmt) # ------------------------------------------------------------------------- def font_size(self, font_size): fmt = self._text_edit_cursor.charFormat() fmt.setFontPointSize(font_size) self._text_edit_cursor.setCharFormat(fmt) # ------------------------------------------------------------------------- def hline(self): """ Insert horizontal line """ # Tag HR is not correctly displayed in QTextview cur_char_fmt = self._text_edit_cursor.charFormat() cur_block_fmt = self._text_edit_cursor.blockFormat() if bool(self._text_edit_cursor.currentTable()): self._text_edit_cursor.insertBlock(cur_block_fmt, cur_char_fmt) block_fmt = QTextBlockFormat() block_fmt.setTopMargin(5) block_fmt.setBottomMargin(5) block_fmt.setAlignment(Qt.AlignLeft) block_fmt.setBackground(QBrush(QColor("#C1C1C1"))) char_format = QTextCharFormat() char_format.setFont(QFont("Arial", 1)) self._text_edit_cursor.insertBlock(block_fmt, char_format) self._text_edit_cursor.insertText(" ") self._text_edit_cursor.insertBlock(cur_block_fmt, cur_char_fmt) self.change(self._text_edit_cursor) # ------------------------------------------------------------------------- def show_hide_hidden_char(self, show: bool): if show: # show hidden char option = self._text.defaultTextOption() option.setFlags( option.flags() | QTextOption.ShowTabsAndSpaces | QTextOption.ShowLineAndParagraphSeparators | QTextOption.AddSpaceForLineAndParagraphSeparators) self._text.setDefaultTextOption(option) else: # remove hidden char option = self._text.defaultTextOption() stas = ~QTextOption.ShowTabsAndSpaces slaps = ~QTextOption.ShowLineAndParagraphSeparators asflaps = ~QTextOption.AddSpaceForLineAndParagraphSeparators option.setFlags(option.flags() & stas & slaps & asflaps) self._text.setDefaultTextOption(option) # ------------------------------------------------------------------------- def blist(self): self._text_edit_cursor.insertList(QTextListFormat.ListDisc) # ------------------------------------------------------------------------- def nlist(self): self._text_edit_cursor.insertList(QTextListFormat.ListDecimal) # ------------------------------------------------------------------------- def copy_format(self, fmt): self._text_edit_cursor.setCharFormat(fmt) # ------------------------------------------------------------------------- def clear_format(self): txt = self._text.toPlainText() self._text_edit_cursor.beginEditBlock() # ---- begin ------- self._text_edit_cursor.select(QTextCursor.Document) self._text_edit_cursor.removeSelectedText() fmt = QTextBlockFormat() self._text_edit_cursor.setBlockFormat(fmt) self._text_edit_cursor.insertText(txt) self._text_edit_cursor.endEditBlock() # ---- end --------- # ------------------------------------------------------------------------- def text_align(self, horiz, vert): fmt = QTextBlockFormat() fmt.setAlignment(horiz) self._text_edit_cursor.mergeBlockFormat(fmt) if self.in_table(): table = self._text_edit_cursor.currentTable() cell = table.cellAt(self._text_edit_cursor) cell_format = cell.format() cell_format.setVerticalAlignment(vert) cell.setFormat(cell_format) # ------------------------------------------------------------------------- def insert_table(self, table_params): fmt = QTextTableFormat() fmt.setCellPadding(table_params["padding"]) fmt.setCellSpacing(table_params["space"]) return self._text_edit_cursor.insertTable(table_params["rows"], table_params["cols"], fmt) # ------------------------------------------------------------------------- def table(self): """ Current table if cursor in table """ return self._text_edit_cursor.currentTable() # ------------------------------------------------------------------------- def cell(self): """ Cell in current table """ return self.table().cellAt(self._text_edit_cursor) # ------------------------------------------------------------------------- def add_row(self): self.table().appendRows(1) # ------------------------------------------------------------------------- def add_col(self): self.table().appendColumns(1) # ------------------------------------------------------------------------- def del_row(self): self.table().removeRows(self.cell().row(), 1) # ------------------------------------------------------------------------- def del_col(self): self.table().removeColumns(self.cell().column(), 1) # ------------------------------------------------------------------------- def ins_row(self): self.table().insertRows(self.cell().row(), 1) # ------------------------------------------------------------------------- def ins_col(self): self.table().insertColumns(self.cell().column(), 1) # ------------------------------------------------------------------------- def merge_cells(self): self.table().mergeCells(self._text_edit_cursor) # ------------------------------------------------------------------------- def split_cells(self): self.table().splitCell(self.cell().row(), self.cell().column(), 1, 1) # ------------------------------------------------------------------------- def replace(self, replace_text): self._text_edit_cursor.insertText(replace_text) # ------------------------------------------------------------------------- def ins_date(self): self._text_edit_cursor.insertText( datetime.datetime.now().strftime("%d.%m.%Y ")) # ------------------------------------------------------------------------- def ins_time(self): self._text_edit_cursor.insertText( datetime.datetime.now().strftime("%d.%m.%Y %H:%M:%S ")) # ------------------------------------------------------------------------- def in_image(self): return self._text_edit_cursor.charFormat().isImageFormat() # ------------------------------------------------------------------------- def ins_image(self, image, fmt, width, height, insert_space=True): bytes_ = QByteArray() buffer = QBuffer(bytes_) buffer.open(QIODevice.WriteOnly) image.save(buffer, fmt) buffer.close() base64 = bytes_.toBase64().data().decode(encoding="utf-8") s = (f'<img width="{width}" height="{height}" ' f'src="data:image/{fmt};base64,{base64}"') self._text_edit_cursor.insertHtml(s) if insert_space: self._text_edit_cursor.insertText(" ") # ------------------------------------------------------------------------- @staticmethod def get_image(html): image, width, height, fmt = None, -1, -1, "png" if "<img" in html: raw = html[html.index("<img"):].split(">")[0].split('"') for i, r in enumerate(raw): if "base64" in r: img_txt = r.split(",")[-1].encode(encoding="utf-8") image = QPixmap() image.width() image.height() image.loadFromData(QByteArray.fromBase64(img_txt)) fmt = r.split("image/")[1].split(";")[0] if "width" in r and i + 1 < len(raw): width = int(raw[i + 1]) if "height" in r and i + 1 < len(raw): height = int(raw[i + 1]) return image, width, height, fmt # ------------------------------------------------------------------------- def save(self, save_proc): if self._text.isModified(): self._text.setModified(False) if self._cfg.get("TextEditor/PlainText", 0): txt = str(self._text.toPlainText()) if self._cfg.get("TextEditor/ReplaceTabWithSpace", 0): cnt = self._cfg.get("TextEditor/CountSpaceInTab", 1) txt = txt.replace("\t", " " * cnt) res = save_proc(txt) else: res = save_proc(str(self._text.toHtml(encoding=QByteArray()))) if res is not None: self._text.setModified(True) self.change(self._text_edit_cursor) # ------------------------------------------------------------------------- def load(self, load_proc): self._text.clear() if self._cfg.get("TextEditor/PlainText", 0): self._text.setPlainText(load_proc()) else: self._text.setHtml(load_proc()) self._text.setModified(False) # ------------------------------------------------------------------------- def get_text(self): if self._cfg.get("TextEditor/PlainText", 0): return str(self._text.toPlainText()) else: return str(self._text.toHtml(encoding=QByteArray())) # ------------------------------------------------------------------------- def is_empty(self): return not bool(len(self._text.toPlainText().strip()))
def apply_smart_comment(editor, opening='/*', closing='*/', line_comment=None): doc = editor.document() c = QTextCursor(editor.textCursor()) c.clearSelection() before_opening = doc.find(opening, c, doc.FindBackward | doc.FindCaseSensitively) before_closing = doc.find(closing, c, doc.FindBackward | doc.FindCaseSensitively) after_opening = doc.find(opening, c, doc.FindCaseSensitively) after_closing = doc.find(closing, c, doc.FindCaseSensitively) in_block_comment = (not before_opening.isNull() and (before_closing.isNull() or before_opening.position() >= before_closing.position())) and \ (not after_closing.isNull() and (after_opening.isNull() or after_closing.position() <= after_opening.position())) if in_block_comment: before_opening.removeSelectedText(), after_closing.removeSelectedText() return c = QTextCursor(editor.textCursor()) left, right = min(c.position(), c.anchor()), max(c.position(), c.anchor()) c.beginEditBlock() c.setPosition(right), c.insertText(closing) c.setPosition(left), c.insertText(opening) c.endEditBlock()
def show_context_menu(self, pos): m = QMenu(self) a = m.addAction c = self.editor.cursorForPosition(pos) origc = QTextCursor(c) current_cursor = self.editor.textCursor() r = origr = self.editor.syntax_range_for_cursor(c) if ( r is None or not r.format.property(SPELL_PROPERTY) ) and c.positionInBlock() > 0 and not current_cursor.hasSelection(): c.setPosition(c.position() - 1) r = self.editor.syntax_range_for_cursor(c) if r is not None and r.format.property(SPELL_PROPERTY): word = self.editor.text_for_range(c.block(), r) locale = self.editor.spellcheck_locale_for_cursor(c) orig_pos = c.position() c.setPosition(orig_pos - utf16_length(word)) found = False self.editor.setTextCursor(c) if self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False): found = True fc = self.editor.textCursor() if fc.position() < c.position(): self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False) spell_cursor = self.editor.textCursor() if current_cursor.hasSelection(): # Restore the current cursor so that any selection is preserved # for the change case actions self.editor.setTextCursor(current_cursor) if found: suggestions = dictionaries.suggestions(word, locale)[:7] if suggestions: for suggestion in suggestions: ac = m.addAction( suggestion, partial(self.editor.simple_replace, suggestion, cursor=spell_cursor)) f = ac.font() f.setBold(True), ac.setFont(f) m.addSeparator() m.addAction(actions['spell-next']) m.addAction(_('Ignore this word'), partial(self._nuke_word, None, word, locale)) dics = dictionaries.active_user_dictionaries if len(dics) > 0: if len(dics) == 1: m.addAction( _('Add this word to the dictionary: {0}').format( dics[0].name), partial(self._nuke_word, dics[0].name, word, locale)) else: ac = m.addAction(_('Add this word to the dictionary')) dmenu = QMenu(m) ac.setMenu(dmenu) for dic in dics: dmenu.addAction( dic.name, partial(self._nuke_word, dic.name, word, locale)) m.addSeparator() if origr is not None and origr.format.property(LINK_PROPERTY): href = self.editor.text_for_range(origc.block(), origr) m.addAction( _('Open %s') % href, partial(self.link_clicked.emit, href)) if origr is not None and (origr.format.property(TAG_NAME_PROPERTY) or origr.format.property(CSS_PROPERTY)): word = self.editor.text_for_range(origc.block(), origr) item_type = 'tag_name' if origr.format.property( TAG_NAME_PROPERTY) else 'css_property' url = help_url(word, item_type, self.editor.highlighter.doc_name, extra_data=current_container().opf_version) if url is not None: m.addAction( _('Show help for: %s') % word, partial(open_url, url)) for x in ('undo', 'redo'): ac = actions['editor-%s' % x] if ac.isEnabled(): a(ac) m.addSeparator() for x in ('cut', 'copy', 'paste'): ac = actions['editor-' + x] if ac.isEnabled(): a(ac) m.addSeparator() m.addAction(_('&Select all'), self.editor.select_all) if self.selected_text or self.has_marked_text: update_mark_text_action(self) m.addAction(actions['mark-selected-text']) if self.syntax != 'css' and actions['editor-cut'].isEnabled(): cm = QMenu(_('Change &case'), m) for ac in 'upper lower swap title capitalize'.split(): cm.addAction(actions['transform-case-' + ac]) m.addMenu(cm) if self.syntax == 'html': m.addAction(actions['multisplit']) m.exec_(self.editor.viewport().mapToGlobal(pos))
class DocumentFactory: def __init__(self, contentPath, formatManager): self.formatManager = formatManager self.contentPath = contentPath def createDocument(self, rootFrame): # Create empty document self.document = QTextDocument() self.document.setUndoRedoEnabled(False) self.document.setIndentWidth(20) # Register a renderer for custom text objects mo = CustomObjectRenderer() mo.setParent(self.document) self.document.documentLayout().registerHandler(QTextCharFormat.UserObject+1, mo); self.cursor = QTextCursor(self.document) self.listLevel = 0 self.paraFormat = None # add all root paragraphs for n in rootFrame.children: self.addNode(n) # Clean up the first paragraph if document is not empty self.cursor.movePosition(QTextCursor.Start) b = self.cursor.block() if b.length() == 1: cursor = QTextCursor(self.document.findBlockByLineNumber(0)) cursor.select(QTextCursor.BlockUnderCursor) cursor.deleteChar() return self.document def addNode(self, node): if type(node) == Paragraph: self.paraFormat = self.formatManager.getFormat(node.style) # NOTE: "The block char format is the format used when inserting # text at the beginning of an empty block." # See also below. self.cursor.insertBlock(self.paraFormat.getBlockFormat(), self.paraFormat.getCharFormat()) # self.cursor.insertFragment(QTextDocumentFragment.fromPlainText('')) if self.listLevel > 0: # TODO: use list style from list node - requires a stack, though ... listStyle = ('itemizedlist', 'level', str(self.listLevel)) newList = self.cursor.createList(self.formatManager.getFormat(listStyle).getListFormat()) for n in node.children: self.addNode(n) elif type(node) == List: self.listLevel += 1 for n in node.children: self.addNode(n) self.listLevel -= 1 elif type(node) is ImageFragment: imageObject = ImageObject() imagePath = os.path.join(self.contentPath, node.image) imageObject.setName(imagePath) imageObjectFormat = QTextCharFormat() imageObjectFormat.setObjectType(QTextFormat.UserObject + 1) imageObjectFormat.setProperty(QTextFormat.UserProperty + 1, imageObject) self.cursor.insertText('\ufffc', imageObjectFormat); elif type(node) is MathFragment: mathFormula = MathFormulaObject() mathFormula.setFormula(node.text) mathFormula.image = node.image # renderFormula() mathObjectFormat = QTextCharFormat() mathObjectFormat.setObjectType(QTextFormat.UserObject + 1) mathObjectFormat.setVerticalAlignment(QTextCharFormat.AlignMiddle) mathObjectFormat.setProperty(QTextFormat.UserProperty + 1, mathFormula) self.cursor.insertText('\ufffc', mathObjectFormat); elif type(node) is TextFragment: text = node.text.replace('\n', '\u2028') if node.href is not None: fmt = self.formatManager.getFormat(('link', None, None)) # TODO! charFmt = fmt.getCharFormat() charFmt.setAnchorHref(node.href) self.cursor.insertText(text, charFmt) else: # "The block char format is the format used when inserting text at the beginning of an empty block. # Hence, the block char format is only useful for the first fragment - # once a fragment is inserted with a different style, and afterwards # another fragment is inserted with no specific style, we need to reset # the char format to the block's char format explicitly! if node.style is not None: fmt = self.formatManager.getFormat(node.style) else: fmt = self.paraFormat self.cursor.insertText(text, fmt.getCharFormat())