mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
ac919b48ca
commit
7ab719cfcd
@ -8,10 +8,11 @@ __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
import tempfile, shutil, sys, os
|
import tempfile, shutil, sys, os
|
||||||
from functools import partial, wraps
|
from functools import partial, wraps
|
||||||
|
from urlparse import urlparse
|
||||||
|
|
||||||
from PyQt4.Qt import (
|
from PyQt4.Qt import (
|
||||||
QObject, QApplication, QDialog, QGridLayout, QLabel, QSize, Qt,
|
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 import prints, isbytestring
|
||||||
from calibre.ptempfile import PersistentTemporaryDirectory, TemporaryDirectory
|
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.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.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.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.dialogs.confirm_delete import confirm
|
||||||
from calibre.gui2.tweak_book import (
|
from calibre.gui2.tweak_book import (
|
||||||
set_current_container, current_container, tprefs, actions, editors,
|
set_current_container, current_container, tprefs, actions, editors,
|
||||||
@ -788,6 +789,21 @@ class Boss(QObject):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
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):
|
def saved_searches(self):
|
||||||
self.gui.saved_searches.show(), self.gui.saved_searches.raise_()
|
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)
|
editor.cursor_position_changed.connect(self.update_cursor_position)
|
||||||
if hasattr(editor, 'word_ignored'):
|
if hasattr(editor, 'word_ignored'):
|
||||||
editor.word_ignored.connect(self.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 data is not None:
|
||||||
if use_template:
|
if use_template:
|
||||||
editor.init_from_template(data)
|
editor.init_from_template(data)
|
||||||
|
@ -36,6 +36,7 @@ def editor_from_syntax(syntax, parent=None):
|
|||||||
SYNTAX_PROPERTY = QTextCharFormat.UserProperty
|
SYNTAX_PROPERTY = QTextCharFormat.UserProperty
|
||||||
SPELL_PROPERTY = SYNTAX_PROPERTY + 1
|
SPELL_PROPERTY = SYNTAX_PROPERTY + 1
|
||||||
SPELL_LOCALE_PROPERTY = SPELL_PROPERTY + 1
|
SPELL_LOCALE_PROPERTY = SPELL_PROPERTY + 1
|
||||||
|
LINK_PROPERTY = SPELL_LOCALE_PROPERTY + 1
|
||||||
|
|
||||||
def syntax_text_char_format(*args):
|
def syntax_text_char_format(*args):
|
||||||
ans = QTextCharFormat(*args)
|
ans = QTextCharFormat(*args)
|
||||||
|
@ -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.dictionary import parse_lang_code
|
||||||
from calibre.spell.break_iterator import split_into_words_and_positions
|
from calibre.spell.break_iterator import split_into_words_and_positions
|
||||||
from calibre.gui2.tweak_book import dictionaries, tprefs
|
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.base import SyntaxHighlighter, run_loop
|
||||||
from calibre.gui2.tweak_book.editor.syntax.css import (
|
from calibre.gui2.tweak_book.editor.syntax.css import (
|
||||||
create_formats as create_css_formats, state_map as css_state_map, CSSState, CSSUserData)
|
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)
|
pos = text.find(quote, i)
|
||||||
if pos == -1:
|
if pos == -1:
|
||||||
num = len(text) - i
|
num = len(text) - i
|
||||||
|
is_link = False
|
||||||
else:
|
else:
|
||||||
num = pos - i + 1
|
num = pos - i + 1
|
||||||
state.parse = IN_OPENING_TAG
|
state.parse = IN_OPENING_TAG
|
||||||
@ -437,6 +439,10 @@ def quoted_val(state, text, i, formats, user_data):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
add_attr_data(user_data, ATTR_VALUE, ATTR_END, i + num)
|
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'])]
|
return [(num, formats['string'])]
|
||||||
|
|
||||||
def closing_tag(state, text, i, formats, user_data):
|
def closing_tag(state, text, i, formats, user_data):
|
||||||
@ -502,6 +508,7 @@ def create_formats(highlighter, add_css=True):
|
|||||||
'preproc': t['PreProc'],
|
'preproc': t['PreProc'],
|
||||||
'nbsp': t['SpecialCharacter'],
|
'nbsp': t['SpecialCharacter'],
|
||||||
'spell': t['SpellError'],
|
'spell': t['SpellError'],
|
||||||
|
'link': t['Link'],
|
||||||
}
|
}
|
||||||
for name, msg in {
|
for name, msg in {
|
||||||
'<': _('An unescaped < is not allowed. Replace it with <'),
|
'<': _('An unescaped < is not allowed. Replace it with <'),
|
||||||
@ -520,6 +527,8 @@ def create_formats(highlighter, add_css=True):
|
|||||||
if add_css:
|
if add_css:
|
||||||
formats['css_sub_formats'] = create_css_formats(highlighter)
|
formats['css_sub_formats'] = create_css_formats(highlighter)
|
||||||
formats['spell'].setProperty(SPELL_PROPERTY, True)
|
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
|
return formats
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,11 +14,12 @@ import regex
|
|||||||
from PyQt4.Qt import (
|
from PyQt4.Qt import (
|
||||||
QPlainTextEdit, QFontDatabase, QToolTip, QPalette, QFont, QKeySequence,
|
QPlainTextEdit, QFontDatabase, QToolTip, QPalette, QFont, QKeySequence,
|
||||||
QTextEdit, QTextFormat, QWidget, QSize, QPainter, Qt, QRect, pyqtSlot,
|
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 import prepare_string_for_xml, xml_entity_to_unicode
|
||||||
from calibre.gui2.tweak_book import tprefs, TOP
|
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.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.base import SyntaxHighlighter
|
||||||
from calibre.gui2.tweak_book.editor.syntax.html import HTMLHighlighter, XMLHighlighter
|
from calibre.gui2.tweak_book.editor.syntax.html import HTMLHighlighter, XMLHighlighter
|
||||||
@ -130,6 +131,8 @@ class PlainTextEdit(QPlainTextEdit):
|
|||||||
|
|
||||||
class TextEdit(PlainTextEdit):
|
class TextEdit(PlainTextEdit):
|
||||||
|
|
||||||
|
link_clicked = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, parent=None, expected_geometry=(100, 50)):
|
def __init__(self, parent=None, expected_geometry=(100, 50)):
|
||||||
PlainTextEdit.__init__(self, parent)
|
PlainTextEdit.__init__(self, parent)
|
||||||
self.expected_geometry = expected_geometry
|
self.expected_geometry = expected_geometry
|
||||||
@ -618,6 +621,21 @@ class TextEdit(PlainTextEdit):
|
|||||||
ev.ignore()
|
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):
|
def get_range_inside_tag(self):
|
||||||
c = self.textCursor()
|
c = self.textCursor()
|
||||||
left = min(c.anchor(), c.position())
|
left = min(c.anchor(), c.position())
|
||||||
|
@ -105,6 +105,7 @@ class Editor(QMainWindow):
|
|||||||
data_changed = pyqtSignal(object)
|
data_changed = pyqtSignal(object)
|
||||||
cursor_position_changed = pyqtSignal()
|
cursor_position_changed = pyqtSignal()
|
||||||
word_ignored = pyqtSignal(object, object)
|
word_ignored = pyqtSignal(object, object)
|
||||||
|
link_clicked = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, syntax, parent=None):
|
def __init__(self, syntax, parent=None):
|
||||||
QMainWindow.__init__(self, parent)
|
QMainWindow.__init__(self, parent)
|
||||||
@ -126,6 +127,7 @@ class Editor(QMainWindow):
|
|||||||
self.editor.textChanged.connect(self._data_changed)
|
self.editor.textChanged.connect(self._data_changed)
|
||||||
self.editor.copyAvailable.connect(self._copy_available)
|
self.editor.copyAvailable.connect(self._copy_available)
|
||||||
self.editor.cursorPositionChanged.connect(self._cursor_position_changed)
|
self.editor.cursorPositionChanged.connect(self._cursor_position_changed)
|
||||||
|
self.editor.link_clicked.connect(self.link_clicked)
|
||||||
|
|
||||||
@dynamic_property
|
@dynamic_property
|
||||||
def current_line(self):
|
def current_line(self):
|
||||||
@ -300,14 +302,11 @@ class Editor(QMainWindow):
|
|||||||
add_action(name, self.format_bar)
|
add_action(name, self.format_bar)
|
||||||
|
|
||||||
def break_cycles(self):
|
def break_cycles(self):
|
||||||
try:
|
for x in ('modification_state_changed', 'word_ignored', 'link_clicked'):
|
||||||
self.modification_state_changed.disconnect()
|
try:
|
||||||
except TypeError:
|
getattr(self, x).disconnect()
|
||||||
pass # in case this signal was never connected
|
except TypeError:
|
||||||
try:
|
pass # in case this signal was never connected
|
||||||
self.word_ignored.disconnect()
|
|
||||||
except TypeError:
|
|
||||||
pass # in case this signal was never connected
|
|
||||||
self.undo_redo_state_changed.disconnect()
|
self.undo_redo_state_changed.disconnect()
|
||||||
self.copy_available_state_changed.disconnect()
|
self.copy_available_state_changed.disconnect()
|
||||||
self.cursor_position_changed.disconnect()
|
self.cursor_position_changed.disconnect()
|
||||||
@ -318,6 +317,7 @@ class Editor(QMainWindow):
|
|||||||
self.editor.textChanged.disconnect()
|
self.editor.textChanged.disconnect()
|
||||||
self.editor.copyAvailable.disconnect()
|
self.editor.copyAvailable.disconnect()
|
||||||
self.editor.cursorPositionChanged.disconnect()
|
self.editor.cursorPositionChanged.disconnect()
|
||||||
|
self.editor.link_clicked.disconnect()
|
||||||
self.editor.setPlainText('')
|
self.editor.setPlainText('')
|
||||||
self.editor.smarts = None
|
self.editor.smarts = None
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user