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