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