1# This file is part of the Frescobaldi project, http://www.frescobaldi.org/ 2# 3# Copyright (c) 2008 - 2014 by Wilbert Berendsen 4# 5# This program is free software; you can redistribute it and/or 6# modify it under the terms of the GNU General Public License 7# as published by the Free Software Foundation; either version 2 8# of the License, or (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18# See http://www.gnu.org/licenses/ for more information. 19 20""" 21The SVG view (a QWebView displaying a SVG file). 22 23Interaction between the SVG object and Python is done via a JavaScript bridge 24that runs inside the displayed SVG file. 25 26""" 27 28 29 30 31import os 32import sys 33 34from PyQt5.QtCore import pyqtSignal, pyqtSlot, QFile, QIODevice, QObject, QSettings, QUrl 35from PyQt5.QtGui import QTextCharFormat, QTextCursor 36from PyQt5.QtWebChannel import QWebChannel 37from PyQt5.QtWebEngineWidgets import QWebEngineView 38 39import app 40import util 41import textedit 42import textformats 43import pointandclick 44import scratchdir 45 46 47from . import __path__ 48 49 50def getJsScript(filename): 51 """fetch the js file""" 52 directory = __path__[0] 53 with open(os.path.join(directory, filename), 'r') as fileObject: 54 jsValue = fileObject.read() 55 return jsValue 56 57 58class View(QWebEngineView): 59 zoomFactorChanged = pyqtSignal(float) 60 objectDragged = pyqtSignal(float, float) 61 objectDragging = pyqtSignal(float, float) 62 objectStartDragging = pyqtSignal(float, float) 63 64 cursor = pyqtSignal(QTextCursor) 65 selectedObject = pyqtSignal(str) 66 selectedUrl = pyqtSignal(QTextCursor) 67 68 defaulturl = QUrl.fromLocalFile(os.path.join(__path__[0], 'background.html')) 69 70 def __init__(self, parent): 71 super(View, self).__init__(parent) 72 self._highlightFormat = QTextCharFormat() 73 self.jslink = JSLink(self) 74 channel = QWebChannel(self) 75 channel.registerObject("pyLinks", self.jslink) 76 self.page().setWebChannel(channel) 77 self.loadFinished.connect(self.svgLoaded) 78 self.mainwindow().aboutToClose.connect(self.cleanupForClose) 79 app.settingsChanged.connect(self.readSettings) 80 self.readSettings() 81 self.load(self.defaulturl) 82 83 def cleanupForClose(self): 84 """Called when our mainwindow is about to close. 85 86 Disconnects the loadFinished signal to prevent a RuntimeError 87 about the QWebView already being deleted. 88 89 """ 90 self.loadFinished.disconnect(self.svgLoaded) 91 92 def mainwindow(self): 93 return self.parent().mainwindow() 94 95 def currentSVG(self): 96 return self.parent().getCurrent() 97 98 def document(self, filename, load=False): 99 """Get the document with the specified filename. 100 101 If load is True, the document is loaded if it wasn't already. 102 Also takes scratchdir into account for unnamed or non-local documents. 103 104 """ 105 doc = scratchdir.findDocument(filename) 106 if not doc and load: 107 doc = app.openUrl(QUrl.fromLocalFile(filename)) 108 return doc 109 110 def initJavaScript(self): 111 """Return a string containing all JavaScript to run in a page.""" 112 try: 113 return self._initJavaScript 114 except AttributeError: 115 js = [] 116 qwebchannel_js = QFile(':/qtwebchannel/qwebchannel.js') 117 qwebchannel_js.open(QIODevice.ReadOnly) 118 js.append(bytes(qwebchannel_js.readAll()).decode('utf-8')) 119 js.append("new QWebChannel(qt.webChannelTransport, function (channel) {\n" 120 " window.pyLinks = channel.objects.pyLinks;\n" 121 "});\n") 122 js.append(getJsScript('pointandclick.js')) 123 # for now only editable in dev (git) or when the user explicitly allows experimental features 124 if app.is_git_controlled() or QSettings().value("experimental-features", False, bool): 125 js.append(getJsScript('editsvg.js')) 126 self._initJavaScript = '\n'.join(js) 127 return self._initJavaScript 128 129 def svgLoaded(self): 130 if not self.url().isEmpty() and not self.url().path().endswith(".html"): 131 # initialize the js module 132 self.page().runJavaScript(self.initJavaScript()) 133 134 def evalSave(self): 135 # to enable useful save of SVG edits to file uncomment the line below 136 # self.page().runJavaScript(getJsScript('cleansvg.js')) 137 self.page().runJavaScript(getJsScript('savesvg.js')) 138 139 def clear(self): 140 """Empty the View.""" 141 self.load(self.defaulturl) 142 143 def dragElement(self, url): 144 t = textedit.link(url) 145 # Only process textedit links 146 if not t: 147 return False 148 filename = util.normpath(t.filename) 149 doc = self.document(filename, True) 150 if doc: 151 cursor = QTextCursor(doc) 152 b = doc.findBlockByNumber(t.line - 1) 153 p = b.position() + t.column 154 cursor.setPosition(p) 155 self.emitCursor(cursor) 156 157 def doObjectDragged(self, offsX, offsY): 158 """announce extra-offsets an element has been dragged to""" 159 self.objectDragged.emit(offsX, offsY) 160 161 def doObjectDragging(self, offsX, offsY): 162 """announce extra-offsets while dragging an element""" 163 self.objectDragging.emit(offsX, offsY) 164 165 def doObjectStartDragging(self, offsX, offsY): 166 """announce extra-offsets when starting to drag an element""" 167 self.objectStartDragging.emit(offsX, offsY) 168 169 def doTextEdit(self, url, setCursor = False): 170 """Process a textedit link and either highlight 171 the corresponding source code or set the 172 cursor to it. 173 """ 174 t = textedit.link(url) 175 # Only process textedit links 176 if not t: 177 return False 178 filename = util.normpath(t.filename) 179 doc = self.document(filename, setCursor) 180 if doc: 181 cursor = QTextCursor(doc) 182 b = doc.findBlockByNumber(t.line - 1) 183 p = b.position() + t.column 184 cursor.setPosition(p) 185 cursors = pointandclick.positions(cursor) 186 # Do highlighting if the document is active 187 if cursors and doc == self.mainwindow().currentDocument(): 188 import viewhighlighter 189 view = self.mainwindow().currentView() 190 viewhighlighter.highlighter(view).highlight(self._highlightFormat, cursors, 2, 0) 191 # set the cursor and bring the document to front 192 if setCursor: 193 mainwindow = self.mainwindow() 194 mainwindow.setTextCursor(cursor) 195 import widgets.blink 196 widgets.blink.Blinker.blink_cursor(mainwindow.currentView()) 197 self.mainwindow().setCurrentDocument(doc) 198 mainwindow.activateWindow() 199 mainwindow.currentView().setFocus() 200 return True 201 202 def emitCursor(self, cursor): 203 self.cursor.emit(cursor) 204 205 def readSettings(self): 206 """Reads the settings from the user's preferences.""" 207 color = textformats.formatData('editor').baseColors['selectionbackground'] 208 color.setAlpha(128) 209 self._highlightFormat.setBackground(color) 210 211 def saveSVG(self, svg_string): 212 """Pass string from JavaScript and save to current SVG page.""" 213 with open(self.currentSVG(), 'wb') as f: 214 f.write(svg_string.encode('utf8')) 215 216 def unHighlight(self): 217 import viewhighlighter 218 view = self.mainwindow().currentView() 219 viewhighlighter.highlighter(view).clear(self._highlightFormat) 220 221 def zoomIn(self): 222 self.setZoomFactor(self.zoomFactor() * 1.1) 223 224 def zoomOut(self): 225 self.setZoomFactor(self.zoomFactor() / 1.1) 226 227 def zoomOriginal(self): 228 self.setZoomFactor(1.0) 229 230 def setZoomFactor(self, value): 231 changed = self.zoomFactor() != value 232 super(View, self).setZoomFactor(value) 233 if changed: 234 self.zoomFactorChanged.emit(self.zoomFactor()) 235 236 237class JSLink(QObject): 238 """functions to be called from JavaScript 239 240 using addToJavaScriptWindowObject 241 242 """ 243 def __init__(self, view): 244 super(JSLink, self).__init__() 245 self.view = view 246 247 @pyqtSlot(str) 248 def click(self, url): 249 """set cursor in source by clicked textedit link""" 250 if not self.view.doTextEdit(url, True): 251 import helpers 252 helpers.openUrl(QUrl(url)) 253 254 @pyqtSlot(float, float) 255 def dragged(self, offX, offY): 256 """announce extra-offsets an element has been dragged to""" 257 self.view.doObjectDragged(offX, offY) 258 259 @pyqtSlot(str) 260 def draggedObject(self, JSON_string): 261 # leave the following commented code as an idea how to proceed from here 262 #print("Dragged object JSON representation:") 263 #import json 264 #js = json.JSONDecoder() 265 #print(js.decode(JSON_string)) 266 pass 267 268 @pyqtSlot(str) 269 def dragElement(self, url): 270 self.view.dragElement(url) 271 272 @pyqtSlot(float, float) 273 def dragging(self, offX, offY): 274 """announce extra-offsets while dragging an element""" 275 self.view.doObjectDragging(offX, offY) 276 277 @pyqtSlot(str) 278 def hover(self, url): 279 """actions when user set mouse over link""" 280 self.view.doTextEdit(url, False) 281 282 @pyqtSlot(str) 283 def leave(self, url): 284 """actions when user moves mouse off link""" 285 self.view.unHighlight() 286 287 @pyqtSlot(str) 288 def pyLog(self, txt): 289 """Temporary function. Print to Python console.""" 290 print(txt) 291 292 @pyqtSlot(str) 293 def saveSVG(self, svg_string): 294 """Pass string from JavaScript and save to current SVG page.""" 295 self.view.saveSVG(svg_string) 296 297 @pyqtSlot(float, float) 298 def startDragging(self, offX, offY): 299 """announce extra-offsets when starting to drag an element""" 300 self.view.doObjectStartDragging(offX, offY) 301