Implement position sync from editor to preview panel

This commit is contained in:
Kovid Goyal 2013-11-18 18:35:11 +05:30
parent 5abe588d71
commit 7d1b8a056d
3 changed files with 140 additions and 15 deletions

View File

@ -495,12 +495,24 @@ class Boss(QObject):
def sync_editor_to_preview(self, name, lnum):
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):
editor.undo_redo_state_changed.connect(self.editor_undo_redo_state_changed)
editor.data_changed.connect(self.editor_data_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 use_template:
editor.init_from_template(data)

View File

@ -19,6 +19,7 @@ class Editor(QMainWindow):
undo_redo_state_changed = pyqtSignal(object, object)
copy_available_state_changed = pyqtSignal(object)
data_changed = pyqtSignal(object)
cursor_position_changed = pyqtSignal()
def __init__(self, syntax, parent=None):
QMainWindow.__init__(self, parent)
@ -36,6 +37,15 @@ class Editor(QMainWindow):
self.editor.redoAvailable.connect(self._redo_available)
self.editor.textChanged.connect(self._data_changed)
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
def data(self):
@ -51,9 +61,6 @@ class Editor(QMainWindow):
def init_from_template(self, template):
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):
return unicode(self.editor.toPlainText())
@ -118,12 +125,14 @@ class Editor(QMainWindow):
self.modification_state_changed.disconnect()
self.undo_redo_state_changed.disconnect()
self.copy_available_state_changed.disconnect()
self.cursor_position_changed.disconnect()
self.data_changed.disconnect()
self.editor.undoAvailable.disconnect()
self.editor.redoAvailable.disconnect()
self.editor.modificationChanged.disconnect()
self.editor.textChanged.disconnect()
self.editor.copyAvailable.disconnect()
self.editor.cursorPositionChanged.disconnect()
self.editor.setPlainText('')
def _data_changed(self):
@ -141,6 +150,9 @@ class Editor(QMainWindow):
self.copy_available = self.cut_available = available
self.copy_available_state_changed.emit(available)
def _cursor_position_changed(self, *args):
self.cursor_position_changed.emit()
def cut(self):
self.editor.cut()

View File

@ -7,6 +7,8 @@ __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import time
from bisect import bisect_right
from future_builtins import map
from threading import Thread
from Queue import Queue, Empty
@ -36,7 +38,7 @@ def get_data(name):
# Parsing of html to add linenumbers {{{
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')
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):
sync_requested = pyqtSignal(object)
@ -233,18 +309,10 @@ class WebPage(QWebPage):
prints('preview js:%s:%s:'%(unicode(source_id), lineno), unicode(msg))
def init_javascript(self):
self._line_numbers = None
mf = self.mainFrame()
mf.addToJavaScriptWindowObject("py_bridge", self)
mf.evaluateJavaScript(
'''
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);
}
''')
mf.evaluateJavaScript(JS)
@pyqtSlot(str)
def request_sync(self, lnum):
@ -253,6 +321,24 @@ class WebPage(QWebPage):
except (TypeError, ValueError, OverflowError, AttributeError):
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):
def __init__(self, parent=None):
@ -341,11 +427,26 @@ class Preview(QWidget):
self.refresh_timer = QTimer(self)
self.refresh_timer.timeout.connect(self.refresh)
parse_worker.start()
self.current_sync_request = None
def request_sync(self, lnum):
if self.current_name:
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):
if name != self.current_name:
self.refresh_timer.stop()