diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 84c085a689..9e2e00597d 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -8,10 +8,11 @@ __copyright__ = '2013, Kovid Goyal ' import tempfile, shutil, sys, os from functools import partial, wraps +from urlparse import urlparse from PyQt4.Qt import ( QObject, QApplication, QDialog, QGridLayout, QLabel, QSize, Qt, - QDialogButtonBox, QIcon, QTimer, QPixmap, QInputDialog) + QDialogButtonBox, QIcon, QTimer, QPixmap, QInputDialog, QUrl) from calibre import prints, isbytestring from calibre.ptempfile import PersistentTemporaryDirectory, TemporaryDirectory @@ -25,7 +26,7 @@ from calibre.ebooks.oeb.polish.replace import rename_files, replace_file, get_re from calibre.ebooks.oeb.polish.split import split, merge, AbortError, multisplit from calibre.ebooks.oeb.polish.toc import remove_names_from_toc, find_existing_toc, create_inline_toc from calibre.ebooks.oeb.polish.utils import link_stylesheets, setup_cssutils_serialization as scs -from calibre.gui2 import error_dialog, choose_files, question_dialog, info_dialog, choose_save_file +from calibre.gui2 import error_dialog, choose_files, question_dialog, info_dialog, choose_save_file, open_url from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.tweak_book import ( set_current_container, current_container, tprefs, actions, editors, @@ -788,6 +789,21 @@ class Boss(QObject): except AttributeError: pass + def editor_link_clicked(self, url): + ed = self.gui.central.current_editor + name = editor_name(ed) + target = current_container().href_to_name(url, name) + frag = url.partition('#')[-1] + if current_container().has_name(target): + self.link_clicked(target, frag) + else: + purl = urlparse(url) + if purl.scheme not in {'', 'file'}: + open_url(QUrl(url)) + else: + error_dialog(self, _('Not found'), _( + 'No file with the name %s was found in the book') % target, show=True) + def saved_searches(self): self.gui.saved_searches.show(), self.gui.saved_searches.raise_() @@ -1092,6 +1108,8 @@ class Boss(QObject): editor.cursor_position_changed.connect(self.update_cursor_position) if hasattr(editor, 'word_ignored'): editor.word_ignored.connect(self.word_ignored) + if hasattr(editor, 'link_clicked'): + editor.link_clicked.connect(self.editor_link_clicked) if data is not None: if use_template: editor.init_from_template(data) diff --git a/src/calibre/gui2/tweak_book/editor/__init__.py b/src/calibre/gui2/tweak_book/editor/__init__.py index d810122bc7..67f3ef1b23 100644 --- a/src/calibre/gui2/tweak_book/editor/__init__.py +++ b/src/calibre/gui2/tweak_book/editor/__init__.py @@ -36,6 +36,7 @@ def editor_from_syntax(syntax, parent=None): SYNTAX_PROPERTY = QTextCharFormat.UserProperty SPELL_PROPERTY = SYNTAX_PROPERTY + 1 SPELL_LOCALE_PROPERTY = SPELL_PROPERTY + 1 +LINK_PROPERTY = SPELL_LOCALE_PROPERTY + 1 def syntax_text_char_format(*args): ans = QTextCharFormat(*args) diff --git a/src/calibre/gui2/tweak_book/editor/syntax/html.py b/src/calibre/gui2/tweak_book/editor/syntax/html.py index a1a03d23f8..1102b21cee 100644 --- a/src/calibre/gui2/tweak_book/editor/syntax/html.py +++ b/src/calibre/gui2/tweak_book/editor/syntax/html.py @@ -16,7 +16,8 @@ from calibre.ebooks.oeb.polish.spell import html_spell_tags, xml_spell_tags from calibre.spell.dictionary import parse_lang_code from calibre.spell.break_iterator import split_into_words_and_positions from calibre.gui2.tweak_book import dictionaries, tprefs -from calibre.gui2.tweak_book.editor import syntax_text_char_format, SPELL_PROPERTY, SPELL_LOCALE_PROPERTY, store_locale +from calibre.gui2.tweak_book.editor import ( + syntax_text_char_format, SPELL_PROPERTY, SPELL_LOCALE_PROPERTY, store_locale, LINK_PROPERTY) from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter, run_loop from calibre.gui2.tweak_book.editor.syntax.css import ( create_formats as create_css_formats, state_map as css_state_map, CSSState, CSSUserData) @@ -428,6 +429,7 @@ def quoted_val(state, text, i, formats, user_data): pos = text.find(quote, i) if pos == -1: num = len(text) - i + is_link = False else: num = pos - i + 1 state.parse = IN_OPENING_TAG @@ -437,6 +439,10 @@ def quoted_val(state, text, i, formats, user_data): except ValueError: pass add_attr_data(user_data, ATTR_VALUE, ATTR_END, i + num) + is_link = state.attribute_name in {'href', 'src'} + + if is_link: + return [(num - 1, formats['link']), (1, formats['string'])] return [(num, formats['string'])] def closing_tag(state, text, i, formats, user_data): @@ -502,6 +508,7 @@ def create_formats(highlighter, add_css=True): 'preproc': t['PreProc'], 'nbsp': t['SpecialCharacter'], 'spell': t['SpellError'], + 'link': t['Link'], } for name, msg in { '<': _('An unescaped < is not allowed. Replace it with <'), @@ -520,6 +527,8 @@ def create_formats(highlighter, add_css=True): if add_css: formats['css_sub_formats'] = create_css_formats(highlighter) formats['spell'].setProperty(SPELL_PROPERTY, True) + formats['link'].setProperty(LINK_PROPERTY, True) + formats['link'].setToolTip(_('Hold down the Ctrl key and click to open this link')) return formats diff --git a/src/calibre/gui2/tweak_book/editor/text.py b/src/calibre/gui2/tweak_book/editor/text.py index 28327cabd4..4319936ffd 100644 --- a/src/calibre/gui2/tweak_book/editor/text.py +++ b/src/calibre/gui2/tweak_book/editor/text.py @@ -14,11 +14,12 @@ import regex from PyQt4.Qt import ( QPlainTextEdit, QFontDatabase, QToolTip, QPalette, QFont, QKeySequence, QTextEdit, QTextFormat, QWidget, QSize, QPainter, Qt, QRect, pyqtSlot, - QApplication, QMimeData, QColor, QColorDialog, QTimer) + QApplication, QMimeData, QColor, QColorDialog, QTimer, pyqtSignal) from calibre import prepare_string_for_xml, xml_entity_to_unicode from calibre.gui2.tweak_book import tprefs, TOP -from calibre.gui2.tweak_book.editor import SYNTAX_PROPERTY, SPELL_PROPERTY, SPELL_LOCALE_PROPERTY, store_locale +from calibre.gui2.tweak_book.editor import ( + SYNTAX_PROPERTY, SPELL_PROPERTY, SPELL_LOCALE_PROPERTY, store_locale, LINK_PROPERTY) from calibre.gui2.tweak_book.editor.themes import get_theme, theme_color, theme_format from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter from calibre.gui2.tweak_book.editor.syntax.html import HTMLHighlighter, XMLHighlighter @@ -130,6 +131,8 @@ class PlainTextEdit(QPlainTextEdit): class TextEdit(PlainTextEdit): + link_clicked = pyqtSignal(object) + def __init__(self, parent=None, expected_geometry=(100, 50)): PlainTextEdit.__init__(self, parent) self.expected_geometry = expected_geometry @@ -618,6 +621,21 @@ class TextEdit(PlainTextEdit): ev.ignore() # }}} + def link_for_position(self, pos): + c = self.cursorForPosition(pos) + r = self.syntax_range_for_cursor(c) + if r is not None and r.format.property(LINK_PROPERTY).toBool(): + return self.text_for_range(c.block(), r) + + def mousePressEvent(self, ev): + if ev.modifiers() & Qt.CTRL: + url = self.link_for_position(ev.pos()) + if url is not None: + ev.accept() + self.link_clicked.emit(url) + return + return PlainTextEdit.mousePressEvent(self, ev) + def get_range_inside_tag(self): c = self.textCursor() left = min(c.anchor(), c.position()) diff --git a/src/calibre/gui2/tweak_book/editor/widget.py b/src/calibre/gui2/tweak_book/editor/widget.py index 84d5fd8d14..3d065bfe4d 100644 --- a/src/calibre/gui2/tweak_book/editor/widget.py +++ b/src/calibre/gui2/tweak_book/editor/widget.py @@ -105,6 +105,7 @@ class Editor(QMainWindow): data_changed = pyqtSignal(object) cursor_position_changed = pyqtSignal() word_ignored = pyqtSignal(object, object) + link_clicked = pyqtSignal(object) def __init__(self, syntax, parent=None): QMainWindow.__init__(self, parent) @@ -126,6 +127,7 @@ class Editor(QMainWindow): 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) @dynamic_property def current_line(self): @@ -300,14 +302,11 @@ class Editor(QMainWindow): 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 + for x in ('modification_state_changed', 'word_ignored', 'link_clicked'): + 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() @@ -318,6 +317,7 @@ class Editor(QMainWindow): self.editor.textChanged.disconnect() self.editor.copyAvailable.disconnect() self.editor.cursorPositionChanged.disconnect() + self.editor.link_clicked.disconnect() self.editor.setPlainText('') self.editor.smarts = None