class Editor(QMainWindow): has_line_numbers = True modification_state_changed = pyqtSignal(object) undo_redo_state_changed = pyqtSignal(object, object) copy_available_state_changed = pyqtSignal(object) data_changed = pyqtSignal(object) cursor_position_changed = pyqtSignal() word_ignored = pyqtSignal(object, object) link_clicked = pyqtSignal(object) smart_highlighting_updated = pyqtSignal() def __init__(self, syntax, parent=None): QMainWindow.__init__(self, parent) if parent is None: self.setWindowFlags(Qt.Widget) self.is_synced_to_container = False self.syntax = syntax self.editor = TextEdit(self) self.editor.setContextMenuPolicy(Qt.CustomContextMenu) self.editor.customContextMenuRequested.connect(self.show_context_menu) self.setCentralWidget(self.editor) self.create_toolbars() self.undo_available = False self.redo_available = False self.copy_available = self.cut_available = False self.editor.modificationChanged.connect( self._modification_state_changed) self.editor.undoAvailable.connect(self._undo_available) self.editor.redoAvailable.connect(self._redo_available) self.editor.textChanged.connect(self._data_changed) self.editor.copyAvailable.connect(self._copy_available) self.editor.cursorPositionChanged.connect( self._cursor_position_changed) self.editor.link_clicked.connect(self.link_clicked) self.editor.smart_highlighting_updated.connect( self.smart_highlighting_updated) @property def current_line(self): return self.editor.textCursor().blockNumber() @current_line.setter def current_line(self, val): self.editor.go_to_line(val) @property def current_editing_state(self): c = self.editor.textCursor() return {'cursor': (c.anchor(), c.position())} @current_editing_state.setter def current_editing_state(self, val): anchor, position = val.get('cursor', (None, None)) if anchor is not None and position is not None: c = self.editor.textCursor() c.setPosition(anchor), c.setPosition(position, c.KeepAnchor) self.editor.setTextCursor(c) def current_tag(self, for_position_sync=True): return self.editor.current_tag(for_position_sync=for_position_sync) @property def number_of_lines(self): return self.editor.blockCount() @property def data(self): ans = self.get_raw_data() ans, changed = replace_encoding_declarations(ans, enc='utf-8', limit=4 * 1024) if changed: self.data = ans return ans.encode('utf-8') @data.setter def data(self, val): self.editor.load_text(val, syntax=self.syntax, doc_name=editor_name(self)) def init_from_template(self, template): self.editor.load_text(template, syntax=self.syntax, process_template=True, doc_name=editor_name(self)) def change_document_name(self, newname): self.editor.change_document_name(newname) self.editor.completion_doc_name = newname def get_raw_data(self): # The EPUB spec requires NFC normalization, see section 1.3.6 of # http://www.idpf.org/epub/20/spec/OPS_2.0.1_draft.htm return unicodedata.normalize( 'NFC', unicode_type(self.editor.toPlainText()).rstrip('\0')) def replace_data(self, raw, only_if_different=True): if isinstance(raw, bytes): raw = raw.decode('utf-8') current = self.get_raw_data() if only_if_different else False if current != raw: self.editor.replace_text(raw) def apply_settings(self, prefs=None, dictionaries_changed=False): self.editor.apply_settings(prefs=None, dictionaries_changed=dictionaries_changed) def set_focus(self): self.editor.setFocus(Qt.OtherFocusReason) def action_triggered(self, action): action, args = action[0], action[1:] func = getattr(self.editor, action) func(*args) def insert_image(self, href, fullpage=False, preserve_aspect_ratio=False, width=-1, height=-1): self.editor.insert_image(href, fullpage=fullpage, preserve_aspect_ratio=preserve_aspect_ratio, width=width, height=height) def insert_hyperlink(self, href, text, template=None): self.editor.insert_hyperlink(href, text, template=template) def _build_insert_tag_button_menu(self): m = self.insert_tag_menu m.clear() names = tprefs['insert_tag_mru'] for name in names: m.addAction(name, partial(self.insert_tag, name)) if names: m.addSeparator() m = m.addMenu(_('Remove from this menu')) for name in names: m.addAction(name, partial(self.remove_insert_tag, name)) def insert_tag(self, name): self.editor.insert_tag(name) mru = tprefs['insert_tag_mru'] try: mru.remove(name) except ValueError: pass mru.insert(0, name) tprefs['insert_tag_mru'] = mru self._build_insert_tag_button_menu() def remove_insert_tag(self, name): mru = tprefs['insert_tag_mru'] try: mru.remove(name) except ValueError: pass tprefs['insert_tag_mru'] = mru self._build_insert_tag_button_menu() def set_request_completion(self, callback=None, doc_name=None): self.editor.request_completion = callback self.editor.completion_doc_name = doc_name def handle_completion_result(self, result): return self.editor.handle_completion_result(result) def undo(self): self.editor.undo() def redo(self): self.editor.redo() @property def selected_text(self): return self.editor.selected_text def get_smart_selection(self, update=True): return self.editor.smarts.get_smart_selection(self.editor, update=update) # Search and replace {{{ def mark_selected_text(self): self.editor.mark_selected_text() def find(self, *args, **kwargs): return self.editor.find(*args, **kwargs) def find_text(self, *args, **kwargs): return self.editor.find_text(*args, **kwargs) def find_spell_word(self, *args, **kwargs): return self.editor.find_spell_word(*args, **kwargs) def replace(self, *args, **kwargs): return self.editor.replace(*args, **kwargs) def all_in_marked(self, *args, **kwargs): return self.editor.all_in_marked(*args, **kwargs) def go_to_anchor(self, *args, **kwargs): return self.editor.go_to_anchor(*args, **kwargs) # }}} @property def has_marked_text(self): return self.editor.current_search_mark is not None @property def is_modified(self): return self.editor.is_modified @is_modified.setter def is_modified(self, val): self.editor.is_modified = val def create_toolbars(self): self.action_bar = b = self.addToolBar(_('Edit actions tool bar')) b.setObjectName('action_bar') # Needed for saveState self.tools_bar = b = self.addToolBar(_('Editor tools')) b.setObjectName('tools_bar') self.bars = [self.action_bar, self.tools_bar] if self.syntax == 'html': self.format_bar = b = self.addToolBar(_('Format text')) b.setObjectName('html_format_bar') self.bars.append(self.format_bar) self.insert_tag_menu = QMenu(self) self.populate_toolbars() for x in self.bars: x.setFloatable(False) x.topLevelChanged.connect(self.toolbar_floated) x.setIconSize( QSize(tprefs['toolbar_icon_size'], tprefs['toolbar_icon_size'])) def toolbar_floated(self, floating): if not floating: self.save_state() for ed in itervalues(editors): if ed is not self: ed.restore_state() def save_state(self): for bar in self.bars: if bar.isFloating(): return tprefs['%s-editor-state' % self.syntax] = bytearray(self.saveState()) def restore_state(self): state = tprefs.get('%s-editor-state' % self.syntax, None) if state is not None: self.restoreState(state) for bar in self.bars: bar.setVisible(len(bar.actions()) > 0) def populate_toolbars(self): self.action_bar.clear(), self.tools_bar.clear() def add_action(name, bar): if name is None: bar.addSeparator() return try: ac = actions[name] except KeyError: if DEBUG: prints('Unknown editor tool: %r' % name) return bar.addAction(ac) if name == 'insert-tag': w = bar.widgetForAction(ac) if hasattr(w, 'setPopupMode'): # For some unknown reason this button is occassionally a # QPushButton instead of a QToolButton w.setPopupMode(QToolButton.MenuButtonPopup) w.setMenu(self.insert_tag_menu) w.setContextMenuPolicy(Qt.CustomContextMenu) w.customContextMenuRequested.connect(w.showMenu) self._build_insert_tag_button_menu() elif name == 'change-paragraph': m = ac.m = QMenu() ac.setMenu(m) ch = bar.widgetForAction(ac) if hasattr(ch, 'setPopupMode'): # For some unknown reason this button is occassionally a # QPushButton instead of a QToolButton ch.setPopupMode(QToolButton.InstantPopup) for name in tuple('h%d' % d for d in range(1, 7)) + ('p', ): m.addAction(actions['rename-block-tag-%s' % name]) for name in tprefs.get('editor_common_toolbar', ()): add_action(name, self.action_bar) for name in tprefs.get('editor_%s_toolbar' % self.syntax, ()): add_action(name, self.tools_bar) if self.syntax == 'html': self.format_bar.clear() for name in tprefs['editor_format_toolbar']: add_action(name, self.format_bar) self.restore_state() def break_cycles(self): for x in ('modification_state_changed', 'word_ignored', 'link_clicked', 'smart_highlighting_updated'): try: getattr(self, x).disconnect() except TypeError: pass # in case this signal was never connected self.undo_redo_state_changed.disconnect() self.copy_available_state_changed.disconnect() self.cursor_position_changed.disconnect() self.data_changed.disconnect() self.editor.undoAvailable.disconnect() self.editor.redoAvailable.disconnect() self.editor.modificationChanged.disconnect() self.editor.textChanged.disconnect() self.editor.copyAvailable.disconnect() self.editor.cursorPositionChanged.disconnect() self.editor.link_clicked.disconnect() self.editor.smart_highlighting_updated.disconnect() self.editor.setPlainText('') self.editor.smarts = None self.editor.request_completion = None def _modification_state_changed(self): self.is_synced_to_container = self.is_modified self.modification_state_changed.emit(self.is_modified) def _data_changed(self): self.is_synced_to_container = False self.data_changed.emit(self) def _undo_available(self, available): self.undo_available = available self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) def _redo_available(self, available): self.redo_available = available self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) def _copy_available(self, available): self.copy_available = self.cut_available = available self.copy_available_state_changed.emit(available) def _cursor_position_changed(self, *args): self.cursor_position_changed.emit() @property def cursor_position(self): c = self.editor.textCursor() char = '' col = c.positionInBlock() if not c.atStart(): c.clearSelection() c.movePosition(c.PreviousCharacter, c.KeepAnchor) char = unicode_type(c.selectedText()).rstrip('\0') return (c.blockNumber() + 1, col, char) def cut(self): self.editor.cut() def copy(self): self.editor.copy() def go_to_line(self, line, col=None): self.editor.go_to_line(line, col=col) def paste(self): if not self.editor.canPaste(): return error_dialog( self, _('No text'), _('There is no suitable text in the clipboard to paste.'), show=True) self.editor.paste() def contextMenuEvent(self, ev): ev.ignore() def fix_html(self): if self.syntax == 'html': from calibre.ebooks.oeb.polish.pretty import fix_html self.editor.replace_text( fix_html(current_container(), unicode_type( self.editor.toPlainText())).decode('utf-8')) return True return False def pretty_print(self, name): from calibre.ebooks.oeb.polish.pretty import pretty_html, pretty_css, pretty_xml if self.syntax in {'css', 'html', 'xml'}: func = { 'css': pretty_css, 'xml': pretty_xml }.get(self.syntax, pretty_html) original_text = unicode_type(self.editor.toPlainText()) prettied_text = func(current_container(), name, original_text).decode('utf-8') if original_text != prettied_text: self.editor.replace_text(prettied_text) return True return False 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 goto_sourceline(self, *args, **kwargs): return self.editor.goto_sourceline(*args, **kwargs) def goto_css_rule(self, *args, **kwargs): return self.editor.goto_css_rule(*args, **kwargs) def get_tag_contents(self, *args, **kwargs): return self.editor.get_tag_contents(*args, **kwargs) def _nuke_word(self, dic, word, locale): if dic is None: dictionaries.ignore_word(word, locale) else: dictionaries.add_to_user_dictionary(dic, word, locale) self.word_ignored.emit(word, locale)
class Editor(QMainWindow): has_line_numbers = True modification_state_changed = pyqtSignal(object) undo_redo_state_changed = pyqtSignal(object, object) copy_available_state_changed = pyqtSignal(object) data_changed = pyqtSignal(object) cursor_position_changed = pyqtSignal() word_ignored = pyqtSignal(object, object) link_clicked = pyqtSignal(object) smart_highlighting_updated = pyqtSignal() def __init__(self, syntax, parent=None): QMainWindow.__init__(self, parent) if parent is None: self.setWindowFlags(Qt.Widget) self.is_synced_to_container = False self.syntax = syntax self.editor = TextEdit(self) self.editor.setContextMenuPolicy(Qt.CustomContextMenu) self.editor.customContextMenuRequested.connect(self.show_context_menu) self.setCentralWidget(self.editor) self.create_toolbars() self.undo_available = False self.redo_available = False self.copy_available = self.cut_available = False self.editor.modificationChanged.connect(self._modification_state_changed) self.editor.undoAvailable.connect(self._undo_available) self.editor.redoAvailable.connect(self._redo_available) self.editor.textChanged.connect(self._data_changed) self.editor.copyAvailable.connect(self._copy_available) self.editor.cursorPositionChanged.connect(self._cursor_position_changed) self.editor.link_clicked.connect(self.link_clicked) self.editor.smart_highlighting_updated.connect(self.smart_highlighting_updated) @dynamic_property def current_line(self): def fget(self): return self.editor.textCursor().blockNumber() def fset(self, val): self.editor.go_to_line(val) return property(fget=fget, fset=fset) @dynamic_property def current_editing_state(self): def fget(self): c = self.editor.textCursor() return {'cursor':(c.anchor(), c.position())} def fset(self, val): anchor, position = val.get('cursor', (None, None)) if anchor is not None and position is not None: c = self.editor.textCursor() c.setPosition(anchor), c.setPosition(position, c.KeepAnchor) self.editor.setTextCursor(c) return property(fget=fget, fset=fset) def current_tag(self, for_position_sync=True): return self.editor.current_tag(for_position_sync=for_position_sync) @property def number_of_lines(self): return self.editor.blockCount() @dynamic_property def data(self): def fget(self): ans = self.get_raw_data() ans, changed = replace_encoding_declarations(ans, enc='utf-8', limit=4*1024) if changed: self.data = ans return ans.encode('utf-8') def fset(self, val): self.editor.load_text(val, syntax=self.syntax, doc_name=editor_name(self)) return property(fget=fget, fset=fset) def init_from_template(self, template): self.editor.load_text(template, syntax=self.syntax, process_template=True, doc_name=editor_name(self)) def change_document_name(self, newname): self.editor.change_document_name(newname) self.editor.completion_doc_name = newname def get_raw_data(self): # The EPUB spec requires NFC normalization, see section 1.3.6 of # http://www.idpf.org/epub/20/spec/OPS_2.0.1_draft.htm return unicodedata.normalize('NFC', unicode(self.editor.toPlainText()).rstrip('\0')) def replace_data(self, raw, only_if_different=True): if isinstance(raw, bytes): raw = raw.decode('utf-8') current = self.get_raw_data() if only_if_different else False if current != raw: self.editor.replace_text(raw) def apply_settings(self, prefs=None, dictionaries_changed=False): self.editor.apply_settings(prefs=None, dictionaries_changed=dictionaries_changed) def set_focus(self): self.editor.setFocus(Qt.OtherFocusReason) def action_triggered(self, action): action, args = action[0], action[1:] func = getattr(self.editor, action) func(*args) def insert_image(self, href, fullpage=False, preserve_aspect_ratio=False): self.editor.insert_image(href, fullpage=fullpage, preserve_aspect_ratio=preserve_aspect_ratio) def insert_hyperlink(self, href, text): self.editor.insert_hyperlink(href, text) def _build_insert_tag_button_menu(self): m = self.insert_tag_menu m.clear() names = tprefs['insert_tag_mru'] for name in names: m.addAction(name, partial(self.insert_tag, name)) if names: m.addSeparator() m = m.addMenu(_('Remove from this menu')) for name in names: m.addAction(name, partial(self.remove_insert_tag, name)) def insert_tag(self, name): self.editor.insert_tag(name) mru = tprefs['insert_tag_mru'] try: mru.remove(name) except ValueError: pass mru.insert(0, name) tprefs['insert_tag_mru'] = mru self._build_insert_tag_button_menu() def remove_insert_tag(self, name): mru = tprefs['insert_tag_mru'] try: mru.remove(name) except ValueError: pass tprefs['insert_tag_mru'] = mru self._build_insert_tag_button_menu() def set_request_completion(self, callback=None, doc_name=None): self.editor.request_completion = callback self.editor.completion_doc_name = doc_name def handle_completion_result(self, result): return self.editor.handle_completion_result(result) def undo(self): self.editor.undo() def redo(self): self.editor.redo() @property def selected_text(self): return self.editor.selected_text def get_smart_selection(self, update=True): return self.editor.smarts.get_smart_selection(self.editor, update=update) # Search and replace {{{ def mark_selected_text(self): self.editor.mark_selected_text() def find(self, *args, **kwargs): return self.editor.find(*args, **kwargs) def find_text(self, *args, **kwargs): return self.editor.find_text(*args, **kwargs) def find_spell_word(self, *args, **kwargs): return self.editor.find_spell_word(*args, **kwargs) def replace(self, *args, **kwargs): return self.editor.replace(*args, **kwargs) def all_in_marked(self, *args, **kwargs): return self.editor.all_in_marked(*args, **kwargs) def go_to_anchor(self, *args, **kwargs): return self.editor.go_to_anchor(*args, **kwargs) # }}} @property def has_marked_text(self): return self.editor.current_search_mark is not None @dynamic_property def is_modified(self): def fget(self): return self.editor.is_modified def fset(self, val): self.editor.is_modified = val return property(fget=fget, fset=fset) def create_toolbars(self): self.action_bar = b = self.addToolBar(_('Edit actions tool bar')) b.setObjectName('action_bar') # Needed for saveState self.tools_bar = b = self.addToolBar(_('Editor tools')) b.setObjectName('tools_bar') self.bars = [self.action_bar, self.tools_bar] if self.syntax == 'html': self.format_bar = b = self.addToolBar(_('Format text')) b.setObjectName('html_format_bar') self.bars.append(self.format_bar) self.insert_tag_menu = QMenu(self) self.populate_toolbars() for x in self.bars: x.setFloatable(False) x.topLevelChanged.connect(self.toolbar_floated) x.setIconSize(QSize(tprefs['toolbar_icon_size'], tprefs['toolbar_icon_size'])) def toolbar_floated(self, floating): if not floating: self.save_state() for ed in editors.itervalues(): if ed is not self: ed.restore_state() def save_state(self): for bar in self.bars: if bar.isFloating(): return tprefs['%s-editor-state' % self.syntax] = bytearray(self.saveState()) def restore_state(self): state = tprefs.get('%s-editor-state' % self.syntax, None) if state is not None: self.restoreState(state) def populate_toolbars(self): self.action_bar.clear(), self.tools_bar.clear() def add_action(name, bar): if name is None: bar.addSeparator() return try: ac = actions[name] except KeyError: if DEBUG: prints('Unknown editor tool: %r' % name) return bar.addAction(ac) if name == 'insert-tag': w = bar.widgetForAction(ac) w.setPopupMode(QToolButton.MenuButtonPopup) w.setMenu(self.insert_tag_menu) w.setContextMenuPolicy(Qt.CustomContextMenu) w.customContextMenuRequested.connect(w.showMenu) self._build_insert_tag_button_menu() elif name == 'change-paragraph': m = ac.m = QMenu() ac.setMenu(m) ch = bar.widgetForAction(ac) ch.setPopupMode(QToolButton.InstantPopup) for name in tuple('h%d' % d for d in range(1, 7)) + ('p',): m.addAction(actions['rename-block-tag-%s' % name]) for name in tprefs.get('editor_common_toolbar', ()): add_action(name, self.action_bar) for name in tprefs.get('editor_%s_toolbar' % self.syntax, ()): add_action(name, self.tools_bar) if self.syntax == 'html': self.format_bar.clear() for name in tprefs['editor_format_toolbar']: add_action(name, self.format_bar) self.restore_state() def break_cycles(self): for x in ('modification_state_changed', 'word_ignored', 'link_clicked', 'smart_highlighting_updated'): try: getattr(self, x).disconnect() except TypeError: pass # in case this signal was never connected self.undo_redo_state_changed.disconnect() self.copy_available_state_changed.disconnect() self.cursor_position_changed.disconnect() self.data_changed.disconnect() self.editor.undoAvailable.disconnect() self.editor.redoAvailable.disconnect() self.editor.modificationChanged.disconnect() self.editor.textChanged.disconnect() self.editor.copyAvailable.disconnect() self.editor.cursorPositionChanged.disconnect() self.editor.link_clicked.disconnect() self.editor.smart_highlighting_updated.disconnect() self.editor.setPlainText('') self.editor.smarts = None self.editor.request_completion = None def _modification_state_changed(self): self.is_synced_to_container = self.is_modified self.modification_state_changed.emit(self.is_modified) def _data_changed(self): self.is_synced_to_container = False self.data_changed.emit(self) def _undo_available(self, available): self.undo_available = available self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) def _redo_available(self, available): self.redo_available = available self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) def _copy_available(self, available): self.copy_available = self.cut_available = available self.copy_available_state_changed.emit(available) def _cursor_position_changed(self, *args): self.cursor_position_changed.emit() @property def cursor_position(self): c = self.editor.textCursor() char = '' col = c.positionInBlock() if not c.atStart(): c.clearSelection() c.movePosition(c.PreviousCharacter, c.KeepAnchor) char = unicode(c.selectedText()).rstrip('\0') return (c.blockNumber() + 1, col, char) def cut(self): self.editor.cut() def copy(self): self.editor.copy() def go_to_line(self, line, col=None): self.editor.go_to_line(line, col=col) def paste(self): if not self.editor.canPaste(): return error_dialog(self, _('No text'), _( 'There is no suitable text in the clipboard to paste.'), show=True) self.editor.paste() def contextMenuEvent(self, ev): ev.ignore() def fix_html(self): if self.syntax == 'html': from calibre.ebooks.oeb.polish.pretty import fix_html self.editor.replace_text(fix_html(current_container(), unicode(self.editor.toPlainText())).decode('utf-8')) return True return False def pretty_print(self, name): from calibre.ebooks.oeb.polish.pretty import pretty_html, pretty_css, pretty_xml if self.syntax in {'css', 'html', 'xml'}: func = {'css':pretty_css, 'xml':pretty_xml}.get(self.syntax, pretty_html) original_text = unicode(self.editor.toPlainText()) prettied_text = func(current_container(), name, original_text).decode('utf-8') if original_text != prettied_text: self.editor.replace_text(prettied_text) return True return False 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 goto_sourceline(self, *args, **kwargs): return self.editor.goto_sourceline(*args, **kwargs) def goto_css_rule(self, *args, **kwargs): return self.editor.goto_css_rule(*args, **kwargs) def get_tag_contents(self, *args, **kwargs): return self.editor.get_tag_contents(*args, **kwargs) def _nuke_word(self, dic, word, locale): if dic is None: dictionaries.ignore_word(word, locale) else: dictionaries.add_to_user_dictionary(dic, word, locale) self.word_ignored.emit(word, locale)
class Editor(QMainWindow): modification_state_changed = pyqtSignal(object) undo_redo_state_changed = pyqtSignal(object, object) copy_available_state_changed = pyqtSignal(object) data_changed = pyqtSignal(object) def __init__(self, syntax, parent=None): QMainWindow.__init__(self, parent) if parent is None: self.setWindowFlags(Qt.Widget) self.syntax = syntax self.editor = TextEdit(self) self.setCentralWidget(self.editor) self.editor.modificationChanged.connect(self.modification_state_changed.emit) self.create_toolbars() self.undo_available = False self.redo_available = False self.copy_available = self.cut_available = False self.editor.undoAvailable.connect(self._undo_available) self.editor.redoAvailable.connect(self._redo_available) self.editor.textChanged.connect(self._data_changed) self.editor.copyAvailable.connect(self._copy_available) @dynamic_property def data(self): def fget(self): ans = unicode(self.editor.toPlainText()) if self.syntax == 'html': ans = xml_replace_entities(ans) return ans.encode('utf-8') def fset(self, val): self.editor.load_text(val, syntax=self.syntax) return property(fget=fget, fset=fset) def get_raw_data(self): return unicode(self.editor.toPlainText()) def replace_data(self, raw, only_if_different=True): if isinstance(raw, bytes): raw = raw.decode('utf-8') current = self.get_raw_data() if only_if_different else False if current != raw: self.editor.replace_text(raw) def set_focus(self): self.editor.setFocus(Qt.OtherFocusReason) def undo(self): self.editor.undo() def redo(self): self.editor.redo() @dynamic_property def is_modified(self): def fget(self): return self.editor.is_modified def fset(self, val): self.editor.is_modified = val return property(fget=fget, fset=fset) def create_toolbars(self): self.action_bar = b = self.addToolBar(_('File actions tool bar')) b.setObjectName('action_bar') # Needed for saveState for x in ('save', 'undo', 'redo'): try: b.addAction(actions['editor-%s' % x]) except KeyError: pass self.edit_bar = b = self.addToolBar(_('Edit actions tool bar')) for x in ('cut', 'copy', 'paste'): try: b.addAction(actions['editor-%s' % x]) except KeyError: pass def break_cycles(self): self.modification_state_changed.disconnect() self.undo_redo_state_changed.disconnect() self.copy_available_state_changed.disconnect() self.data_changed.disconnect() self.editor.undoAvailable.disconnect() self.editor.redoAvailable.disconnect() self.editor.modificationChanged.disconnect() self.editor.textChanged.disconnect() self.editor.copyAvailable.disconnect() self.editor.setPlainText('') def _data_changed(self): self.data_changed.emit(self) def _undo_available(self, available): self.undo_available = available self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) def _redo_available(self, available): self.redo_available = available self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) def _copy_available(self, available): self.copy_available = self.cut_available = available self.copy_available_state_changed.emit(available) def cut(self): self.editor.cut() def copy(self): self.editor.copy() def paste(self): if not self.editor.canPaste(): return error_dialog(self, _('No text'), _( 'There is no suitable text in the clipboard to paste.'), show=True) self.editor.paste() def contextMenuEvent(self, ev): ev.ignore()
class Editor(QMainWindow): has_line_numbers = True modification_state_changed = pyqtSignal(object) undo_redo_state_changed = pyqtSignal(object, object) copy_available_state_changed = pyqtSignal(object) data_changed = pyqtSignal(object) cursor_position_changed = pyqtSignal() def __init__(self, syntax, parent=None): QMainWindow.__init__(self, parent) if parent is None: self.setWindowFlags(Qt.Widget) self.is_synced_to_container = False self.syntax = syntax self.editor = TextEdit(self) self.editor.setContextMenuPolicy(Qt.CustomContextMenu) self.editor.customContextMenuRequested.connect(self.show_context_menu) self.setCentralWidget(self.editor) self.create_toolbars() self.undo_available = False self.redo_available = False self.copy_available = self.cut_available = False self.editor.modificationChanged.connect(self._modification_state_changed) self.editor.undoAvailable.connect(self._undo_available) self.editor.redoAvailable.connect(self._redo_available) self.editor.textChanged.connect(self._data_changed) self.editor.copyAvailable.connect(self._copy_available) self.editor.cursorPositionChanged.connect(self._cursor_position_changed) @dynamic_property def current_line(self): def fget(self): return self.editor.textCursor().blockNumber() def fset(self, val): self.editor.go_to_line(val) return property(fget=fget, fset=fset) @property def number_of_lines(self): return self.editor.blockCount() @dynamic_property def data(self): def fget(self): ans = self.get_raw_data() return ans.encode('utf-8') def fset(self, val): self.editor.load_text(val, syntax=self.syntax) return property(fget=fget, fset=fset) def init_from_template(self, template): self.editor.load_text(template, syntax=self.syntax, process_template=True) def get_raw_data(self): return unicodedata.normalize('NFC', unicode(self.editor.toPlainText()).rstrip('\0')) def replace_data(self, raw, only_if_different=True): if isinstance(raw, bytes): raw = raw.decode('utf-8') current = self.get_raw_data() if only_if_different else False if current != raw: self.editor.replace_text(raw) def apply_settings(self, prefs=None): self.editor.apply_settings(prefs=None) def set_focus(self): self.editor.setFocus(Qt.OtherFocusReason) def action_triggered(self, action): action, args = action[0], action[1:] func = getattr(self.editor, action) func(*args) def insert_image(self, href): self.editor.insert_image(href) def undo(self): self.editor.undo() def redo(self): self.editor.redo() @property def selected_text(self): return self.editor.selected_text # Search and replace {{{ def mark_selected_text(self): self.editor.mark_selected_text() def find(self, *args, **kwargs): return self.editor.find(*args, **kwargs) def replace(self, *args, **kwargs): return self.editor.replace(*args, **kwargs) def all_in_marked(self, *args, **kwargs): return self.editor.all_in_marked(*args, **kwargs) def go_to_anchor(self, *args, **kwargs): return self.editor.go_to_anchor(*args, **kwargs) # }}} @property def has_marked_text(self): return self.editor.current_search_mark is not None @dynamic_property def is_modified(self): def fget(self): return self.editor.is_modified def fset(self, val): self.editor.is_modified = val return property(fget=fget, fset=fset) def create_toolbars(self): self.action_bar = b = self.addToolBar(_('File actions tool bar')) b.setObjectName('action_bar') # Needed for saveState for x in ('undo', 'redo'): b.addAction(actions['editor-%s' % x]) self.edit_bar = b = self.addToolBar(_('Edit actions tool bar')) for x in ('cut', 'copy', 'paste'): b.addAction(actions['editor-%s' % x]) self.tools_bar = b = self.addToolBar(_('Editor tools')) if self.syntax == 'html': b.addAction(actions['fix-html-current']) if self.syntax in {'xml', 'html', 'css'}: b.addAction(actions['pretty-current']) if self.syntax in {'html', 'css'}: b.addAction(actions['insert-image']) if self.syntax == 'html': self.format_bar = b = self.addToolBar(_('Format text')) for x in ('bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript', 'color', 'background-color'): b.addAction(actions['format-text-%s' % x]) ac = b.addAction(QIcon(I('format-text-heading.png')), _('Change paragraph to heading')) m = QMenu() ac.setMenu(m) b.widgetForAction(ac).setPopupMode(QToolButton.InstantPopup) for name in tuple('h%d' % d for d in range(1, 7)) + ('p',): m.addAction(actions['rename-block-tag-%s' % name]) def break_cycles(self): self.modification_state_changed.disconnect() self.undo_redo_state_changed.disconnect() self.copy_available_state_changed.disconnect() self.cursor_position_changed.disconnect() self.data_changed.disconnect() self.editor.undoAvailable.disconnect() self.editor.redoAvailable.disconnect() self.editor.modificationChanged.disconnect() self.editor.textChanged.disconnect() self.editor.copyAvailable.disconnect() self.editor.cursorPositionChanged.disconnect() self.editor.setPlainText('') self.editor.smarts = None def _modification_state_changed(self): self.is_synced_to_container = self.is_modified self.modification_state_changed.emit(self.is_modified) def _data_changed(self): self.is_synced_to_container = False self.data_changed.emit(self) def _undo_available(self, available): self.undo_available = available self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) def _redo_available(self, available): self.redo_available = available self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) def _copy_available(self, available): self.copy_available = self.cut_available = available self.copy_available_state_changed.emit(available) def _cursor_position_changed(self, *args): self.cursor_position_changed.emit() @property def cursor_position(self): c = self.editor.textCursor() char = '' if not c.atStart(): c.clearSelection() c.setPosition(c.position()-1, c.KeepAnchor) char = unicode(c.selectedText()).rstrip('\0') return (c.blockNumber() + 1, c.positionInBlock(), char) def cut(self): self.editor.cut() def copy(self): self.editor.copy() def go_to_line(self, line, col=None): self.editor.go_to_line(line, col=col) def paste(self): if not self.editor.canPaste(): return error_dialog(self, _('No text'), _( 'There is no suitable text in the clipboard to paste.'), show=True) self.editor.paste() def contextMenuEvent(self, ev): ev.ignore() def fix_html(self): if self.syntax == 'html': from calibre.ebooks.oeb.polish.pretty import fix_html self.editor.replace_text(fix_html(current_container(), unicode(self.editor.toPlainText())).decode('utf-8')) return True return False def pretty_print(self, name): from calibre.ebooks.oeb.polish.pretty import pretty_html, pretty_css, pretty_xml if self.syntax in {'css', 'html', 'xml'}: func = {'css':pretty_css, 'xml':pretty_xml}.get(self.syntax, pretty_html) self.editor.replace_text(func(current_container(), name, unicode(self.editor.toPlainText())).decode('utf-8')) return True return False def show_context_menu(self, pos): m = QMenu(self) a = m.addAction for x in ('undo', 'redo'): a(actions['editor-%s' % x]) m.addSeparator() for x in ('cut', 'copy', 'paste'): a(actions['editor-' + x]) m.addSeparator() m.addAction(_('&Select all'), self.editor.select_all) m.addAction(actions['mark-selected-text']) if self.syntax == 'html': m.addAction(actions['multisplit']) m.exec_(self.editor.mapToGlobal(pos))
class Editor(QMainWindow): has_line_numbers = True modification_state_changed = pyqtSignal(object) undo_redo_state_changed = pyqtSignal(object, object) copy_available_state_changed = pyqtSignal(object) data_changed = pyqtSignal(object) cursor_position_changed = pyqtSignal() def __init__(self, syntax, parent=None): QMainWindow.__init__(self, parent) if parent is None: self.setWindowFlags(Qt.Widget) self.is_synced_to_container = False self.syntax = syntax self.editor = TextEdit(self) self.setCentralWidget(self.editor) self.create_toolbars() self.undo_available = False self.redo_available = False self.copy_available = self.cut_available = False self.editor.modificationChanged.connect(self._modification_state_changed) self.editor.undoAvailable.connect(self._undo_available) self.editor.redoAvailable.connect(self._redo_available) self.editor.textChanged.connect(self._data_changed) self.editor.copyAvailable.connect(self._copy_available) self.editor.cursorPositionChanged.connect(self._cursor_position_changed) @dynamic_property def current_line(self): def fget(self): return self.editor.textCursor().blockNumber() def fset(self, val): self.editor.go_to_line(val) return property(fget=fget, fset=fset) @property def number_of_lines(self): return self.editor.blockCount() @dynamic_property def data(self): def fget(self): ans = self.get_raw_data() return ans.encode('utf-8') def fset(self, val): self.editor.load_text(val, syntax=self.syntax) return property(fget=fget, fset=fset) def init_from_template(self, template): self.editor.load_text(template, syntax=self.syntax, process_template=True) def get_raw_data(self): return unicodedata.normalize('NFC', unicode(self.editor.toPlainText())) def replace_data(self, raw, only_if_different=True): if isinstance(raw, bytes): raw = raw.decode('utf-8') current = self.get_raw_data() if only_if_different else False if current != raw: self.editor.replace_text(raw) def apply_settings(self, prefs=None): self.editor.apply_settings(prefs=None) def set_focus(self): self.editor.setFocus(Qt.OtherFocusReason) def undo(self): self.editor.undo() def redo(self): self.editor.redo() @property def selected_text(self): return self.editor.selected_text # Search and replace {{{ def mark_selected_text(self): self.editor.mark_selected_text() def find(self, *args, **kwargs): return self.editor.find(*args, **kwargs) def replace(self, *args, **kwargs): return self.editor.replace(*args, **kwargs) def all_in_marked(self, *args, **kwargs): return self.editor.all_in_marked(*args, **kwargs) def go_to_anchor(self, *args, **kwargs): return self.editor.go_to_anchor(*args, **kwargs) # }}} @property def has_marked_text(self): return self.editor.current_search_mark is not None @dynamic_property def is_modified(self): def fget(self): return self.editor.is_modified def fset(self, val): self.editor.is_modified = val return property(fget=fget, fset=fset) def create_toolbars(self): self.action_bar = b = self.addToolBar(_('File actions tool bar')) b.setObjectName('action_bar') # Needed for saveState for x in ('undo', 'redo'): b.addAction(actions['editor-%s' % x]) self.edit_bar = b = self.addToolBar(_('Edit actions tool bar')) for x in ('cut', 'copy', 'paste'): b.addAction(actions['editor-%s' % x]) self.tools_bar = b = self.addToolBar(_('Editor tools')) if self.syntax == 'html': b.addAction(actions['fix-html-current']) if self.syntax in {'xml', 'html', 'css'}: b.addAction(actions['pretty-current']) def break_cycles(self): self.modification_state_changed.disconnect() self.undo_redo_state_changed.disconnect() self.copy_available_state_changed.disconnect() self.cursor_position_changed.disconnect() self.data_changed.disconnect() self.editor.undoAvailable.disconnect() self.editor.redoAvailable.disconnect() self.editor.modificationChanged.disconnect() self.editor.textChanged.disconnect() self.editor.copyAvailable.disconnect() self.editor.cursorPositionChanged.disconnect() self.editor.setPlainText('') def _modification_state_changed(self): self.is_synced_to_container = self.is_modified self.modification_state_changed.emit(self.is_modified) def _data_changed(self): self.is_synced_to_container = False self.data_changed.emit(self) def _undo_available(self, available): self.undo_available = available self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) def _redo_available(self, available): self.redo_available = available self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) def _copy_available(self, available): self.copy_available = self.cut_available = available self.copy_available_state_changed.emit(available) def _cursor_position_changed(self, *args): self.cursor_position_changed.emit() @property def cursor_position(self): c = self.editor.textCursor() return (c.blockNumber() + 1, c.positionInBlock()) def cut(self): self.editor.cut() def copy(self): self.editor.copy() def go_to_line(self, line, col=None): self.editor.go_to_line(line, col=col) def paste(self): if not self.editor.canPaste(): return error_dialog(self, _('No text'), _( 'There is no suitable text in the clipboard to paste.'), show=True) self.editor.paste() def contextMenuEvent(self, ev): ev.ignore() def fix_html(self): if self.syntax == 'html': from calibre.ebooks.oeb.polish.pretty import fix_html self.editor.replace_text(fix_html(current_container(), unicode(self.editor.toPlainText())).decode('utf-8')) return True return False def pretty_print(self, name): from calibre.ebooks.oeb.polish.pretty import pretty_html, pretty_css, pretty_xml if self.syntax in {'css', 'html', 'xml'}: func = {'css':pretty_css, 'xml':pretty_xml}.get(self.syntax, pretty_html) self.editor.replace_text(func(current_container(), name, unicode(self.editor.toPlainText())).decode('utf-8')) return True return False
class Editor(QMainWindow): has_line_numbers = True modification_state_changed = pyqtSignal(object) undo_redo_state_changed = pyqtSignal(object, object) copy_available_state_changed = pyqtSignal(object) data_changed = pyqtSignal(object) cursor_position_changed = pyqtSignal() def __init__(self, syntax, parent=None): QMainWindow.__init__(self, parent) if parent is None: self.setWindowFlags(Qt.Widget) self.is_synced_to_container = False self.syntax = syntax self.editor = TextEdit(self) self.editor.setContextMenuPolicy(Qt.CustomContextMenu) self.editor.customContextMenuRequested.connect(self.show_context_menu) self.setCentralWidget(self.editor) self.create_toolbars() self.undo_available = False self.redo_available = False self.copy_available = self.cut_available = False self.editor.modificationChanged.connect( self._modification_state_changed) self.editor.undoAvailable.connect(self._undo_available) self.editor.redoAvailable.connect(self._redo_available) self.editor.textChanged.connect(self._data_changed) self.editor.copyAvailable.connect(self._copy_available) self.editor.cursorPositionChanged.connect( self._cursor_position_changed) @dynamic_property def current_line(self): def fget(self): return self.editor.textCursor().blockNumber() def fset(self, val): self.editor.go_to_line(val) return property(fget=fget, fset=fset) @property def number_of_lines(self): return self.editor.blockCount() @dynamic_property def data(self): def fget(self): ans = self.get_raw_data() return ans.encode('utf-8') def fset(self, val): self.editor.load_text(val, syntax=self.syntax) return property(fget=fget, fset=fset) def init_from_template(self, template): self.editor.load_text(template, syntax=self.syntax, process_template=True) def get_raw_data(self): return unicodedata.normalize( 'NFC', unicode(self.editor.toPlainText()).rstrip('\0')) def replace_data(self, raw, only_if_different=True): if isinstance(raw, bytes): raw = raw.decode('utf-8') current = self.get_raw_data() if only_if_different else False if current != raw: self.editor.replace_text(raw) def apply_settings(self, prefs=None, dictionaries_changed=False): self.editor.apply_settings(prefs=None, dictionaries_changed=dictionaries_changed) def set_focus(self): self.editor.setFocus(Qt.OtherFocusReason) def action_triggered(self, action): action, args = action[0], action[1:] func = getattr(self.editor, action) func(*args) def insert_image(self, href): self.editor.insert_image(href) def insert_hyperlink(self, href, text): self.editor.insert_hyperlink(href, text) def _build_insert_tag_button_menu(self): m = self.insert_tag_button.menu() m.clear() for name in tprefs['insert_tag_mru']: m.addAction(name, partial(self.insert_tag, name)) def insert_tag(self, name): self.editor.insert_tag(name) mru = tprefs['insert_tag_mru'] try: mru.remove(name) except ValueError: pass mru.insert(0, name) tprefs['insert_tag_mru'] = mru self._build_insert_tag_button_menu() def undo(self): self.editor.undo() def redo(self): self.editor.redo() @property def selected_text(self): return self.editor.selected_text def get_smart_selection(self, update=True): return self.editor.smarts.get_smart_selection(self.editor, update=update) # Search and replace {{{ def mark_selected_text(self): self.editor.mark_selected_text() def find(self, *args, **kwargs): return self.editor.find(*args, **kwargs) def find_spell_word(self, *args, **kwargs): return self.editor.find_spell_word(*args, **kwargs) def replace(self, *args, **kwargs): return self.editor.replace(*args, **kwargs) def all_in_marked(self, *args, **kwargs): return self.editor.all_in_marked(*args, **kwargs) def go_to_anchor(self, *args, **kwargs): return self.editor.go_to_anchor(*args, **kwargs) # }}} @property def has_marked_text(self): return self.editor.current_search_mark is not None @dynamic_property def is_modified(self): def fget(self): return self.editor.is_modified def fset(self, val): self.editor.is_modified = val return property(fget=fget, fset=fset) def create_toolbars(self): self.action_bar = b = self.addToolBar(_('File actions tool bar')) b.setObjectName('action_bar') # Needed for saveState for x in ('undo', 'redo'): b.addAction(actions['editor-%s' % x]) self.edit_bar = b = self.addToolBar(_('Edit actions tool bar')) for x in ('cut', 'copy', 'paste'): b.addAction(actions['editor-%s' % x]) self.tools_bar = b = self.addToolBar(_('Editor tools')) b.setObjectName('tools_bar') if self.syntax == 'html': b.addAction(actions['fix-html-current']) if self.syntax in {'xml', 'html', 'css'}: b.addAction(actions['pretty-current']) if self.syntax in {'html', 'css'}: b.addAction(actions['insert-image']) if self.syntax == 'html': b.addAction(actions['insert-hyperlink']) if self.syntax in {'xml', 'html'}: b.addAction(actions['insert-tag']) w = self.insert_tag_button = b.widgetForAction( actions['insert-tag']) w.setPopupMode(QToolButton.MenuButtonPopup) w.m = m = QMenu() w.setMenu(m) w.setContextMenuPolicy(Qt.CustomContextMenu) w.customContextMenuRequested.connect( self.insert_tag_button.showMenu) self._build_insert_tag_button_menu() if self.syntax == 'html': self.format_bar = b = self.addToolBar(_('Format text')) b.setObjectName('html_format_bar') for x in ('bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript', 'color', 'background-color'): b.addAction(actions['format-text-%s' % x]) ac = b.addAction(QIcon(I('format-text-heading.png')), _('Change paragraph to heading')) m = QMenu() ac.setMenu(m) b.widgetForAction(ac).setPopupMode(QToolButton.InstantPopup) for name in tuple('h%d' % d for d in range(1, 7)) + ('p', ): m.addAction(actions['rename-block-tag-%s' % name]) def break_cycles(self): self.modification_state_changed.disconnect() self.undo_redo_state_changed.disconnect() self.copy_available_state_changed.disconnect() self.cursor_position_changed.disconnect() self.data_changed.disconnect() self.editor.undoAvailable.disconnect() self.editor.redoAvailable.disconnect() self.editor.modificationChanged.disconnect() self.editor.textChanged.disconnect() self.editor.copyAvailable.disconnect() self.editor.cursorPositionChanged.disconnect() self.editor.setPlainText('') self.editor.smarts = None def _modification_state_changed(self): self.is_synced_to_container = self.is_modified self.modification_state_changed.emit(self.is_modified) def _data_changed(self): self.is_synced_to_container = False self.data_changed.emit(self) def _undo_available(self, available): self.undo_available = available self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) def _redo_available(self, available): self.redo_available = available self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) def _copy_available(self, available): self.copy_available = self.cut_available = available self.copy_available_state_changed.emit(available) def _cursor_position_changed(self, *args): self.cursor_position_changed.emit() @property def cursor_position(self): c = self.editor.textCursor() char = '' col = c.positionInBlock() if not c.atStart(): c.clearSelection() c.movePosition(c.PreviousCharacter, c.KeepAnchor) char = unicode(c.selectedText()).rstrip('\0') return (c.blockNumber() + 1, col, char) def cut(self): self.editor.cut() def copy(self): self.editor.copy() def go_to_line(self, line, col=None): self.editor.go_to_line(line, col=col) def paste(self): if not self.editor.canPaste(): return error_dialog( self, _('No text'), _('There is no suitable text in the clipboard to paste.'), show=True) self.editor.paste() def contextMenuEvent(self, ev): ev.ignore() def fix_html(self): if self.syntax == 'html': from calibre.ebooks.oeb.polish.pretty import fix_html self.editor.replace_text( fix_html(current_container(), unicode(self.editor.toPlainText())).decode('utf-8')) return True return False def pretty_print(self, name): from calibre.ebooks.oeb.polish.pretty import pretty_html, pretty_css, pretty_xml if self.syntax in {'css', 'html', 'xml'}: func = { 'css': pretty_css, 'xml': pretty_xml }.get(self.syntax, pretty_html) original_text = unicode(self.editor.toPlainText()) prettied_text = func(current_container(), name, original_text).decode('utf-8') if original_text != prettied_text: self.editor.replace_text(prettied_text) return True return False def show_context_menu(self, pos): m = QMenu(self) a = m.addAction for x in ('undo', 'redo'): a(actions['editor-%s' % x]) m.addSeparator() for x in ('cut', 'copy', 'paste'): a(actions['editor-' + x]) m.addSeparator() m.addAction(_('&Select all'), self.editor.select_all) m.addAction(actions['mark-selected-text']) if self.syntax == 'html': m.addAction(actions['multisplit']) m.exec_(self.editor.mapToGlobal(pos))
class Editor(QMainWindow): has_line_numbers = True modification_state_changed = pyqtSignal(object) undo_redo_state_changed = pyqtSignal(object, object) copy_available_state_changed = pyqtSignal(object) data_changed = pyqtSignal(object) cursor_position_changed = pyqtSignal() word_ignored = pyqtSignal(object, object) def __init__(self, syntax, parent=None): QMainWindow.__init__(self, parent) if parent is None: self.setWindowFlags(Qt.Widget) self.is_synced_to_container = False self.syntax = syntax self.editor = TextEdit(self) self.editor.setContextMenuPolicy(Qt.CustomContextMenu) self.editor.customContextMenuRequested.connect(self.show_context_menu) self.setCentralWidget(self.editor) self.create_toolbars() self.undo_available = False self.redo_available = False self.copy_available = self.cut_available = False self.editor.modificationChanged.connect(self._modification_state_changed) self.editor.undoAvailable.connect(self._undo_available) self.editor.redoAvailable.connect(self._redo_available) self.editor.textChanged.connect(self._data_changed) self.editor.copyAvailable.connect(self._copy_available) self.editor.cursorPositionChanged.connect(self._cursor_position_changed) @dynamic_property def current_line(self): def fget(self): return self.editor.textCursor().blockNumber() def fset(self, val): self.editor.go_to_line(val) return property(fget=fget, fset=fset) def current_tag(self): return self.editor.current_tag() @property def number_of_lines(self): return self.editor.blockCount() @dynamic_property def data(self): def fget(self): ans = self.get_raw_data() ans, changed = replace_encoding_declarations(ans, enc='utf-8', limit=4*1024) if changed: self.data = ans return ans.encode('utf-8') def fset(self, val): self.editor.load_text(val, syntax=self.syntax) return property(fget=fget, fset=fset) def init_from_template(self, template): self.editor.load_text(template, syntax=self.syntax, process_template=True) def get_raw_data(self): # The EPUB spec requires NFC normalization, see section 1.3.6 of # http://www.idpf.org/epub/20/spec/OPS_2.0.1_draft.htm return unicodedata.normalize('NFC', unicode(self.editor.toPlainText()).rstrip('\0')) def replace_data(self, raw, only_if_different=True): if isinstance(raw, bytes): raw = raw.decode('utf-8') current = self.get_raw_data() if only_if_different else False if current != raw: self.editor.replace_text(raw) def apply_settings(self, prefs=None, dictionaries_changed=False): self.editor.apply_settings(prefs=None, dictionaries_changed=dictionaries_changed) def set_focus(self): self.editor.setFocus(Qt.OtherFocusReason) def action_triggered(self, action): action, args = action[0], action[1:] func = getattr(self.editor, action) func(*args) def insert_image(self, href): self.editor.insert_image(href) def insert_hyperlink(self, href, text): self.editor.insert_hyperlink(href, text) def _build_insert_tag_button_menu(self): m = self.insert_tag_menu m.clear() for name in tprefs['insert_tag_mru']: m.addAction(name, partial(self.insert_tag, name)) def insert_tag(self, name): self.editor.insert_tag(name) mru = tprefs['insert_tag_mru'] try: mru.remove(name) except ValueError: pass mru.insert(0, name) tprefs['insert_tag_mru'] = mru self._build_insert_tag_button_menu() def undo(self): self.editor.undo() def redo(self): self.editor.redo() @property def selected_text(self): return self.editor.selected_text def get_smart_selection(self, update=True): return self.editor.smarts.get_smart_selection(self.editor, update=update) # Search and replace {{{ def mark_selected_text(self): self.editor.mark_selected_text() def find(self, *args, **kwargs): return self.editor.find(*args, **kwargs) def find_spell_word(self, *args, **kwargs): return self.editor.find_spell_word(*args, **kwargs) def replace(self, *args, **kwargs): return self.editor.replace(*args, **kwargs) def all_in_marked(self, *args, **kwargs): return self.editor.all_in_marked(*args, **kwargs) def go_to_anchor(self, *args, **kwargs): return self.editor.go_to_anchor(*args, **kwargs) # }}} @property def has_marked_text(self): return self.editor.current_search_mark is not None @dynamic_property def is_modified(self): def fget(self): return self.editor.is_modified def fset(self, val): self.editor.is_modified = val return property(fget=fget, fset=fset) def create_toolbars(self): self.action_bar = b = self.addToolBar(_('File actions tool bar')) b.setObjectName('action_bar') # Needed for saveState for x in ('undo', 'redo'): b.addAction(actions['editor-%s' % x]) self.edit_bar = b = self.addToolBar(_('Edit actions tool bar')) for x in ('cut', 'copy', 'paste'): b.addAction(actions['editor-%s' % x]) self.tools_bar = b = self.addToolBar(_('Editor tools')) b.setObjectName('tools_bar') if self.syntax == 'html': self.format_bar = b = self.addToolBar(_('Format text')) b.setObjectName('html_format_bar') self.insert_tag_menu = QMenu(self) self.populate_toolbars() def populate_toolbars(self): self.tools_bar.clear() def add_action(name, bar): try: ac = actions[name] except KeyError: if DEBUG: prints('Unknown editor tool: %r' % name) return bar.addAction(ac) if name == 'insert-tag': w = bar.widgetForAction(ac) w.setPopupMode(QToolButton.MenuButtonPopup) w.setMenu(self.insert_tag_menu) w.setContextMenuPolicy(Qt.CustomContextMenu) w.customContextMenuRequested.connect(w.showMenu) self._build_insert_tag_button_menu() elif name == 'change-paragraph': m = ac.m = QMenu() ac.setMenu(m) bar.widgetForAction(ac).setPopupMode(QToolButton.InstantPopup) for name in tuple('h%d' % d for d in range(1, 7)) + ('p',): m.addAction(actions['rename-block-tag-%s' % name]) for name in tprefs.get('editor_%s_toolbar' % self.syntax, ()): add_action(name, self.tools_bar) if self.syntax == 'html': self.format_bar.clear() for name in tprefs['editor_format_toolbar']: add_action(name, self.format_bar) def break_cycles(self): try: self.modification_state_changed.disconnect() except TypeError: pass # in case this signal was never connected try: self.word_ignored.disconnect() except TypeError: pass # in case this signal was never connected self.undo_redo_state_changed.disconnect() self.copy_available_state_changed.disconnect() self.cursor_position_changed.disconnect() self.data_changed.disconnect() self.editor.undoAvailable.disconnect() self.editor.redoAvailable.disconnect() self.editor.modificationChanged.disconnect() self.editor.textChanged.disconnect() self.editor.copyAvailable.disconnect() self.editor.cursorPositionChanged.disconnect() self.editor.setPlainText('') self.editor.smarts = None def _modification_state_changed(self): self.is_synced_to_container = self.is_modified self.modification_state_changed.emit(self.is_modified) def _data_changed(self): self.is_synced_to_container = False self.data_changed.emit(self) def _undo_available(self, available): self.undo_available = available self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) def _redo_available(self, available): self.redo_available = available self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) def _copy_available(self, available): self.copy_available = self.cut_available = available self.copy_available_state_changed.emit(available) def _cursor_position_changed(self, *args): self.cursor_position_changed.emit() @property def cursor_position(self): c = self.editor.textCursor() char = '' col = c.positionInBlock() if not c.atStart(): c.clearSelection() c.movePosition(c.PreviousCharacter, c.KeepAnchor) char = unicode(c.selectedText()).rstrip('\0') return (c.blockNumber() + 1, col, char) def cut(self): self.editor.cut() def copy(self): self.editor.copy() def go_to_line(self, line, col=None): self.editor.go_to_line(line, col=col) def paste(self): if not self.editor.canPaste(): return error_dialog(self, _('No text'), _( 'There is no suitable text in the clipboard to paste.'), show=True) self.editor.paste() def contextMenuEvent(self, ev): ev.ignore() def fix_html(self): if self.syntax == 'html': from calibre.ebooks.oeb.polish.pretty import fix_html self.editor.replace_text(fix_html(current_container(), unicode(self.editor.toPlainText())).decode('utf-8')) return True return False def pretty_print(self, name): from calibre.ebooks.oeb.polish.pretty import pretty_html, pretty_css, pretty_xml if self.syntax in {'css', 'html', 'xml'}: func = {'css':pretty_css, 'xml':pretty_xml}.get(self.syntax, pretty_html) original_text = unicode(self.editor.toPlainText()) prettied_text = func(current_container(), name, original_text).decode('utf-8') if original_text != prettied_text: self.editor.replace_text(prettied_text) return True return False def show_context_menu(self, pos): m = QMenu(self) a = m.addAction c = self.editor.cursorForPosition(pos) r = self.editor.syntax_range_for_cursor(c) if r is not None and r.format.property(SPELL_PROPERTY).toBool(): 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) if found: suggestions = dictionaries.suggestions(word, locale)[:7] if suggestions: for suggestion in suggestions: ac = m.addAction(suggestion, partial(self.editor.simple_replace, suggestion)) 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() for x in ('undo', 'redo'): a(actions['editor-%s' % x]) m.addSeparator() for x in ('cut', 'copy', 'paste'): a(actions['editor-' + x]) m.addSeparator() m.addAction(_('&Select all'), self.editor.select_all) m.addAction(actions['mark-selected-text']) if self.syntax == 'html': m.addAction(actions['multisplit']) m.exec_(self.editor.mapToGlobal(pos)) def goto_sourceline(self, *args, **kwargs): return self.editor.goto_sourceline(*args, **kwargs) def goto_css_rule(self, *args, **kwargs): return self.editor.goto_css_rule(*args, **kwargs) def get_tag_contents(self, *args, **kwargs): return self.editor.get_tag_contents(*args, **kwargs) def _nuke_word(self, dic, word, locale): if dic is None: dictionaries.ignore_word(word, locale) else: dictionaries.add_to_user_dictionary(dic, word, locale) self.word_ignored.emit(word, locale)
class Editor(QMainWindow): has_line_numbers = True modification_state_changed = pyqtSignal(object) undo_redo_state_changed = pyqtSignal(object, object) copy_available_state_changed = pyqtSignal(object) data_changed = pyqtSignal(object) cursor_position_changed = pyqtSignal() def __init__(self, syntax, parent=None): QMainWindow.__init__(self, parent) if parent is None: self.setWindowFlags(Qt.Widget) self.is_synced_to_container = False self.syntax = syntax self.editor = TextEdit(self) self.setCentralWidget(self.editor) self.create_toolbars() self.undo_available = False self.redo_available = False self.copy_available = self.cut_available = False self.editor.modificationChanged.connect( self._modification_state_changed) self.editor.undoAvailable.connect(self._undo_available) self.editor.redoAvailable.connect(self._redo_available) self.editor.textChanged.connect(self._data_changed) self.editor.copyAvailable.connect(self._copy_available) self.editor.cursorPositionChanged.connect( self._cursor_position_changed) @dynamic_property def current_line(self): def fget(self): return self.editor.textCursor().blockNumber() def fset(self, val): self.editor.go_to_line(val) return property(fget=fget, fset=fset) @property def number_of_lines(self): return self.editor.blockCount() @dynamic_property def data(self): def fget(self): ans = self.get_raw_data() return ans.encode('utf-8') def fset(self, val): self.editor.load_text(val, syntax=self.syntax) return property(fget=fget, fset=fset) def init_from_template(self, template): self.editor.load_text(template, syntax=self.syntax, process_template=True) def get_raw_data(self): return unicodedata.normalize('NFC', unicode(self.editor.toPlainText())) def replace_data(self, raw, only_if_different=True): if isinstance(raw, bytes): raw = raw.decode('utf-8') current = self.get_raw_data() if only_if_different else False if current != raw: self.editor.replace_text(raw) def apply_settings(self, prefs=None): self.editor.apply_settings(prefs=None) def set_focus(self): self.editor.setFocus(Qt.OtherFocusReason) def undo(self): self.editor.undo() def redo(self): self.editor.redo() @property def selected_text(self): return self.editor.selected_text # Search and replace {{{ def mark_selected_text(self): self.editor.mark_selected_text() def find(self, *args, **kwargs): return self.editor.find(*args, **kwargs) def replace(self, *args, **kwargs): return self.editor.replace(*args, **kwargs) def all_in_marked(self, *args, **kwargs): return self.editor.all_in_marked(*args, **kwargs) def go_to_anchor(self, *args, **kwargs): return self.editor.go_to_anchor(*args, **kwargs) # }}} @property def has_marked_text(self): return self.editor.current_search_mark is not None @dynamic_property def is_modified(self): def fget(self): return self.editor.is_modified def fset(self, val): self.editor.is_modified = val return property(fget=fget, fset=fset) def create_toolbars(self): self.action_bar = b = self.addToolBar(_('File actions tool bar')) b.setObjectName('action_bar') # Needed for saveState for x in ('undo', 'redo'): b.addAction(actions['editor-%s' % x]) self.edit_bar = b = self.addToolBar(_('Edit actions tool bar')) for x in ('cut', 'copy', 'paste'): b.addAction(actions['editor-%s' % x]) self.tools_bar = b = self.addToolBar(_('Editor tools')) if self.syntax == 'html': b.addAction(actions['fix-html-current']) if self.syntax in {'xml', 'html', 'css'}: b.addAction(actions['pretty-current']) def break_cycles(self): self.modification_state_changed.disconnect() self.undo_redo_state_changed.disconnect() self.copy_available_state_changed.disconnect() self.cursor_position_changed.disconnect() self.data_changed.disconnect() self.editor.undoAvailable.disconnect() self.editor.redoAvailable.disconnect() self.editor.modificationChanged.disconnect() self.editor.textChanged.disconnect() self.editor.copyAvailable.disconnect() self.editor.cursorPositionChanged.disconnect() self.editor.setPlainText('') def _modification_state_changed(self): self.is_synced_to_container = self.is_modified self.modification_state_changed.emit(self.is_modified) def _data_changed(self): self.is_synced_to_container = False self.data_changed.emit(self) def _undo_available(self, available): self.undo_available = available self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) def _redo_available(self, available): self.redo_available = available self.undo_redo_state_changed.emit(self.undo_available, self.redo_available) def _copy_available(self, available): self.copy_available = self.cut_available = available self.copy_available_state_changed.emit(available) def _cursor_position_changed(self, *args): self.cursor_position_changed.emit() @property def cursor_position(self): c = self.editor.textCursor() return (c.blockNumber() + 1, c.positionInBlock()) def cut(self): self.editor.cut() def copy(self): self.editor.copy() def go_to_line(self, line, col=None): self.editor.go_to_line(line, col=col) def paste(self): if not self.editor.canPaste(): return error_dialog( self, _('No text'), _('There is no suitable text in the clipboard to paste.'), show=True) self.editor.paste() def contextMenuEvent(self, ev): ev.ignore() def fix_html(self): if self.syntax == 'html': from calibre.ebooks.oeb.polish.pretty import fix_html self.editor.replace_text( fix_html(current_container(), unicode(self.editor.toPlainText())).decode('utf-8')) return True return False def pretty_print(self, name): from calibre.ebooks.oeb.polish.pretty import pretty_html, pretty_css, pretty_xml if self.syntax in {'css', 'html', 'xml'}: func = { 'css': pretty_css, 'xml': pretty_xml }.get(self.syntax, pretty_html) self.editor.replace_text( func(current_container(), name, unicode(self.editor.toPlainText())).decode('utf-8')) return True return False