1# vim: ts=8:sts=8:sw=8:noexpandtab 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 os.path import exists 20import time 21from PyQt5.QtCore import QDir, QUrl, Qt 22from PyQt5.QtGui import QDesktopServices, QGuiApplication, QTextCursor, QTextDocument 23from PyQt5.QtWidgets import QTextBrowser 24from ReText import globalSettings 25 26class ReTextPreview(QTextBrowser): 27 28 def __init__(self, tab): 29 QTextBrowser.__init__(self) 30 self.tab = tab 31 # if set to True, links to other files will unsuccessfully be opened as anchors 32 self.setOpenLinks(False) 33 self.anchorClicked.connect(self.openInternal) 34 self.lastRenderTime = 0 35 self.distToBottom = None 36 self.verticalScrollBar().rangeChanged.connect(self.updateScrollPosition) 37 38 def disconnectExternalSignals(self): 39 pass 40 41 def openInternal(self, link): 42 url = link.url() 43 if url.startswith('#'): 44 self.scrollToAnchor(url[1:]) 45 return 46 elif link.isRelative(): 47 fileToOpen = QDir.current().filePath(url) 48 else: 49 fileToOpen = link.toLocalFile() if link.isLocalFile() else None 50 if fileToOpen is not None: 51 if exists(fileToOpen): 52 link = QUrl.fromLocalFile(fileToOpen) 53 if globalSettings.handleWebLinks and fileToOpen.endswith('.html'): 54 self.setSource(link) 55 return 56 # This is outside the "if exists" block because we can prompt for 57 # creating the file 58 if self.tab.openSourceFile(fileToOpen): 59 return 60 QDesktopServices.openUrl(link) 61 62 def findText(self, text, flags, wrap=False): 63 cursor = self.textCursor() 64 if wrap and flags & QTextDocument.FindFlag.FindBackward: 65 cursor.movePosition(QTextCursor.MoveOperation.End) 66 elif wrap: 67 cursor.movePosition(QTextCursor.MoveOperation.Start) 68 newCursor = self.document().find(text, cursor, flags) 69 if not newCursor.isNull(): 70 self.setTextCursor(newCursor) 71 return True 72 if not wrap: 73 return self.findText(text, flags, wrap=True) 74 return False 75 76 def updateScrollPosition(self, minimum, maximum): 77 """Called when vertical scroll bar range changes. 78 79 If this happened during preview rendering (less than 0.5s since it 80 was started), set the position such that distance to bottom is the 81 same as before refresh. 82 """ 83 timeSinceRender = time.time() - self.lastRenderTime 84 if timeSinceRender < 0.5 and self.distToBottom is not None and maximum: 85 newValue = maximum - self.distToBottom 86 if newValue >= minimum: 87 self.verticalScrollBar().setValue(newValue) 88 89 90class ReTextWebPreview: 91 """This is a common class shared between WebKit and WebEngine 92 based previews.""" 93 94 def __init__(self, editBox): 95 self.editBox = editBox 96 97 self.settings().setDefaultTextEncoding('utf-8') 98 99 # Events relevant to sync scrolling 100 self.editBox.cursorPositionChanged.connect(self._handleCursorPositionChanged) 101 self.editBox.verticalScrollBar().valueChanged.connect(self.syncscroll.handleEditorScrolled) 102 self.editBox.resized.connect(self._handleEditorResized) 103 104 # Scroll the preview when the mouse wheel is used to scroll 105 # beyond the beginning/end of the editor 106 self.editBox.scrollLimitReached.connect(self._handleWheelEvent) 107 108 def disconnectExternalSignals(self): 109 self.editBox.cursorPositionChanged.disconnect(self._handleCursorPositionChanged) 110 self.editBox.verticalScrollBar().valueChanged.disconnect(self.syncscroll.handleEditorScrolled) 111 self.editBox.resized.disconnect(self._handleEditorResized) 112 113 self.editBox.scrollLimitReached.disconnect(self._handleWheelEvent) 114 115 def _handleCursorPositionChanged(self): 116 editorCursorPosition = self.editBox.verticalScrollBar().value() + \ 117 self.editBox.cursorRect().top() 118 self.syncscroll.handleCursorPositionChanged(editorCursorPosition) 119 120 def _handleEditorResized(self, rect): 121 self.syncscroll.handleEditorResized(rect.height()) 122 123 def wheelEvent(self, event): 124 if QGuiApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier: 125 zoomFactor = self.zoomFactor() 126 zoomFactor *= 1.001 ** event.angleDelta().y() 127 self.setZoomFactor(zoomFactor) 128 return super().wheelEvent(event) 129