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