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