Edit Book: Highlight href/src attributes and allow clicking on them while holding down the Ctrl key to jump to the linked to file

This commit is contained in:
Kovid Goyal 2014-07-09 17:59:16 +05:30
parent ac919b48ca
commit 7ab719cfcd
5 changed files with 59 additions and 13 deletions

View File

@ -8,10 +8,11 @@ __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
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)

View File

@ -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)

View File

@ -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 &lt;'),
@ -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

View File

@ -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())

View File

@ -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,12 +302,9 @@ class Editor(QMainWindow):
add_action(name, self.format_bar)
def break_cycles(self):
for x in ('modification_state_changed', 'word_ignored', 'link_clicked'):
try:
self.modification_state_changed.disconnect()
except TypeError:
pass # in case this signal was never connected
try:
self.word_ignored.disconnect()
getattr(self, x).disconnect()
except TypeError:
pass # in case this signal was never connected
self.undo_redo_state_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