mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Implement position sync from editor to preview panel
This commit is contained in:
parent
5abe588d71
commit
7d1b8a056d
@ -495,12 +495,24 @@ class Boss(QObject):
|
|||||||
|
|
||||||
def sync_editor_to_preview(self, name, lnum):
|
def sync_editor_to_preview(self, name, lnum):
|
||||||
editor = self.edit_file(name, 'html')
|
editor = self.edit_file(name, 'html')
|
||||||
editor.go_to_line(lnum)
|
editor.current_line = lnum
|
||||||
|
|
||||||
|
def sync_preview_to_editor(self):
|
||||||
|
ed = self.gui.central.current_editor
|
||||||
|
if ed is not None:
|
||||||
|
name = None
|
||||||
|
for n, x in editors.iteritems():
|
||||||
|
if ed is x:
|
||||||
|
name = n
|
||||||
|
break
|
||||||
|
if name is not None and getattr(ed, 'syntax', None) == 'html':
|
||||||
|
self.gui.preview.sync_to_editor(name, ed.current_line)
|
||||||
|
|
||||||
def init_editor(self, name, editor, data=None, use_template=False):
|
def init_editor(self, name, editor, data=None, use_template=False):
|
||||||
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)
|
editor.data_changed.connect(self.editor_data_changed)
|
||||||
editor.copy_available_state_changed.connect(self.editor_copy_available_state_changed)
|
editor.copy_available_state_changed.connect(self.editor_copy_available_state_changed)
|
||||||
|
editor.cursor_position_changed.connect(self.sync_preview_to_editor)
|
||||||
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)
|
||||||
|
@ -19,6 +19,7 @@ class Editor(QMainWindow):
|
|||||||
undo_redo_state_changed = pyqtSignal(object, object)
|
undo_redo_state_changed = pyqtSignal(object, object)
|
||||||
copy_available_state_changed = pyqtSignal(object)
|
copy_available_state_changed = pyqtSignal(object)
|
||||||
data_changed = pyqtSignal(object)
|
data_changed = pyqtSignal(object)
|
||||||
|
cursor_position_changed = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, syntax, parent=None):
|
def __init__(self, syntax, parent=None):
|
||||||
QMainWindow.__init__(self, parent)
|
QMainWindow.__init__(self, parent)
|
||||||
@ -36,6 +37,15 @@ class Editor(QMainWindow):
|
|||||||
self.editor.redoAvailable.connect(self._redo_available)
|
self.editor.redoAvailable.connect(self._redo_available)
|
||||||
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)
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def current_line(self):
|
||||||
|
def fget(self):
|
||||||
|
return self.editor.textCursor().blockNumber()
|
||||||
|
def fset(self, val):
|
||||||
|
self.editor.go_to_line(val)
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
@dynamic_property
|
@dynamic_property
|
||||||
def data(self):
|
def data(self):
|
||||||
@ -51,9 +61,6 @@ class Editor(QMainWindow):
|
|||||||
def init_from_template(self, template):
|
def init_from_template(self, template):
|
||||||
self.editor.load_text(template, syntax=self.syntax, process_template=True)
|
self.editor.load_text(template, syntax=self.syntax, process_template=True)
|
||||||
|
|
||||||
def go_to_line(self, lnum):
|
|
||||||
self.editor.go_to_line(lnum)
|
|
||||||
|
|
||||||
def get_raw_data(self):
|
def get_raw_data(self):
|
||||||
return unicode(self.editor.toPlainText())
|
return unicode(self.editor.toPlainText())
|
||||||
|
|
||||||
@ -118,12 +125,14 @@ class Editor(QMainWindow):
|
|||||||
self.modification_state_changed.disconnect()
|
self.modification_state_changed.disconnect()
|
||||||
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.data_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.textChanged.disconnect()
|
||||||
self.editor.copyAvailable.disconnect()
|
self.editor.copyAvailable.disconnect()
|
||||||
|
self.editor.cursorPositionChanged.disconnect()
|
||||||
self.editor.setPlainText('')
|
self.editor.setPlainText('')
|
||||||
|
|
||||||
def _data_changed(self):
|
def _data_changed(self):
|
||||||
@ -141,6 +150,9 @@ class Editor(QMainWindow):
|
|||||||
self.copy_available = self.cut_available = available
|
self.copy_available = self.cut_available = available
|
||||||
self.copy_available_state_changed.emit(available)
|
self.copy_available_state_changed.emit(available)
|
||||||
|
|
||||||
|
def _cursor_position_changed(self, *args):
|
||||||
|
self.cursor_position_changed.emit()
|
||||||
|
|
||||||
def cut(self):
|
def cut(self):
|
||||||
self.editor.cut()
|
self.editor.cut()
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
from bisect import bisect_right
|
||||||
|
from future_builtins import map
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from Queue import Queue, Empty
|
from Queue import Queue, Empty
|
||||||
|
|
||||||
@ -36,7 +38,7 @@ def get_data(name):
|
|||||||
|
|
||||||
# Parsing of html to add linenumbers {{{
|
# Parsing of html to add linenumbers {{{
|
||||||
def parse_html(raw):
|
def parse_html(raw):
|
||||||
root = parse(raw, decoder=lambda x:x.decode('utf-8'), line_numbers=True, linenumber_attribute='lnum')
|
root = parse(raw, decoder=lambda x:x.decode('utf-8'), line_numbers=True, linenumber_attribute='data-lnum')
|
||||||
return serialize(root, 'text/html').encode('utf-8')
|
return serialize(root, 'text/html').encode('utf-8')
|
||||||
|
|
||||||
class ParseItem(object):
|
class ParseItem(object):
|
||||||
@ -218,6 +220,80 @@ class NetworkAccessManager(QNetworkAccessManager):
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
JS = '''
|
||||||
|
function handle_click(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
window.py_bridge.request_sync(event.target.getAttribute("data-lnum"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function line_numbers() {
|
||||||
|
var elements = document.getElementsByTagName('*'), found_body = false, ans = [], node, i;
|
||||||
|
var found_body = false;
|
||||||
|
var ans = [];
|
||||||
|
for (i = 0; i < elements.length; i++) {
|
||||||
|
node = elements[i];
|
||||||
|
if (!found_body && node.tagName.toLowerCase() === "body") {
|
||||||
|
found_body = true;
|
||||||
|
}
|
||||||
|
if (found_body) {
|
||||||
|
ans.push(node.getAttribute("data-lnum"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ans;
|
||||||
|
}
|
||||||
|
|
||||||
|
function document_offset_top(obj) {
|
||||||
|
var curtop = 0;
|
||||||
|
if (obj.offsetParent) {
|
||||||
|
do {
|
||||||
|
curtop += obj.offsetTop;
|
||||||
|
} while (obj = obj.offsetParent);
|
||||||
|
return curtop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_hidden(elem) {
|
||||||
|
var p = elem;
|
||||||
|
while (p) {
|
||||||
|
if (p.style && (p.style.visibility === 'hidden' || p.style.display === 'none'))
|
||||||
|
return true;
|
||||||
|
p = p.parentNode;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function go_to_line(lnum) {
|
||||||
|
var elements = document.querySelectorAll('[data-lnum="' + lnum + '"]');
|
||||||
|
for (var i = 0; i < elements.length; i++) {
|
||||||
|
var node = elements[i];
|
||||||
|
if (is_hidden(node)) continue;
|
||||||
|
var top = document_offset_top(node) - (window.innerHeight / 2);
|
||||||
|
if (top < 0) top = 0;
|
||||||
|
window.scrollTo(0, top);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onload = function() {
|
||||||
|
document.body.addEventListener('click', handle_click, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
def uniq(vals):
|
||||||
|
''' Remove all duplicates from vals, while preserving order. '''
|
||||||
|
vals = vals or ()
|
||||||
|
seen = set()
|
||||||
|
seen_add = seen.add
|
||||||
|
return tuple(x for x in vals if x not in seen and not seen_add(x))
|
||||||
|
|
||||||
|
def find_le(a, x):
|
||||||
|
'Find rightmost value in a less than or equal to x'
|
||||||
|
i = bisect_right(a, x)
|
||||||
|
if i:
|
||||||
|
return a[i-1]
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
class WebPage(QWebPage):
|
class WebPage(QWebPage):
|
||||||
|
|
||||||
sync_requested = pyqtSignal(object)
|
sync_requested = pyqtSignal(object)
|
||||||
@ -233,18 +309,10 @@ class WebPage(QWebPage):
|
|||||||
prints('preview js:%s:%s:'%(unicode(source_id), lineno), unicode(msg))
|
prints('preview js:%s:%s:'%(unicode(source_id), lineno), unicode(msg))
|
||||||
|
|
||||||
def init_javascript(self):
|
def init_javascript(self):
|
||||||
|
self._line_numbers = None
|
||||||
mf = self.mainFrame()
|
mf = self.mainFrame()
|
||||||
mf.addToJavaScriptWindowObject("py_bridge", self)
|
mf.addToJavaScriptWindowObject("py_bridge", self)
|
||||||
mf.evaluateJavaScript(
|
mf.evaluateJavaScript(JS)
|
||||||
'''
|
|
||||||
function handle_click(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
window.py_bridge.request_sync(event.target.getAttribute("lnum"));
|
|
||||||
}
|
|
||||||
window.onload = function() {
|
|
||||||
document.body.addEventListener('click', handle_click, true);
|
|
||||||
}
|
|
||||||
''')
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def request_sync(self, lnum):
|
def request_sync(self, lnum):
|
||||||
@ -253,6 +321,24 @@ class WebPage(QWebPage):
|
|||||||
except (TypeError, ValueError, OverflowError, AttributeError):
|
except (TypeError, ValueError, OverflowError, AttributeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def line_numbers(self):
|
||||||
|
if self._line_numbers is None:
|
||||||
|
def atoi(x):
|
||||||
|
ans, ok = x.toUInt()
|
||||||
|
if not ok:
|
||||||
|
ans = None
|
||||||
|
return ans
|
||||||
|
self._line_numbers = sorted(uniq(filter(lambda x:x is not None, map(atoi, self.mainFrame().evaluateJavaScript('line_numbers()').toStringList()))))
|
||||||
|
return self._line_numbers
|
||||||
|
|
||||||
|
def go_to_line(self, lnum):
|
||||||
|
try:
|
||||||
|
lnum = find_le(self.line_numbers, lnum)
|
||||||
|
except ValueError:
|
||||||
|
return
|
||||||
|
self.mainFrame().evaluateJavaScript('go_to_line(%d)' % lnum)
|
||||||
|
|
||||||
class WebView(QWebView):
|
class WebView(QWebView):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
@ -341,11 +427,26 @@ class Preview(QWidget):
|
|||||||
self.refresh_timer = QTimer(self)
|
self.refresh_timer = QTimer(self)
|
||||||
self.refresh_timer.timeout.connect(self.refresh)
|
self.refresh_timer.timeout.connect(self.refresh)
|
||||||
parse_worker.start()
|
parse_worker.start()
|
||||||
|
self.current_sync_request = None
|
||||||
|
|
||||||
def request_sync(self, lnum):
|
def request_sync(self, lnum):
|
||||||
if self.current_name:
|
if self.current_name:
|
||||||
self.sync_requested.emit(self.current_name, lnum)
|
self.sync_requested.emit(self.current_name, lnum)
|
||||||
|
|
||||||
|
def sync_to_editor(self, name, lnum):
|
||||||
|
self.current_sync_request = (name, lnum)
|
||||||
|
QTimer.singleShot(100, self._sync_to_editor)
|
||||||
|
|
||||||
|
def _sync_to_editor(self):
|
||||||
|
try:
|
||||||
|
if self.refresh_timer.isActive() or self.current_sync_request[0] != self.current_name:
|
||||||
|
return QTimer.singleShot(100, self._sync_to_editor)
|
||||||
|
except TypeError:
|
||||||
|
return # Happens if current_sync_request is None
|
||||||
|
lnum = self.current_sync_request[1]
|
||||||
|
self.current_sync_request = None
|
||||||
|
self.view.page().go_to_line(lnum)
|
||||||
|
|
||||||
def show(self, name):
|
def show(self, name):
|
||||||
if name != self.current_name:
|
if name != self.current_name:
|
||||||
self.refresh_timer.stop()
|
self.refresh_timer.stop()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user