1# vim: ts=8:sts=8:sw=8:noexpandtab
2#
3# This file is part of ReText
4# Copyright: 2012-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
19import os
20import re
21import weakref
22
23from markups import MarkdownMarkup, ReStructuredTextMarkup, TextileMarkup
24from ReText import globalSettings, settings, tablemode
25
26from PyQt5.QtCore import pyqtSignal, QFileInfo, QPoint, QRect, QSize, Qt
27from PyQt5.QtGui import QColor, QImage, QKeyEvent, QMouseEvent, QPainter, \
28QPalette, QTextCursor, QTextFormat, QWheelEvent, QGuiApplication
29from PyQt5.QtWidgets import QAction, QApplication, QFileDialog, QLabel, QTextEdit, QWidget
30
31try:
32	from ReText.fakevimeditor import ReTextFakeVimHandler
33except ImportError:
34	ReTextFakeVimHandler = None
35
36colors = {
37	# Editor
38	'marginLine':           {'light': '#dcd2dc', 'dark': '#3daee9'},
39	'currentLineHighlight': {'light': '#ffffc8', 'dark': '#31363b'},
40	'infoArea':             {'light': '#aaaaff55', 'dark': '#aa557f2a'},
41	'statsArea':            {'light': '#aaffaa55', 'dark': '#aa7f552a'},
42	'lineNumberArea':       {'light': '#00ffff', 'dark': '#31363b'},
43	'lineNumberAreaText':   {'light': '#008080', 'dark': '#bdc3c7'},
44	# Highlighter
45	'htmlTags':             {'light': '#800080', 'dark': '#d070d0'},
46	'htmlSymbols':          {'light': '#008080', 'dark': '#70d0a0'},
47	'htmlStrings':          {'light': '#808000', 'dark': '#d0d070'},
48	'htmlComments':         {'light': '#a0a0a4', 'dark': '#b0b0aa'},
49	'codeSpans':            {'light': '#505050', 'dark': '#afafaf'},
50	'markdownLinks':        {'light': '#000090', 'dark': '#8080ff'},
51	'blockquotes':          {'light': '#808080', 'dark': '#b0b0b0'},
52	'restDirectives':       {'light': '#800080', 'dark': '#d070d0'},
53	'restRoles':            {'light': '#800000', 'dark': '#d07070'},
54	'whitespaceOnEnd':      {'light': '#80e1e1a5', 'dark': '#8096966e'},
55}
56
57colorValues = {}
58
59def updateColorScheme(settings=settings):
60	palette = QApplication.palette()
61	windowColor = palette.color(QPalette.ColorRole.Window)
62	themeVariant = 'light' if windowColor.lightness() > 150 else 'dark'
63	settings.beginGroup('ColorScheme')
64	for key in colors:
65		if settings.contains(key):
66			colorValues[key] = settings.value(key, type=QColor)
67		else:
68			colorValues[key] = QColor(colors[key][themeVariant])
69	settings.endGroup()
70
71def getColor(colorName):
72	if not colorValues:
73		updateColorScheme()
74	return colorValues[colorName]
75
76def documentIndentMore(document, cursor, globalSettings=globalSettings):
77	if cursor.hasSelection():
78		block = document.findBlock(cursor.selectionStart())
79		end = document.findBlock(cursor.selectionEnd()).next()
80		cursor.beginEditBlock()
81		while block != end:
82			cursor.setPosition(block.position())
83			if globalSettings.tabInsertsSpaces:
84				cursor.insertText(' ' * globalSettings.tabWidth)
85			else:
86				cursor.insertText('\t')
87			block = block.next()
88		cursor.endEditBlock()
89	else:
90		indent = globalSettings.tabWidth - (cursor.positionInBlock()
91			% globalSettings.tabWidth)
92		if globalSettings.tabInsertsSpaces:
93			cursor.insertText(' ' * indent)
94		else:
95			cursor.insertText('\t')
96
97def documentIndentLess(document, cursor, globalSettings=globalSettings):
98	if cursor.hasSelection():
99		block = document.findBlock(cursor.selectionStart())
100		end = document.findBlock(cursor.selectionEnd()).next()
101	else:
102		block = document.findBlock(cursor.position())
103		end = block.next()
104	cursor.beginEditBlock()
105	while block != end:
106		cursor.setPosition(block.position())
107		if document.characterAt(cursor.position()) == '\t':
108			cursor.deleteChar()
109		else:
110			pos = 0
111			while document.characterAt(cursor.position()) == ' ' \
112			and pos < globalSettings.tabWidth:
113				pos += 1
114				cursor.deleteChar()
115		block = block.next()
116	cursor.endEditBlock()
117
118class ReTextEdit(QTextEdit):
119	resized = pyqtSignal(QRect)
120	scrollLimitReached = pyqtSignal(QWheelEvent)
121	returnBlockPattern = re.compile("^[\\s]*([*>-]|\\d+\\.) ")
122	orderedListPattern = re.compile("^([\\s]*)(\\d+)\\. $")
123	wordPattern = re.compile(r"\w+")
124	nonAlphaNumPattern = re.compile(r"\W")
125	surroundKeysSelfClose = [
126		Qt.Key.Key_Underscore,
127		Qt.Key.Key_Asterisk,
128		Qt.Key.Key_QuoteDbl,
129		Qt.Key.Key_Apostrophe
130	]
131	surroundKeysOtherClose = {
132		Qt.Key.Key_ParenLeft: ')',
133		Qt.Key.Key_BracketLeft: ']'
134	}
135
136	def __init__(self, parent, settings=globalSettings):
137		QTextEdit.__init__(self)
138		self.tab = weakref.proxy(parent)
139		self.parent = parent.p
140		self.undoRedoActive = False
141		self.tableModeEnabled = False
142		self.setAcceptRichText(False)
143		self.lineNumberArea = LineNumberArea(self)
144		self.infoArea = LineInfoArea(self)
145		self.statistics = (0, 0, 0)
146		self.statsArea = TextInfoArea(self)
147		self.updateFont()
148		self.setWrapModeAndWidth()
149		self.document().blockCountChanged.connect(self.updateLineNumberAreaWidth)
150		self.cursorPositionChanged.connect(self.highlightCurrentLine)
151		self.document().contentsChange.connect(self.contentsChange)
152		self.settings = settings
153		if globalSettings.useFakeVim:
154			self.installFakeVimHandler()
155
156	def setWrapModeAndWidth(self):
157		if globalSettings.rightMarginWrap and (self.rect().topRight().x() > self.marginx):
158			self.setLineWrapMode(QTextEdit.LineWrapMode.FixedPixelWidth)
159			self.setLineWrapColumnOrWidth(self.marginx)
160		else:
161			self.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
162
163	def updateFont(self):
164		self.setFont(globalSettings.editorFont)
165		metrics = self.fontMetrics()
166		self.marginx = (int(self.document().documentMargin())
167			+ metrics.horizontalAdvance(' ' * globalSettings.rightMargin))
168		self.setTabStopDistance(globalSettings.tabWidth * metrics.horizontalAdvance(' '))
169		self.updateLineNumberAreaWidth()
170		self.infoArea.updateTextAndGeometry()
171		self.updateTextStatistics()
172		self.statsArea.updateTextAndGeometry()
173		if globalSettings.wideCursor:
174			self.setCursorWidth(metrics.averageCharWidth())
175
176	def paintEvent(self, event):
177		if not globalSettings.rightMargin:
178			return super().paintEvent(event)
179		painter = QPainter(self.viewport())
180		painter.setPen(getColor('marginLine'))
181		y1 = self.rect().topLeft().y()
182		y2 = self.rect().bottomLeft().y()
183		painter.drawLine(self.marginx, y1, self.marginx, y2)
184		super().paintEvent(event)
185
186	def wheelEvent(self, event):
187		modifiers = QGuiApplication.keyboardModifiers()
188		if modifiers == Qt.KeyboardModifier.ControlModifier:
189			font = globalSettings.editorFont
190			size = font.pointSize()
191			scroll = event.angleDelta().y()
192			if scroll > 0:
193				size += 1
194			elif scroll < 0:
195				size -= 1
196			else:
197				return
198			font.setPointSize(size)
199			self.parent.setEditorFont(font)
200		else:
201			super().wheelEvent(event)
202
203			if event.angleDelta().y() < 0:
204				scrollBarLimit = self.verticalScrollBar().maximum()
205			else:
206				scrollBarLimit = self.verticalScrollBar().minimum()
207
208			if self.verticalScrollBar().value() == scrollBarLimit:
209				self.scrollLimitReached.emit(event)
210
211	def scrollContentsBy(self, dx, dy):
212		super().scrollContentsBy(dx, dy)
213		self.lineNumberArea.update()
214
215	def contextMenuEvent(self, event):
216		# Create base menu
217		menu = self.createStandardContextMenu()
218		if self.parent.actionPasteImage.isEnabled():
219			actions = menu.actions()
220			actionPaste = menu.findChild(QAction, "edit-paste")
221			actionNextAfterPaste = actions[actions.index(actionPaste) + 1]
222			menu.insertAction(actionNextAfterPaste, self.parent.actionPasteImage)
223
224		text = self.toPlainText()
225		if not text:
226			menu.exec(event.globalPos())
227			return
228
229		# Check word under the cursor
230		oldcursor = self.textCursor()
231		cursor = self.cursorForPosition(event.pos())
232		curchar = self.document().characterAt(cursor.position())
233		isalpha = curchar.isalpha()
234		dictionary = self.tab.highlighter.dictionary
235		word = None
236		if isalpha and not (oldcursor.hasSelection() and oldcursor.selectedText() != cursor.selectedText()):
237			cursor.select(QTextCursor.SelectionType.WordUnderCursor)
238			word = cursor.selectedText()
239
240		if word is not None and dictionary and not dictionary.check(word):
241			self.setTextCursor(cursor)
242			suggestions = dictionary.suggest(word)
243			actions = [self.parent.act(sug, trig=self.fixWord(sug)) for sug in suggestions]
244			menu.insertSeparator(menu.actions()[0])
245			for action in actions[::-1]:
246				menu.insertAction(menu.actions()[0], action)
247			menu.insertSeparator(menu.actions()[0])
248			menu.insertAction(menu.actions()[0], self.parent.act(self.tr('Add to dictionary'), trig=self.learnWord(word)))
249
250		menu.addSeparator()
251		menu.addAction(self.parent.actionMoveUp)
252		menu.addAction(self.parent.actionMoveDown)
253
254		menu.exec(event.globalPos())
255
256	def fixWord(self, correctword):
257		return lambda: self.insertPlainText(correctword)
258
259	def learnWord(self, newword):
260		return lambda: self.addNewWord(newword)
261
262	def addNewWord(self, newword):
263		cursor = self.textCursor()
264		block = cursor.block()
265		cursor.clearSelection()
266		self.setTextCursor(cursor)
267		dictionary = self.tab.highlighter.dictionary
268		if (dictionary is None) or not newword:
269			return
270		dictionary.add(newword)
271		self.tab.highlighter.rehighlightBlock(block)
272
273	def isSurroundKey(self, key):
274		return key in self.surroundKeysSelfClose or key in self.surroundKeysOtherClose
275
276	def getCloseKey(self, event, key):
277		if key in self.surroundKeysSelfClose:
278			return event.text()
279
280		if key in self.surroundKeysOtherClose:
281			return self.surroundKeysOtherClose[key]
282
283	def surroundText(self, cursor, event, key):
284		text = cursor.selectedText()
285		keyStr = event.text()
286		keyClose = self.getCloseKey(event, key)
287
288		cursor.insertText(keyStr + text + keyClose)
289
290	def keyPressEvent(self, event):
291		key = event.key()
292		cursor = self.textCursor()
293		if key == Qt.Key.Key_Backspace and event.modifiers() & Qt.KeyboardModifier.GroupSwitchModifier:
294			# Workaround for https://bugreports.qt.io/browse/QTBUG-49771
295			event = QKeyEvent(event.type(), event.key(),
296				event.modifiers() ^ Qt.KeyboardModifier.GroupSwitchModifier)
297		if key == Qt.Key.Key_Tab:
298			documentIndentMore(self.document(), cursor)
299		elif key == Qt.Key.Key_Backtab:
300			documentIndentLess(self.document(), cursor)
301		elif key == Qt.Key.Key_Return:
302			markupClass = self.tab.getActiveMarkupClass()
303			if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
304				cursor.insertText('\n')
305				self.ensureCursorVisible()
306			elif self.tableModeEnabled and tablemode.handleReturn(cursor, markupClass,
307					newRow=(event.modifiers() & Qt.KeyboardModifier.ShiftModifier)):
308				self.setTextCursor(cursor)
309				self.ensureCursorVisible()
310			else:
311				if event.modifiers() & Qt.KeyboardModifier.ShiftModifier and markupClass == MarkdownMarkup:
312					# Insert Markdown-style line break
313					cursor.insertText('  ')
314				self.handleReturn(cursor)
315		elif cursor.selectedText() and self.isSurroundKey(key):
316			self.surroundText(cursor, event, key)
317		else:
318			if event.text() and self.tableModeEnabled:
319				cursor.beginEditBlock()
320			super().keyPressEvent(event)
321			if event.text() and self.tableModeEnabled:
322				cursor.endEditBlock()
323
324	def handleReturn(self, cursor):
325		# Select text between the cursor and the line start
326		cursor.movePosition(QTextCursor.MoveOperation.StartOfBlock, QTextCursor.MoveMode.KeepAnchor)
327		text = cursor.selectedText()
328		length = len(text)
329		match = self.returnBlockPattern.search(text)
330		if match is not None:
331			matchedText = match.group(0)
332			if len(matchedText) == length:
333				cursor.removeSelectedText()
334				matchedText = ''
335			else:
336				matchOL = self.orderedListPattern.match(matchedText)
337				if matchOL is not None:
338					matchedPrefix = matchOL.group(1)
339					matchedNumber = int(matchOL.group(2))
340					nextNumber = matchedNumber if self.settings.orderedListMode == 'repeat' else matchedNumber + 1
341					matchedText = matchedPrefix + str(nextNumber) + ". "
342		else:
343			matchedText = ''
344		# Reset the cursor
345		cursor = self.textCursor()
346		cursor.insertText('\n' + matchedText)
347		self.ensureCursorVisible()
348
349	def moveLineUp(self):
350		self.moveLine(QTextCursor.MoveOperation.PreviousBlock)
351
352	def moveLineDown(self):
353		self.moveLine(QTextCursor.MoveOperation.NextBlock)
354
355	def moveLine(self, direction):
356		cursor = self.textCursor()
357		# Select the current block
358		cursor.movePosition(QTextCursor.MoveOperation.StartOfBlock, QTextCursor.MoveMode.MoveAnchor)
359		cursor.movePosition(QTextCursor.MoveOperation.NextBlock, QTextCursor.MoveMode.KeepAnchor)
360		text = cursor.selectedText()
361		# Remove it
362		cursor.removeSelectedText()
363		# Move to the wanted block
364		cursor.movePosition(direction, QTextCursor.MoveMode.MoveAnchor)
365		# Paste the line
366		cursor.insertText(text)
367		# Move to the pasted block
368		cursor.movePosition(QTextCursor.MoveOperation.PreviousBlock, QTextCursor.MoveMode.MoveAnchor)
369		# Update cursor
370		self.setTextCursor(cursor)
371
372	def lineNumberAreaWidth(self):
373		if not globalSettings.lineNumbersEnabled:
374			return 0
375		cursor = QTextCursor(self.document())
376		cursor.movePosition(QTextCursor.MoveOperation.End)
377		if globalSettings.relativeLineNumbers:
378			digits = len(str(cursor.blockNumber())) + 1
379		else:
380			digits = len(str(cursor.blockNumber() + 1))
381		return 5 + self.fontMetrics().horizontalAdvance('9') * digits
382
383	def updateLineNumberAreaWidth(self, blockcount=0):
384		self.setViewportMargins(self.lineNumberAreaWidth(), 0, 0, 0)
385
386	def resizeEvent(self, event):
387		super().resizeEvent(event)
388		rect = self.contentsRect()
389		self.resized.emit(rect)
390		self.lineNumberArea.setGeometry(rect.left(), rect.top(),
391			self.lineNumberAreaWidth(), rect.height())
392		self.infoArea.updateTextAndGeometry()
393		self.statsArea.updateTextAndGeometry()
394		self.setWrapModeAndWidth()
395		self.ensureCursorVisible()
396
397	def highlightCurrentLine(self):
398		if globalSettings.relativeLineNumbers:
399			self.lineNumberArea.update()
400		if globalSettings.highlightCurrentLine == 'disabled':
401			return self.setExtraSelections([])
402		selection = QTextEdit.ExtraSelection()
403		selection.format.setBackground(getColor('currentLineHighlight'))
404		selection.format.setProperty(QTextFormat.Property.FullWidthSelection, True)
405		selection.cursor = self.textCursor()
406		selection.cursor.clearSelection()
407		selections = [selection]
408		if globalSettings.highlightCurrentLine == 'wrapped-line':
409			selections.append(QTextEdit.ExtraSelection())
410			selections[0].cursor.movePosition(QTextCursor.MoveOperation.StartOfBlock)
411			selections[0].cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.KeepAnchor)
412			selections[1].format.setBackground(getColor('currentLineHighlight'))
413			selections[1].format.setProperty(QTextFormat.Property.FullWidthSelection, True)
414			selections[1].cursor = self.textCursor()
415			selections[1].cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock)
416		elif selection.cursor.block().textDirection() == Qt.LayoutDirection.RightToLeft:
417			# FullWidthSelection does not work correctly for RTL direction
418			selection.cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)
419			selection.cursor.movePosition(QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor)
420		self.setExtraSelections(selections)
421
422	def enableTableMode(self, enable):
423		self.tableModeEnabled = enable
424
425	def backupCursorPositionOnLine(self):
426		return self.textCursor().positionInBlock()
427
428	def restoreCursorPositionOnLine(self, positionOnLine):
429		cursor = self.textCursor()
430		cursor.setPosition(cursor.block().position() + positionOnLine)
431		self.setTextCursor(cursor)
432
433	def contentsChange(self, pos, removed, added):
434		if self.tableModeEnabled:
435			markupClass = self.tab.getActiveMarkupClass()
436
437			cursorPosition = self.backupCursorPositionOnLine()
438			tablemode.adjustTableToChanges(self.document(), pos, added - removed, markupClass)
439			self.restoreCursorPositionOnLine(cursorPosition)
440		self.lineNumberArea.update()
441		self.updateTextStatistics()
442
443	def findNextImageName(self, filenames):
444		highestNumber = 0
445		for filename in filenames:
446			m = re.match(r'image(\d+).png', filename, re.IGNORECASE)
447			if m:
448				number = int(m.group(1))
449				highestNumber = max(number, highestNumber)
450		return 'image%04d.png' % (highestNumber + 1)
451
452	def getImageFilename(self):
453		if self.tab.fileName:
454			saveDir = os.path.dirname(self.tab.fileName)
455		else:
456			saveDir = os.getcwd()
457
458		imageFileName = self.findNextImageName(os.listdir(saveDir))
459
460		return QFileDialog.getSaveFileName(self,
461		                                   self.tr('Save image'),
462		                                   os.path.join(saveDir, imageFileName),
463		                                   self.tr('Images (*.png *.jpg)'))[0]
464
465	def makeFileNameRelative(self, fileName):
466		"""Tries to make the given fileName relative. If the document is
467		not saved, or the fileName is on a different root, returns the
468		original fileName.
469		"""
470		if self.tab.fileName:
471			currentDir = os.path.dirname(self.tab.fileName)
472			try:
473				return os.path.relpath(fileName, currentDir)
474			except ValueError:  # different roots
475				return fileName
476		return fileName
477
478	def getImageMarkup(self, fileName):
479		"""Returns markup for image in the current markup language.
480
481		This method is also accessed in ReTextWindow.insertImage.
482		"""
483		link = self.makeFileNameRelative(fileName)
484		markupClass = self.tab.getActiveMarkupClass()
485		if markupClass == MarkdownMarkup:
486			return '![%s](%s)' % (QFileInfo(link).baseName(), link)
487		elif markupClass == ReStructuredTextMarkup:
488			return '.. image:: %s' % link
489		elif markupClass == TextileMarkup:
490			return '!%s!' % link
491
492	def pasteImage(self):
493		mimeData = QApplication.instance().clipboard().mimeData()
494		fileName = self.getImageFilename()
495		if not fileName or not mimeData.hasImage():
496			return
497		image = QImage(mimeData.imageData())
498		image.save(fileName)
499
500		imageText = self.getImageMarkup(fileName)
501
502		self.textCursor().insertText(imageText)
503
504	def installFakeVimHandler(self):
505		if ReTextFakeVimHandler:
506			fakeVimEditor = ReTextFakeVimHandler(self, self.parent)
507			fakeVimEditor.setSaveAction(self.parent.actionSave)
508			fakeVimEditor.setQuitAction(self.parent.actionQuit)
509			self.parent.actionFakeVimMode.triggered.connect(fakeVimEditor.remove)
510
511	def updateTextStatistics(self):
512		if not globalSettings.documentStatsEnabled:
513			return
514		text = self.toPlainText()
515		wordCount = len(self.wordPattern.findall(text))
516		alphaNums = self.nonAlphaNumPattern.sub('', text)
517		alphaNumCount = len(alphaNums)
518		characterCount = len(text)
519		self.statistics = (wordCount, alphaNumCount, characterCount)
520
521
522class LineNumberArea(QWidget):
523	def __init__(self, editor):
524		QWidget.__init__(self, editor)
525		self.editor = editor
526
527	def sizeHint(self):
528		return QSize(self.editor.lineNumberAreaWidth(), 0)
529
530	def paintEvent(self, event):
531		if not globalSettings.lineNumbersEnabled:
532			return super().paintEvent(event)
533		painter = QPainter(self)
534		painter.fillRect(event.rect(), getColor('lineNumberArea'))
535		painter.setPen(getColor('lineNumberAreaText'))
536		cursor = self.editor.cursorForPosition(QPoint(0, 0))
537		atEnd = False
538		fontHeight = self.fontMetrics().height()
539		height = self.editor.height()
540		if globalSettings.relativeLineNumbers:
541			relativeTo = self.editor.textCursor().blockNumber()
542		else:
543			relativeTo = -1
544		while not atEnd:
545			rect = self.editor.cursorRect(cursor)
546			if rect.top() >= height:
547				break
548			number = str(cursor.blockNumber() - relativeTo).replace('-', '−')
549			painter.drawText(0, rect.top(), self.width() - 2,
550			                 fontHeight, Qt.AlignmentFlag.AlignRight, number)
551			cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock)
552			atEnd = cursor.atEnd()
553			if not atEnd:
554				cursor.movePosition(QTextCursor.MoveOperation.NextBlock)
555
556class InfoArea(QLabel):
557	def __init__(self, editor, baseColor):
558		QWidget.__init__(self, editor)
559		self.editor = editor
560		self.editor.cursorPositionChanged.connect(self.updateTextAndGeometry)
561		self.updateTextAndGeometry()
562		self.setAutoFillBackground(True)
563		self.baseColor = baseColor
564		palette = self.palette()
565		palette.setColor(QPalette.ColorRole.Window, self.baseColor)
566		self.setPalette(palette)
567		self.setCursor(Qt.CursorShape.IBeamCursor)
568
569	def updateTextAndGeometry(self):
570		text = self.getText()
571		(w, h) = self.getAreaSize(text)
572		(x, y) = self.getAreaPosition(w, h)
573		self.setText(text)
574		self.resize(w, h)
575		self.move(x, y)
576		self.setVisible(not globalSettings.useFakeVim)
577
578	def getAreaSize(self, text):
579		metrics = self.fontMetrics()
580		width = metrics.horizontalAdvance(text)
581		height = metrics.height()
582		return width, height
583
584	def getAreaPosition(self, width, height):
585		return 0, 0
586
587	def getText(self):
588		return ""
589
590	def enterEvent(self, event):
591		palette = self.palette()
592		windowColor = QColor(self.baseColor)
593		windowColor.setAlpha(0x20)
594		palette.setColor(QPalette.ColorRole.Window, windowColor)
595		textColor = palette.color(QPalette.ColorRole.WindowText)
596		textColor.setAlpha(0x20)
597		palette.setColor(QPalette.ColorRole.WindowText, textColor)
598		self.setPalette(palette)
599
600	def leaveEvent(self, event):
601		palette = self.palette()
602		palette.setColor(QPalette.ColorRole.Window, self.baseColor)
603		palette.setColor(QPalette.ColorRole.WindowText,
604			self.editor.palette().color(QPalette.ColorRole.WindowText))
605		self.setPalette(palette)
606
607	def mousePressEvent(self, event):
608		pos = self.mapToParent(event.pos())
609		pos.setX(pos.x() - self.editor.lineNumberAreaWidth())
610		newEvent = QMouseEvent(event.type(), pos,
611		                       event.button(), event.buttons(),
612		                       event.modifiers())
613		self.editor.mousePressEvent(newEvent)
614
615	mouseReleaseEvent = mousePressEvent
616	mouseDoubleClickEvent = mousePressEvent
617	mouseMoveEvent = mousePressEvent
618
619class LineInfoArea(InfoArea):
620	def __init__(self, editor):
621		InfoArea.__init__(self, editor, getColor('infoArea'))
622
623	def getAreaPosition(self, width, height):
624		viewport = self.editor.viewport()
625		rightSide = viewport.width() + self.editor.lineNumberAreaWidth()
626		if globalSettings.documentStatsEnabled:
627			return rightSide - width, viewport.height() - (2 * height)
628		else:
629			return rightSide - width, viewport.height() - height
630
631	def getText(self):
632		template = '%d : %d'
633		cursor = self.editor.textCursor()
634		block = cursor.blockNumber() + 1
635		position = cursor.positionInBlock()
636		return template % (block, position)
637
638
639class TextInfoArea(InfoArea):
640	def __init__(self, editor):
641		InfoArea.__init__(self, editor, getColor('statsArea'))
642
643	def getAreaPosition(self, width, height):
644		viewport = self.editor.viewport()
645		rightSide = viewport.width() + self.editor.lineNumberAreaWidth()
646		return rightSide - width, viewport.height() - height
647
648	def getText(self):
649		if not globalSettings.documentStatsEnabled:
650			return
651		template = self.tr('%d w | %d a | %d c',
652		                   'count of words, alphanumeric characters, all characters')
653		words, alphaNums, characters = self.editor.statistics
654		return template % (words, alphaNums, characters)
655