1# This file is part of ReText
2# Copyright: 2014 Lukas Holecek
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17from FakeVim import FakeVimProxy, FakeVimHandler, FAKEVIM_PYQT_VERSION, \
18	MessageError
19
20if FAKEVIM_PYQT_VERSION != 5:
21	raise ImportError("FakeVim must be compiled with Qt 5")
22
23from PyQt5.QtCore import QDir, QRegExp, QObject, Qt
24from PyQt5.QtGui import QPainter, QPen, QTextCursor
25from PyQt5.QtWidgets import QWidget, QLabel, QLineEdit, \
26	QMessageBox, QStatusBar, QTextEdit
27
28class FakeVimMode:
29	@staticmethod
30	def init(window):
31		window.setStatusBar(StatusBar())
32
33	@staticmethod
34	def exit(window):
35		window.statusBar().deleteLater()
36
37class Proxy (FakeVimProxy):
38	""" Used by FakeVim to modify or retrieve editor state. """
39	def __init__(self, window, editor, handler):
40		super(Proxy, self).__init__(handler.handler())
41		self.__handler = handler
42		self.__window = window
43		self.__editor = editor
44
45		self.__statusMessage = ""
46		self.__statusData = ""
47		self.__cursorPosition =  -1
48		self.__cursorAnchor =  -1
49		self.__eventFilter = None
50
51		self.__lastSavePath = ""
52
53	def showMessage(self, messageLevel, message):
54		self.__handler.handler().showMessage(messageLevel, message)
55
56	def needSave(self):
57		return self.__editor.document().isModified()
58
59	def maybeCloseEditor(self):
60		if self.needSave():
61			self.showMessage( MessageError,
62					self.tr("No write since last change (add ! to override)") )
63			self.__updateStatusBar()
64
65			return False
66
67		return True
68
69	def commandQuit(self):
70		self.__handler.quit()
71
72	def commandWrite(self):
73		self.__handler.save()
74		return not self.needSave()
75
76	def handleExCommand(self, cmd):
77		if cmd.matches("q", "quit"):
78			if cmd.hasBang or self.maybeCloseEditor():
79				self.commandQuit()
80		elif cmd.matches("w", "write"):
81			self.commandWrite()
82		elif cmd.cmd == "wq":
83			self.commandWrite() and self.commandQuit()
84		else:
85			return False
86		return True
87
88	def enableBlockSelection(self, cursor):
89		self.__handler.setBlockSelection(True)
90		self.__editor.setTextCursor(cursor)
91
92	def disableBlockSelection(self):
93		self.__handler.setBlockSelection(False)
94
95	def blockSelection(self):
96		self.__handler.setBlockSelection(True)
97		return self.__editor.textCursor()
98
99	def hasBlockSelection(self):
100		return self.__handler.hasBlockSelection()
101
102	def commandBufferChanged(self, msg, cursorPosition, cursorAnchor, messageLevel, eventFilter):
103		# Give focus back to editor if closing command line.
104		if self.__cursorPosition != -1 and cursorPosition == -1:
105			self.__editor.setFocus()
106
107		self.__cursorPosition = cursorPosition
108		self.__cursorAnchor = cursorAnchor
109		self.__statusMessage = msg
110		self.__updateStatusBar();
111		self.__eventFilter = eventFilter
112
113	def statusDataChanged(self, msg):
114		self.__statusData = msg
115		self.__updateStatusBar()
116
117	def extraInformationChanged(self, msg):
118		QMessageBox.information(self.__window, self.tr("Information"), msg)
119
120	def highlightMatches(self, pattern):
121		self.__handler.highlightMatches(pattern)
122
123	def __updateStatusBar(self):
124		self.__window.statusBar().setStatus(
125				self.__statusMessage, self.__statusData,
126				self.__cursorPosition, self.__cursorAnchor, self.__eventFilter)
127
128class BlockSelection (QWidget):
129	def __init__(self, editor):
130		super(BlockSelection, self).__init__(editor.viewport())
131		self.__editor = editor
132		self.__lineWidth = 4
133
134	def updateSelection(self, tc):
135		# block selection rectagle
136		rect = self.__editor.cursorRect(tc)
137		w = rect.width()
138		tc2 = QTextCursor(tc)
139		tc2.setPosition(tc.anchor())
140		rect = rect.united( self.__editor.cursorRect(tc2) )
141		x = self.__lineWidth / 2
142		rect.adjust(-x, -x, x - w, x)
143
144		self.setGeometry(rect)
145
146	def paintEvent(self, paintEvent):
147		painter = QPainter(self)
148		painter.setClipRect(paintEvent.rect())
149
150		color = self.__editor.palette().text()
151		painter.setPen(QPen(color, self.__lineWidth))
152		painter.drawRect(self.rect())
153
154class ReTextFakeVimHandler (QObject):
155	""" Editor widget driven by FakeVim. """
156	def __init__(self, editor, window):
157		super(ReTextFakeVimHandler, self).__init__(window)
158
159		self.__window = window
160		self.__editor = editor
161
162		self.__blockSelection = BlockSelection(self.__editor)
163		self.__blockSelection.hide()
164
165		self.__searchSelections = []
166
167		fm = self.__editor.fontMetrics()
168		self.__cursorWidth = fm.averageCharWidth()
169		self.__oldCursorWidth = self.__editor.cursorWidth()
170		self.__editor.setCursorWidth(self.__cursorWidth)
171
172		self.__handler = FakeVimHandler(self.__editor, self)
173		self.__proxy = Proxy(self.__window, self.__editor, self)
174
175		self.__handler.installEventFilter()
176		self.__handler.setupWidget()
177		self.__handler.handleCommand(
178				'source {home}/.vimrc'.format(home = QDir.homePath()))
179
180		self.__saveAction = None
181		self.__quitAction = None
182
183		# Update selections if cursor changes because of current line can be highlighted.
184		self.__editor.cursorPositionChanged.connect(self.__updateSelections)
185
186	def remove(self):
187		self.__editor.setOverwriteMode(False)
188		self.__editor.setCursorWidth(self.__oldCursorWidth)
189		self.__blockSelection.deleteLater()
190		self.__updateSelections([])
191		self.deleteLater()
192
193	def handler(self):
194		return self.__handler
195
196	def setBlockSelection(self, enabled):
197		self.__editor.setCursorWidth(self.__cursorWidth)
198		self.__blockSelection.setVisible(enabled)
199
200		if enabled:
201			self.__blockSelection.updateSelection(self.__editor.textCursor())
202
203			# Shift text cursor into the block selection.
204			tc = self.__editor.textCursor()
205			if self.__columnForPosition(tc.anchor()) < self.__columnForPosition(tc.position()):
206				self.__editor.setCursorWidth(-self.__cursorWidth)
207
208	def setSaveAction(self, saveAction):
209		self.__saveAction = saveAction
210
211	def setQuitAction(self, quitAction):
212		self.__quitAction = quitAction
213
214	def save(self):
215		if self.__saveAction:
216			self.__saveAction.trigger()
217
218	def quit(self):
219		if self.__quitAction:
220			self.__quitAction.trigger()
221
222	def hasBlockSelection(self):
223		return self.__blockSelection.isVisible()
224
225	def highlightMatches(self, pattern):
226		cur = self.__editor.textCursor()
227
228		re = QRegExp(pattern)
229		cur = self.__editor.document().find(re)
230		a = cur.position()
231
232		searchSelections = []
233
234		while not cur.isNull():
235			if cur.hasSelection():
236				selection = QTextEdit.ExtraSelection()
237				selection.format.setBackground(Qt.GlobalColor.yellow)
238				selection.format.setForeground(Qt.GlobalColor.black)
239				selection.cursor = cur
240				searchSelections.append(selection)
241			else:
242				cur.movePosition(QTextCursor.MoveOperation.NextCharacter)
243
244			cur = self.__editor.document().find(re, cur)
245			b = cur.position()
246
247			if a == b:
248				cur.movePosition(QTextCursor.MoveOperation.NextCharacter)
249				cur = self.__editor.document().find(re, cur)
250				b = cur.position()
251
252				if (a == b):
253					break
254			a = b
255
256		self.__updateSelections(searchSelections)
257
258	def __updateSelections(self, searchSelections = None):
259		oldSelections = self.__editor.extraSelections()
260
261		for selection in self.__searchSelections:
262			for i in range(len(oldSelections) - 1, 0, -1):
263				if selection.cursor == oldSelections[i].cursor:
264					oldSelections.pop(i)
265					break
266
267		if searchSelections != None:
268			self.__searchSelections = searchSelections
269
270		self.__editor.setExtraSelections(oldSelections + self.__searchSelections)
271
272	def __columnForPosition(self, position):
273		return position - self.__editor.document().findBlock(position).position()
274
275class StatusBar (QStatusBar):
276	def __init__(self):
277		super(StatusBar, self).__init__()
278
279		self.__statusMessageLabel = QLabel(self)
280		self.__statusDataLabel = QLabel(self)
281		self.__commandLine = QLineEdit(self)
282
283		self.addPermanentWidget(self.__statusMessageLabel, 1)
284		self.addPermanentWidget(self.__commandLine, 1)
285		self.addPermanentWidget(self.__statusDataLabel)
286
287		self.__commandLine.hide()
288
289	def setStatus(self, statusMessage, statusData, cursorPosition, cursorAnchor, eventFilter):
290		commandMode = cursorPosition != -1
291		self.__commandLine.setVisible(commandMode)
292		self.__statusMessageLabel.setVisible(not commandMode)
293
294		if commandMode:
295			self.__commandLine.installEventFilter(eventFilter)
296			self.__commandLine.setFocus()
297			self.__commandLine.setText(statusMessage)
298			self.__commandLine.setSelection(cursorPosition, cursorAnchor - cursorPosition)
299		else:
300			self.__statusMessageLabel.setText(statusMessage)
301
302		self.__statusDataLabel.setText(statusData)
303