mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Live updates for the preview panel
This commit is contained in:
parent
53b164d7fb
commit
13e4a21ed8
@ -368,6 +368,13 @@ class Container(object): # {{{
|
||||
data = self.parse_css(data, self.relpath(path))
|
||||
return data
|
||||
|
||||
def raw_data(self, name, decode=True):
|
||||
ans = self.open(name).read()
|
||||
mime = self.mime_map.get(name, guess_type(name))
|
||||
if decode and (mime in OEB_STYLES or mime in OEB_DOCS or mime[-4:] in {'+xml', '/xml'}):
|
||||
ans = self.decode(ans)
|
||||
return ans
|
||||
|
||||
def parse_css(self, data, fname):
|
||||
from cssutils import CSSParser, log
|
||||
log.setLevel(logging.WARN)
|
||||
|
@ -13,6 +13,7 @@ tprefs.defaults['editor_theme'] = None
|
||||
tprefs.defaults['editor_font_family'] = None
|
||||
tprefs.defaults['editor_font_size'] = 12
|
||||
tprefs.defaults['editor_line_wrap'] = True
|
||||
tprefs.defaults['preview_refresh_time'] = 2
|
||||
|
||||
_current_container = None
|
||||
|
||||
|
@ -24,6 +24,7 @@ from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.gui2.tweak_book import set_current_container, current_container, tprefs, actions, editors
|
||||
from calibre.gui2.tweak_book.undo import GlobalUndoHistory
|
||||
from calibre.gui2.tweak_book.save import SaveManager
|
||||
from calibre.gui2.tweak_book.preview import parse_worker
|
||||
from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime
|
||||
|
||||
def get_container(*args, **kwargs):
|
||||
@ -232,6 +233,7 @@ class Boss(QObject):
|
||||
if editor is None:
|
||||
editor = editors[name] = editor_from_syntax(syntax, self.gui.editor_tabs)
|
||||
editor.undo_redo_state_changed.connect(self.editor_undo_redo_state_changed)
|
||||
editor.data_changed.connect(self.editor_data_changed)
|
||||
c = current_container()
|
||||
with c.open(name) as f:
|
||||
editor.data = c.decode(f.read())
|
||||
@ -260,6 +262,9 @@ class Boss(QObject):
|
||||
if ed is not None:
|
||||
ed.redo()
|
||||
|
||||
def editor_data_changed(self, editor):
|
||||
self.gui.preview.refresh_timer.start(tprefs['preview_refresh_time'] * 1000)
|
||||
|
||||
def editor_undo_redo_state_changed(self, *args):
|
||||
self.apply_current_editor_state(update_keymap=False)
|
||||
|
||||
@ -280,7 +285,7 @@ class Boss(QObject):
|
||||
if ed is x:
|
||||
name = n
|
||||
break
|
||||
if name is not None:
|
||||
if name is not None and getattr(ed, 'syntax', None) == 'html':
|
||||
self.gui.preview.show(name)
|
||||
else:
|
||||
self.gui.keyboard.set_mode('other')
|
||||
@ -392,9 +397,10 @@ class Boss(QObject):
|
||||
QApplication.instance().quit()
|
||||
|
||||
def shutdown(self):
|
||||
self.gui.preview.refresh_timer.stop()
|
||||
self.save_state()
|
||||
self.save_manager.shutdown()
|
||||
self.gui.preview.parse_worker.shutdown()
|
||||
parse_worker.shutdown()
|
||||
self.save_manager.wait(0.1)
|
||||
|
||||
def save_state(self):
|
||||
|
@ -16,6 +16,7 @@ class Editor(QMainWindow):
|
||||
|
||||
modification_state_changed = pyqtSignal(object)
|
||||
undo_redo_state_changed = pyqtSignal(object, object)
|
||||
data_changed = pyqtSignal(object)
|
||||
|
||||
def __init__(self, syntax, parent=None):
|
||||
QMainWindow.__init__(self, parent)
|
||||
@ -30,6 +31,10 @@ class Editor(QMainWindow):
|
||||
self.redo_available = False
|
||||
self.editor.undoAvailable.connect(self._undo_available)
|
||||
self.editor.redoAvailable.connect(self._redo_available)
|
||||
self.editor.textChanged.connect(self._data_changed)
|
||||
|
||||
def _data_changed(self):
|
||||
self.data_changed.emit(self)
|
||||
|
||||
def _undo_available(self, available):
|
||||
self.undo_available = available
|
||||
@ -50,6 +55,9 @@ class Editor(QMainWindow):
|
||||
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 undo(self):
|
||||
self.editor.undo()
|
||||
|
||||
@ -74,9 +82,11 @@ class Editor(QMainWindow):
|
||||
def break_cycles(self):
|
||||
self.modification_state_changed.disconnect()
|
||||
self.undo_redo_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.setPlainText('')
|
||||
|
||||
def launch_editor(path_to_edit, path_is_raw=False, syntax='html'):
|
||||
|
@ -12,14 +12,14 @@ from Queue import Queue, Empty
|
||||
|
||||
from PyQt4.Qt import (
|
||||
QWidget, QVBoxLayout, QApplication, QSize, QNetworkAccessManager,
|
||||
QNetworkReply, QTimer, QNetworkRequest, QUrl)
|
||||
QNetworkReply, QTimer, QNetworkRequest, QUrl, Qt, QNetworkDiskCache)
|
||||
from PyQt4.QtWebKit import QWebView
|
||||
|
||||
from calibre import prints
|
||||
from calibre.constants import iswindows
|
||||
from calibre.ebooks.oeb.polish.parsing import parse
|
||||
from calibre.ebooks.oeb.base import serialize
|
||||
from calibre.gui2 import Dispatcher
|
||||
from calibre.ebooks.oeb.base import serialize, OEB_DOCS
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
from calibre.gui2.tweak_book import current_container, editors
|
||||
from calibre.gui2.viewer.documentview import apply_settings
|
||||
from calibre.gui2.viewer.config import config
|
||||
@ -27,29 +27,43 @@ from calibre.utils.ipc.simple_worker import offload_worker
|
||||
|
||||
shutdown = object()
|
||||
|
||||
def get_data(name):
|
||||
'Get the data for name. Returns a unicode string if name is a text document/stylesheet'
|
||||
if name in editors:
|
||||
return editors[name].get_raw_data()
|
||||
return current_container().raw_data(name)
|
||||
|
||||
# Parsing of html to add linenumbers {{{
|
||||
def parse_html(raw):
|
||||
root = parse(raw, decoder=lambda x:x.decode('utf-8'), replace_entities=False, line_numbers=True, linenumber_attribute='lnum')
|
||||
return serialize(root, 'text/html').decode('utf-8')
|
||||
root = parse(raw, decoder=lambda x:x.decode('utf-8'), line_numbers=True, linenumber_attribute='lnum')
|
||||
return serialize(root, 'text/html').encode('utf-8')
|
||||
|
||||
class ParseItem(object):
|
||||
|
||||
__slots__ = ('name', 'length', 'fingerprint', 'parsed_data')
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.length, self.fingerprint = 0, None
|
||||
self.parsed_data = None
|
||||
|
||||
class ParseWorker(Thread):
|
||||
|
||||
daemon = True
|
||||
SLEEP_TIME = 1
|
||||
|
||||
def __init__(self, callback=lambda x, y: None):
|
||||
def __init__(self):
|
||||
Thread.__init__(self)
|
||||
self.worker = offload_worker(priority='low')
|
||||
self.requests = Queue()
|
||||
self.request_count = 0
|
||||
self.start()
|
||||
self.cache = {}
|
||||
self.callback = callback
|
||||
self.parse_items = {}
|
||||
|
||||
def run(self):
|
||||
mod, func = 'calibre.gui2.tweak_book.preview', 'parse_html'
|
||||
try:
|
||||
# Connect to the worker and send a dummy job to initialize it
|
||||
self.worker(mod, func, b'<p></p>')
|
||||
self.worker = offload_worker(priority='low')
|
||||
self.worker(mod, func, '<p></p>')
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
@ -69,12 +83,7 @@ class ParseWorker(Thread):
|
||||
break
|
||||
request = sorted(requests, reverse=True)[0]
|
||||
del requests
|
||||
name, data = request[1:]
|
||||
old_len, old_fp, old_parsed = self.cache.get(name, (None, None, None))
|
||||
length, fp = len(data), hash(data)
|
||||
if length == old_len and fp == old_fp:
|
||||
self.done(name, old_parsed)
|
||||
continue
|
||||
pi, data = request[1:]
|
||||
try:
|
||||
res = self.worker(mod, func, data)
|
||||
except:
|
||||
@ -86,45 +95,75 @@ class ParseWorker(Thread):
|
||||
prints("Parser error:")
|
||||
prints(res['tb'])
|
||||
else:
|
||||
self.cache[name] = (length, fp, parsed_data)
|
||||
self.done(name, parsed_data)
|
||||
|
||||
def done(self, name, data):
|
||||
try:
|
||||
self.callback(name, data)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
pi.parsed_data = parsed_data
|
||||
|
||||
def add_request(self, name):
|
||||
data = get_data(name)
|
||||
self.requests.put((self.request_count, name, data))
|
||||
ldata, hdata = len(data), hash(data)
|
||||
pi = self.parse_items.get(name, None)
|
||||
if pi is None:
|
||||
self.parse_items[name] = pi = ParseItem(name)
|
||||
else:
|
||||
if pi.length == ldata and pi.fingerprint == hdata:
|
||||
return
|
||||
pi.parsed_data = None
|
||||
pi.length, pi.fingerprint = ldata, hdata
|
||||
self.requests.put((self.request_count, pi, data))
|
||||
self.request_count += 1
|
||||
|
||||
def shutdown(self):
|
||||
self.requests.put(shutdown)
|
||||
|
||||
def get_data(self, name):
|
||||
return getattr(self.parse_items.get(name, None), 'parsed_data', None)
|
||||
|
||||
class LocalNetworkReply(QNetworkReply):
|
||||
parse_worker = ParseWorker()
|
||||
# }}}
|
||||
|
||||
def __init__(self, parent, request, mime_type, data):
|
||||
# Override network access to load data "live" from the editors {{{
|
||||
class NetworkReply(QNetworkReply):
|
||||
|
||||
def __init__(self, parent, request, mime_type, name):
|
||||
QNetworkReply.__init__(self, parent)
|
||||
self.setOpenMode(QNetworkReply.ReadOnly | QNetworkReply.Unbuffered)
|
||||
self.__data = data
|
||||
self.setRequest(request)
|
||||
self.setUrl(request.url())
|
||||
self.setHeader(QNetworkRequest.ContentTypeHeader, mime_type)
|
||||
self._aborted = False
|
||||
if mime_type in OEB_DOCS:
|
||||
self.resource_name = name
|
||||
QTimer.singleShot(0, self.check_for_parse)
|
||||
else:
|
||||
data = get_data(name)
|
||||
if isinstance(data, type('')):
|
||||
data = data.encode('utf-8')
|
||||
mime_type += '; charset=utf-8'
|
||||
self.__data = data
|
||||
self.setHeader(QNetworkRequest.ContentTypeHeader, mime_type)
|
||||
self.setHeader(QNetworkRequest.ContentLengthHeader, len(self.__data))
|
||||
QTimer.singleShot(0, self.finalize_reply)
|
||||
|
||||
def check_for_parse(self):
|
||||
if self._aborted:
|
||||
return
|
||||
data = parse_worker.get_data(self.resource_name)
|
||||
if data is None:
|
||||
return QTimer.singleShot(10, self.check_for_parse)
|
||||
self.__data = data
|
||||
self.setHeader(QNetworkRequest.ContentTypeHeader, 'text/html; charset=utf-8')
|
||||
self.setHeader(QNetworkRequest.ContentLengthHeader, len(self.__data))
|
||||
QTimer.singleShot(0, self.finalize_reply)
|
||||
self.finalize_reply()
|
||||
|
||||
def bytesAvailable(self):
|
||||
return len(self.__data)
|
||||
try:
|
||||
return len(self.__data)
|
||||
except AttributeError:
|
||||
return 0
|
||||
|
||||
def isSequential(self):
|
||||
return True
|
||||
|
||||
def abort(self):
|
||||
pass
|
||||
self._aborted = True
|
||||
|
||||
def readData(self, maxlen):
|
||||
ans, self.__data = self.__data[:maxlen], self.__data[maxlen:]
|
||||
@ -132,6 +171,8 @@ class LocalNetworkReply(QNetworkReply):
|
||||
read = readData
|
||||
|
||||
def finalize_reply(self):
|
||||
if self._aborted:
|
||||
return
|
||||
self.setFinished(True)
|
||||
self.setAttribute(QNetworkRequest.HttpStatusCodeAttribute, 200)
|
||||
self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, "Ok")
|
||||
@ -140,10 +181,6 @@ class LocalNetworkReply(QNetworkReply):
|
||||
self.readyRead.emit()
|
||||
self.finished.emit()
|
||||
|
||||
def get_data(name):
|
||||
if name in editors:
|
||||
return editors[name].data
|
||||
return current_container().open(name).read()
|
||||
|
||||
class NetworkAccessManager(QNetworkAccessManager):
|
||||
|
||||
@ -152,9 +189,16 @@ class NetworkAccessManager(QNetworkAccessManager):
|
||||
'Custom')
|
||||
}
|
||||
|
||||
def __init__(self, *args):
|
||||
QNetworkAccessManager.__init__(self, *args)
|
||||
self.cache = QNetworkDiskCache(self)
|
||||
self.setCache(self.cache)
|
||||
self.cache.setCacheDirectory(PersistentTemporaryDirectory(prefix='disk_cache_'))
|
||||
self.cache.setMaximumCacheSize(0)
|
||||
|
||||
def createRequest(self, operation, request, data):
|
||||
url = unicode(request.url().toString())
|
||||
if url.startswith('file://'):
|
||||
if operation == self.GetOperation and url.startswith('file://'):
|
||||
path = url[7:]
|
||||
if iswindows and path.startswith('/'):
|
||||
path = path[1:]
|
||||
@ -162,13 +206,13 @@ class NetworkAccessManager(QNetworkAccessManager):
|
||||
name = c.abspath_to_name(path)
|
||||
if c.has_name(name):
|
||||
try:
|
||||
return LocalNetworkReply(self, request, c.mime_map.get(name, 'application/octet-stream'),
|
||||
get_data(name) if operation == self.GetOperation else b'')
|
||||
return NetworkReply(self, request, c.mime_map.get(name, 'application/octet-stream'), name)
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
return QNetworkAccessManager.createRequest(self, operation, request,
|
||||
data)
|
||||
traceback.print_exc()
|
||||
return QNetworkAccessManager.createRequest(self, operation, request, data)
|
||||
|
||||
# }}}
|
||||
|
||||
class WebView(QWebView):
|
||||
|
||||
@ -195,6 +239,20 @@ class WebView(QWebView):
|
||||
def sizeHint(self):
|
||||
return self._size_hint
|
||||
|
||||
def refresh(self):
|
||||
self.pageAction(self.page().Reload).trigger()
|
||||
|
||||
@dynamic_property
|
||||
def scroll_pos(self):
|
||||
def fget(self):
|
||||
mf = self.page().mainFrame()
|
||||
return (mf.scrollBarValue(Qt.Horizontal), mf.scrollBarValue(Qt.Vertical))
|
||||
def fset(self, val):
|
||||
mf = self.page().mainFrame()
|
||||
mf.setScrollBarValue(Qt.Horizontal, val[0])
|
||||
mf.setScrollBarValue(Qt.Vertical, val[1])
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
class Preview(QWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
@ -202,21 +260,27 @@ class Preview(QWidget):
|
||||
self.l = l = QVBoxLayout()
|
||||
self.setLayout(l)
|
||||
l.setContentsMargins(0, 0, 0, 0)
|
||||
self.parse_worker = ParseWorker(callback=Dispatcher(self.parsing_done))
|
||||
self.view = WebView(self)
|
||||
l.addWidget(self.view)
|
||||
|
||||
self.current_name = None
|
||||
self.parse_pending = False
|
||||
self.last_sync_request = None
|
||||
self.refresh_timer = QTimer(self)
|
||||
self.refresh_timer.timeout.connect(self.refresh)
|
||||
parse_worker.start()
|
||||
|
||||
def show(self, name):
|
||||
self.current_name, self.parse_pending = name, True
|
||||
self.parse_worker.add_request(name)
|
||||
if name != self.current_name:
|
||||
self.refresh_timer.stop()
|
||||
self.current_name = name
|
||||
parse_worker.add_request(name)
|
||||
self.view.setUrl(QUrl.fromLocalFile(current_container().name_to_abspath(name)))
|
||||
|
||||
def parsing_done(self, name, data):
|
||||
if name == self.current_name:
|
||||
c = current_container()
|
||||
self.view.setHtml(data, QUrl.fromLocalFile(c.name_to_abspath(name)))
|
||||
self.parse_pending = False
|
||||
def refresh(self):
|
||||
if self.current_name:
|
||||
# This will check if the current html has changed in its editor,
|
||||
# and re-parse it if so
|
||||
parse_worker.add_request(self.current_name)
|
||||
# Tell webkit to reload all html and associated resources
|
||||
self.view.refresh()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user