Live updates for the preview panel

This commit is contained in:
Kovid Goyal 2013-11-07 17:33:18 +05:30
parent 53b164d7fb
commit 13e4a21ed8
5 changed files with 143 additions and 55 deletions

View File

@ -368,6 +368,13 @@ class Container(object): # {{{
data = self.parse_css(data, self.relpath(path)) data = self.parse_css(data, self.relpath(path))
return data 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): def parse_css(self, data, fname):
from cssutils import CSSParser, log from cssutils import CSSParser, log
log.setLevel(logging.WARN) log.setLevel(logging.WARN)

View File

@ -13,6 +13,7 @@ tprefs.defaults['editor_theme'] = None
tprefs.defaults['editor_font_family'] = None tprefs.defaults['editor_font_family'] = None
tprefs.defaults['editor_font_size'] = 12 tprefs.defaults['editor_font_size'] = 12
tprefs.defaults['editor_line_wrap'] = True tprefs.defaults['editor_line_wrap'] = True
tprefs.defaults['preview_refresh_time'] = 2
_current_container = None _current_container = None

View File

@ -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 import set_current_container, current_container, tprefs, actions, editors
from calibre.gui2.tweak_book.undo import GlobalUndoHistory from calibre.gui2.tweak_book.undo import GlobalUndoHistory
from calibre.gui2.tweak_book.save import SaveManager 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 from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime
def get_container(*args, **kwargs): def get_container(*args, **kwargs):
@ -232,6 +233,7 @@ class Boss(QObject):
if editor is None: if editor is None:
editor = editors[name] = editor_from_syntax(syntax, self.gui.editor_tabs) editor = editors[name] = editor_from_syntax(syntax, self.gui.editor_tabs)
editor.undo_redo_state_changed.connect(self.editor_undo_redo_state_changed) editor.undo_redo_state_changed.connect(self.editor_undo_redo_state_changed)
editor.data_changed.connect(self.editor_data_changed)
c = current_container() c = current_container()
with c.open(name) as f: with c.open(name) as f:
editor.data = c.decode(f.read()) editor.data = c.decode(f.read())
@ -260,6 +262,9 @@ class Boss(QObject):
if ed is not None: if ed is not None:
ed.redo() 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): def editor_undo_redo_state_changed(self, *args):
self.apply_current_editor_state(update_keymap=False) self.apply_current_editor_state(update_keymap=False)
@ -280,7 +285,7 @@ class Boss(QObject):
if ed is x: if ed is x:
name = n name = n
break break
if name is not None: if name is not None and getattr(ed, 'syntax', None) == 'html':
self.gui.preview.show(name) self.gui.preview.show(name)
else: else:
self.gui.keyboard.set_mode('other') self.gui.keyboard.set_mode('other')
@ -392,9 +397,10 @@ class Boss(QObject):
QApplication.instance().quit() QApplication.instance().quit()
def shutdown(self): def shutdown(self):
self.gui.preview.refresh_timer.stop()
self.save_state() self.save_state()
self.save_manager.shutdown() self.save_manager.shutdown()
self.gui.preview.parse_worker.shutdown() parse_worker.shutdown()
self.save_manager.wait(0.1) self.save_manager.wait(0.1)
def save_state(self): def save_state(self):

View File

@ -16,6 +16,7 @@ class Editor(QMainWindow):
modification_state_changed = pyqtSignal(object) modification_state_changed = pyqtSignal(object)
undo_redo_state_changed = pyqtSignal(object, object) undo_redo_state_changed = pyqtSignal(object, object)
data_changed = pyqtSignal(object)
def __init__(self, syntax, parent=None): def __init__(self, syntax, parent=None):
QMainWindow.__init__(self, parent) QMainWindow.__init__(self, parent)
@ -30,6 +31,10 @@ class Editor(QMainWindow):
self.redo_available = False self.redo_available = False
self.editor.undoAvailable.connect(self._undo_available) self.editor.undoAvailable.connect(self._undo_available)
self.editor.redoAvailable.connect(self._redo_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): def _undo_available(self, available):
self.undo_available = available self.undo_available = available
@ -50,6 +55,9 @@ class Editor(QMainWindow):
self.editor.load_text(val, syntax=self.syntax) self.editor.load_text(val, syntax=self.syntax)
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def get_raw_data(self):
return unicode(self.editor.toPlainText())
def undo(self): def undo(self):
self.editor.undo() self.editor.undo()
@ -74,9 +82,11 @@ class Editor(QMainWindow):
def break_cycles(self): def break_cycles(self):
self.modification_state_changed.disconnect() self.modification_state_changed.disconnect()
self.undo_redo_state_changed.disconnect() self.undo_redo_state_changed.disconnect()
self.data_changed.disconnect()
self.editor.undoAvailable.disconnect() self.editor.undoAvailable.disconnect()
self.editor.redoAvailable.disconnect() self.editor.redoAvailable.disconnect()
self.editor.modificationChanged.disconnect() self.editor.modificationChanged.disconnect()
self.editor.textChanged.disconnect()
self.editor.setPlainText('') self.editor.setPlainText('')
def launch_editor(path_to_edit, path_is_raw=False, syntax='html'): def launch_editor(path_to_edit, path_is_raw=False, syntax='html'):

