1# vim: ts=4:sw=4:expandtab
2
3# This file is part of ReText
4# Copyright: 2017-2021 Dmitry Shachnev
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
19from ReText import globalSettings
20from ReText.preview import ReTextWebPreview
21from ReText.syncscroll import SyncScroll
22from PyQt5.QtCore import QEvent, Qt
23from PyQt5.QtGui import QDesktopServices, QGuiApplication, QTextDocument
24from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInfo, QWebEngineUrlRequestInterceptor
25from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineView, QWebEngineSettings
26
27
28class ReTextWebEngineUrlRequestInterceptor(QWebEngineUrlRequestInterceptor):
29    def interceptRequest(self, info):
30        if (info.resourceType() == QWebEngineUrlRequestInfo.ResourceType.ResourceTypeXhr
31                and info.requestUrl().isLocalFile()):
32            # For security reasons, disable XMLHttpRequests to local files
33            info.block(True)
34
35
36class ReTextWebEnginePage(QWebEnginePage):
37    def __init__(self, parent, tab):
38        QWebEnginePage.__init__(self, parent)
39        self.tab = tab
40        self.interceptor = ReTextWebEngineUrlRequestInterceptor(self)
41        if hasattr(self, 'setUrlRequestInterceptor'):  # Available since Qt 5.13
42            self.setUrlRequestInterceptor(self.interceptor)
43        else:
44            self.profile().setRequestInterceptor(self.interceptor)
45
46    def setScrollPosition(self, pos):
47        self.runJavaScript("window.scrollTo(%s, %s);" % (pos.x(), pos.y()))
48
49    def getPositionMap(self, callback):
50        def resultCallback(result):
51            if result:
52                return callback({int(a): b for a, b in result.items()})
53
54        script = """
55        var elements = document.querySelectorAll('[data-posmap]');
56        var result = {};
57        var bodyTop = document.body.getBoundingClientRect().top;
58        for (var i = 0; i < elements.length; ++i) {
59            var element = elements[i];
60            value = element.getAttribute('data-posmap');
61            bottom = element.getBoundingClientRect().bottom - bodyTop;
62            result[value] = bottom;
63        }
64        result;
65        """
66        self.runJavaScript(script, resultCallback)
67
68    def javaScriptConsoleMessage(self, level, message, lineNumber, sourceId):
69        print("level=%r message=%r lineNumber=%r sourceId=%r" % (level, message, lineNumber, sourceId))
70
71    def acceptNavigationRequest(self, url, type, isMainFrame):
72        if url.scheme() == "data":
73            return True
74        if url.isLocalFile():
75            localFile = url.toLocalFile()
76            if localFile == self.tab.fileName:
77                self.tab.startPendingConversion()
78                return False
79            if self.tab.openSourceFile(localFile):
80                return False
81        if globalSettings.handleWebLinks:
82            return True
83        QDesktopServices.openUrl(url)
84        return False
85
86
87class ReTextWebEnginePreview(ReTextWebPreview, QWebEngineView):
88
89    def __init__(self, tab,
90                 editorPositionToSourceLineFunc,
91                 sourceLineToEditorPositionFunc):
92
93        QWebEngineView.__init__(self, parent=tab)
94        webPage = ReTextWebEnginePage(self, tab)
95        self.setPage(webPage)
96
97        self.syncscroll = SyncScroll(webPage,
98                                     editorPositionToSourceLineFunc,
99                                     sourceLineToEditorPositionFunc)
100        ReTextWebPreview.__init__(self, tab.editBox)
101
102    def updateFontSettings(self):
103        settings = self.settings()
104        settings.setFontFamily(QWebEngineSettings.FontFamily.StandardFont,
105                               globalSettings.font.family())
106        settings.setFontSize(QWebEngineSettings.FontSize.DefaultFontSize,
107                             globalSettings.font.pointSize())
108
109    def setHtml(self, html, baseUrl):
110        # A hack to prevent WebEngine from stealing the focus
111        self.setEnabled(False)
112        super().setHtml(html, baseUrl)
113        self.setEnabled(True)
114
115    def _handleWheelEvent(self, event):
116        # Only pass wheelEvents on to the preview if syncscroll is
117        # controlling the position of the preview
118        if self.syncscroll.isActive():
119            QGuiApplication.sendEvent(self.focusProxy(), event)
120
121    def event(self, event):
122        # Work-around https://bugreports.qt.io/browse/QTBUG-43602
123        if event.type() == QEvent.Type.ChildAdded:
124            event.child().installEventFilter(self)
125        elif event.type() == QEvent.Type.ChildRemoved:
126            event.child().removeEventFilter(self)
127        return super().event(event)
128
129    def eventFilter(self, object, event):
130        if event.type() == QEvent.Type.Wheel:
131            if QGuiApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier:
132                self.wheelEvent(event)
133                return True
134        return False
135
136    def findText(self, text, flags):
137        options = QWebEnginePage.FindFlags()
138        if flags & QTextDocument.FindFlag.FindBackward:
139            options |= QWebEnginePage.FindFlag.FindBackward
140        if flags & QTextDocument.FindFlag.FindCaseSensitively:
141            options |= QWebEnginePage.FindFlag.FindCaseSensitively
142        super().findText(text, options)
143        return True
144