View File

@ -12,14 +12,14 @@ from Queue import Queue, Empty
from PyQt4.Qt import ( from PyQt4.Qt import (
QWidget, QVBoxLayout, QApplication, QSize, QNetworkAccessManager, QWidget, QVBoxLayout, QApplication, QSize, QNetworkAccessManager,
QNetworkReply, QTimer, QNetworkRequest, QUrl) QNetworkReply, QTimer, QNetworkRequest, QUrl, Qt, QNetworkDiskCache)
from PyQt4.QtWebKit import QWebView from PyQt4.QtWebKit import QWebView
from calibre import prints from calibre import prints
from calibre.constants import iswindows from calibre.constants import iswindows
from calibre.ebooks.oeb.polish.parsing import parse from calibre.ebooks.oeb.polish.parsing import parse
from calibre.ebooks.oeb.base import serialize from calibre.ebooks.oeb.base import serialize, OEB_DOCS
from calibre.gui2 import Dispatcher from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.gui2.tweak_book import current_container, editors from calibre.gui2.tweak_book import current_container, editors
from calibre.gui2.viewer.documentview import apply_settings from calibre.gui2.viewer.documentview import apply_settings
from calibre.gui2.viewer.config import config from calibre.gui2.viewer.config import config
@ -27,29 +27,43 @@ from calibre.utils.ipc.simple_worker import offload_worker
shutdown = object() 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): def parse_html(raw):
root = parse(raw, decoder=lambda x:x.decode('utf-8'), replace_entities=False, line_numbers=True, linenumber_attribute='lnum') root = parse(raw, decoder=lambda x:x.decode('utf-8'), line_numbers=True, linenumber_attribute='lnum')
return serialize(root, 'text/html').decode('utf-8') 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): class ParseWorker(Thread):
daemon = True daemon = True
SLEEP_TIME = 1 SLEEP_TIME = 1
def __init__(self, callback=lambda x, y: None): def __init__(self):
Thread.__init__(self) Thread.__init__(self)
self.worker = offload_worker(priority='low')
self.requests = Queue() self.requests = Queue()
self.request_count = 0 self.request_count = 0
self.start() self.parse_items = {}
self.cache = {}
self.callback = callback
def run(self): def run(self):
mod, func = 'calibre.gui2.tweak_book.preview', 'parse_html' mod, func = 'calibre.gui2.tweak_book.preview', 'parse_html'
try: try:
# Connect to the worker and send a dummy job to initialize it # 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: except:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
@ -69,12 +83,7 @@ class ParseWorker(Thread):
break break
request = sorted(requests, reverse=True)[0] request = sorted(requests, reverse=True)[0]
del requests del requests
name, data = request[1:] pi, 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
try: try:
res = self.worker(mod, func, data) res = self.worker(mod, func, data)
except: except:
@ -86,45 +95,75 @@ class ParseWorker(Thread):
prints("Parser error:") prints("Parser error:")
prints(res['tb']) prints(res['tb'])
else: else:
self.cache[name] = (length, fp, parsed_data) pi.parsed_data = parsed_data
self.done(name, parsed_data)
def done(self, name, data):
try:
self.callback(name, data)
except Exception:
import traceback
traceback.print_exc()
def add_request(self, name): def add_request(self, name):
data = get_data(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 self.request_count += 1
def shutdown(self): def shutdown(self):
self.requests.put(shutdown) 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) QNetworkReply.__init__(self, parent)
self.setOpenMode(QNetworkReply.ReadOnly | QNetworkReply.Unbuffered) self.setOpenMode(QNetworkReply.ReadOnly | QNetworkReply.Unbuffered)
self.__data = data
self.setRequest(request) self.setRequest(request)
self.setUrl(request.url()) 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)) self.setHeader(QNetworkRequest.ContentLengthHeader, len(self.__data))
QTimer.singleShot(0, self.finalize_reply) self.finalize_reply()
def bytesAvailable(self): def bytesAvailable(self):
return len(self.__data) try:
return len(self.__data)
except AttributeError:
return 0
def isSequential(self): def isSequential(self):
return True return True
def abort(self): def abort(self):
pass self._aborted = True
def readData(self, maxlen): def readData(self, maxlen):
ans, self.__data = self.__data[:maxlen], self.__data[maxlen:] ans, self.__data = self.__data[:maxlen], self.__data[maxlen:]
@ -132,6 +171,8 @@ class LocalNetworkReply(QNetworkReply):
read = readData read = readData
def finalize_reply(self): def finalize_reply(self):
if self._aborted:
return
self.setFinished(True) self.setFinished(True)
self.setAttribute(QNetworkRequest.HttpStatusCodeAttribute, 200) self.setAttribute(QNetworkRequest.HttpStatusCodeAttribute, 200)
self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, "Ok") self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, "Ok")
@ -140,10 +181,6 @@ class LocalNetworkReply(QNetworkReply):
self.readyRead.emit() self.readyRead.emit()
self.finished.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): class NetworkAccessManager(QNetworkAccessManager):
@ -152,9 +189,16 @@ class NetworkAccessManager(QNetworkAccessManager):
'Custom') '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): def createRequest(self, operation, request, data):
url = unicode(request.url().toString()) url = unicode(request.url().toString())
if url.startswith('file://'): if operation == self.GetOperation and url.startswith('file://'):
path = url[7:] path = url[7:]
if iswindows and path.startswith('/'): if iswindows and path.startswith('/'):
path = path[1:] path = path[1:]
@ -162,13 +206,13 @@ class NetworkAccessManager(QNetworkAccessManager):
name = c.abspath_to_name(path) name = c.abspath_to_name(path)
if c.has_name(name): if c.has_name(name):
try: try:
return LocalNetworkReply(self, request, c.mime_map.get(name, 'application/octet-stream'), return NetworkReply(self, request, c.mime_map.get(name, 'application/octet-stream'), name)
get_data(name) if operation == self.GetOperation else b'')
except Exception: except Exception:
import traceback import traceback
traceback.print_stack() traceback.print_exc()
return QNetworkAccessManager.createRequest(self, operation, request, return QNetworkAccessManager.createRequest(self, operation, request, data)
data)
# }}}
class WebView(QWebView): class WebView(QWebView):
@ -195,6 +239,20 @@ class WebView(QWebView):
def sizeHint(self): def sizeHint(self):
return self._size_hint 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): class Preview(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
@ -202,21 +260,27 @@ class Preview(QWidget):
self.l = l = QVBoxLayout() self.l = l = QVBoxLayout()
self.setLayout(l) self.setLayout(l)
l.setContentsMargins(0, 0, 0, 0) l.setContentsMargins(0, 0, 0, 0)
self.parse_worker = ParseWorker(callback=Dispatcher(self.parsing_done))
self.view = WebView(self) self.view = WebView(self)
l.addWidget(self.view) l.addWidget(self.view)
self.current_name = None self.current_name = None
self.parse_pending = False
self.last_sync_request = None self.last_sync_request = None
self.refresh_timer = QTimer(self)
self.refresh_timer.timeout.connect(self.refresh)
parse_worker.start()
def show(self, name): def show(self, name):
self.current_name, self.parse_pending = name, True if name != self.current_name:
self.parse_worker.add_request(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): def refresh(self):
if name == self.current_name: if self.current_name:
c = current_container() # This will check if the current html has changed in its editor,
self.view.setHtml(data, QUrl.fromLocalFile(c.name_to_abspath(name))) # and re-parse it if so
self.parse_pending = False parse_worker.add_request(self.current_name)
# Tell webkit to reload all html and associated resources
self.view.refresh()