1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2008 - 2013 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a simple terminal based on QScintilla. |
|
8 """ |
|
9 |
|
10 import sys |
|
11 import os |
|
12 import re |
|
13 |
|
14 from PyQt4.QtCore import pyqtSignal, QSignalMapper, QTimer, QByteArray, QProcess, Qt, \ |
|
15 QEvent |
|
16 from PyQt4.QtGui import QDialog, QInputDialog, QApplication, QMenu, QPalette, QFont, \ |
|
17 QWidget, QHBoxLayout, QShortcut |
|
18 from PyQt4.Qsci import QsciScintilla |
|
19 |
|
20 from E5Gui.E5Application import e5App |
|
21 |
|
22 from .QsciScintillaCompat import QsciScintillaCompat |
|
23 |
|
24 import Preferences |
|
25 import Utilities |
|
26 |
|
27 import UI.PixmapCache |
|
28 |
|
29 |
|
30 class TerminalAssembly(QWidget): |
|
31 """ |
|
32 Class implementing the containing widget for the terminal. |
|
33 """ |
|
34 def __init__(self, vm, parent=None): |
|
35 """ |
|
36 Constructor |
|
37 |
|
38 @param vm reference to the viewmanager object |
|
39 @param parent reference to the parent widget (QWidget) |
|
40 """ |
|
41 super().__init__(parent) |
|
42 |
|
43 self.setWindowIcon(UI.PixmapCache.getIcon("eric.png")) |
|
44 |
|
45 self.__terminal = Terminal(vm, self) |
|
46 |
|
47 from UI.SearchWidget import SearchWidget |
|
48 self.__searchWidget = SearchWidget(self.__terminal, self) |
|
49 self.__searchWidget.hide() |
|
50 |
|
51 self.__layout = QHBoxLayout(self) |
|
52 self.__layout.setContentsMargins(1, 1, 1, 1) |
|
53 self.__layout.addWidget(self.__terminal) |
|
54 self.__layout.addWidget(self.__searchWidget) |
|
55 |
|
56 self.__searchWidget.searchNext.connect(self.__terminal.searchNext) |
|
57 self.__searchWidget.searchPrevious.connect(self.__terminal.searchPrev) |
|
58 self.__terminal.searchStringFound.connect(self.__searchWidget.searchStringFound) |
|
59 |
|
60 def showFind(self, txt=""): |
|
61 """ |
|
62 Public method to display the search widget. |
|
63 |
|
64 @param txt text to be shown in the combo (string) |
|
65 """ |
|
66 self.__searchWidget.showFind(txt) |
|
67 |
|
68 def terminal(self): |
|
69 """ |
|
70 Public method to get a reference to the terminal widget. |
|
71 |
|
72 @return reference to the terminal widget (Terminal) |
|
73 """ |
|
74 return self.__terminal |
|
75 |
|
76 |
|
77 class Terminal(QsciScintillaCompat): |
|
78 """ |
|
79 Class implementing a simple terminal based on QScintilla. |
|
80 |
|
81 A user can enter commands that are executed by a shell process. |
|
82 |
|
83 @signal searchStringFound(found) emitted to indicate the search result (boolean) |
|
84 """ |
|
85 searchStringFound = pyqtSignal(bool) |
|
86 |
|
87 def __init__(self, vm, parent=None): |
|
88 """ |
|
89 Constructor |
|
90 |
|
91 @param vm reference to the viewmanager object |
|
92 @param parent parent widget (QWidget) |
|
93 """ |
|
94 super().__init__(parent) |
|
95 self.setUtf8(True) |
|
96 |
|
97 self.vm = vm |
|
98 self.__mainWindow = parent |
|
99 self.__lastSearch = () |
|
100 |
|
101 self.linesepRegExp = r"\r\n|\n|\r" |
|
102 |
|
103 self.setWindowTitle(self.trUtf8('Terminal')) |
|
104 |
|
105 self.setWhatsThis(self.trUtf8( |
|
106 """<b>The Terminal Window</b>""" |
|
107 """<p>This is a very simple terminal like window, that runs a shell""" |
|
108 """ process in the background.</p>""" |
|
109 """<p>The process can be stopped and started via the context menu. Some""" |
|
110 """ Ctrl command may be sent as well. However, the shell may ignore""" |
|
111 """ them.</p>""" |
|
112 """<p>You can use the cursor keys while entering commands. There is also a""" |
|
113 """ history of commands that can be recalled using the up and down cursor""" |
|
114 """ keys. Pressing the up or down key after some text has been entered will""" |
|
115 """ start an incremental search.</p>""" |
|
116 )) |
|
117 |
|
118 self.ansi_re = re.compile("\033\[\??[\d;]*\w") |
|
119 |
|
120 # Initialise instance variables. |
|
121 self.prline = 0 |
|
122 self.prcol = 0 |
|
123 self.inDragDrop = False |
|
124 self.lexer_ = None |
|
125 |
|
126 # Initialize history |
|
127 self.maxHistoryEntries = Preferences.getTerminal("MaxHistoryEntries") |
|
128 self.history = [] |
|
129 self.histidx = -1 |
|
130 |
|
131 # clear QScintilla defined keyboard commands |
|
132 # we do our own handling through the view manager |
|
133 self.clearAlternateKeys() |
|
134 self.clearKeys() |
|
135 self.__actionsAdded = False |
|
136 |
|
137 # Create the history context menu |
|
138 self.hmenu = QMenu(self.trUtf8('History')) |
|
139 self.hmenu.addAction(self.trUtf8('Select entry'), self.__selectHistory) |
|
140 self.hmenu.addAction(self.trUtf8('Show'), self.__showHistory) |
|
141 self.hmenu.addAction(self.trUtf8('Clear'), self.__clearHistory) |
|
142 |
|
143 # Create a little context menu to send Ctrl-C, Ctrl-D or Ctrl-Z |
|
144 self.csm = QSignalMapper(self) |
|
145 self.csm.mapped[int].connect(self.__sendCtrl) |
|
146 |
|
147 self.cmenu = QMenu(self.trUtf8('Ctrl Commands')) |
|
148 act = self.cmenu.addAction(self.trUtf8('Ctrl-C')) |
|
149 self.csm.setMapping(act, 3) |
|
150 act.triggered[()].connect(self.csm.map) |
|
151 act = self.cmenu.addAction(self.trUtf8('Ctrl-D')) |
|
152 self.csm.setMapping(act, 4) |
|
153 act.triggered[()].connect(self.csm.map) |
|
154 act = self.cmenu.addAction(self.trUtf8('Ctrl-Z')) |
|
155 self.csm.setMapping(act, 26) |
|
156 act.triggered[()].connect(self.csm.map) |
|
157 |
|
158 # Create a little context menu |
|
159 self.menu = QMenu(self) |
|
160 self.menu.addAction(self.trUtf8('Cut'), self.cut) |
|
161 self.menu.addAction(self.trUtf8('Copy'), self.copy) |
|
162 self.menu.addAction(self.trUtf8('Paste'), self.paste) |
|
163 self.menu.addSeparator() |
|
164 self.menu.addAction(self.trUtf8('Find'), self.__find) |
|
165 self.menu.addSeparator() |
|
166 self.menu.addMenu(self.hmenu) |
|
167 self.menu.addSeparator() |
|
168 self.menu.addAction(self.trUtf8('Clear'), self.clear) |
|
169 self.__startAct = self.menu.addAction(self.trUtf8("Start"), self.__startShell) |
|
170 self.__stopAct = self.menu.addAction(self.trUtf8("Stop"), self.__stopShell) |
|
171 self.__resetAct = self.menu.addAction(self.trUtf8('Reset'), self.__reset) |
|
172 self.menu.addSeparator() |
|
173 self.__ctrlAct = self.menu.addMenu(self.cmenu) |
|
174 self.menu.addSeparator() |
|
175 self.menu.addAction(self.trUtf8("Configure..."), self.__configure) |
|
176 |
|
177 self.__bindLexer() |
|
178 self.__setTextDisplay() |
|
179 self.__setMargin0() |
|
180 |
|
181 self.setWindowIcon(UI.PixmapCache.getIcon("eric.png")) |
|
182 |
|
183 self.incrementalSearchString = "" |
|
184 self.incrementalSearchActive = False |
|
185 |
|
186 self.supportedEditorCommands = { |
|
187 QsciScintilla.SCI_LINEDELETE: self.__clearCurrentLine, |
|
188 QsciScintilla.SCI_NEWLINE: self.__QScintillaNewline, |
|
189 |
|
190 QsciScintilla.SCI_DELETEBACK: self.__QScintillaDeleteBack, |
|
191 QsciScintilla.SCI_CLEAR: self.__QScintillaDelete, |
|
192 QsciScintilla.SCI_DELWORDLEFT: self.__QScintillaDeleteWordLeft, |
|
193 QsciScintilla.SCI_DELWORDRIGHT: self.__QScintillaDeleteWordRight, |
|
194 QsciScintilla.SCI_DELLINELEFT: self.__QScintillaDeleteLineLeft, |
|
195 QsciScintilla.SCI_DELLINERIGHT: self.__QScintillaDeleteLineRight, |
|
196 |
|
197 QsciScintilla.SCI_CHARLEFT: self.__QScintillaCharLeft, |
|
198 QsciScintilla.SCI_CHARRIGHT: self.__QScintillaCharRight, |
|
199 QsciScintilla.SCI_WORDLEFT: self.__QScintillaWordLeft, |
|
200 QsciScintilla.SCI_WORDRIGHT: self.__QScintillaWordRight, |
|
201 QsciScintilla.SCI_VCHOME: self.__QScintillaVCHome, |
|
202 QsciScintilla.SCI_LINEEND: self.__QScintillaLineEnd, |
|
203 QsciScintilla.SCI_LINEUP: self.__QScintillaLineUp, |
|
204 QsciScintilla.SCI_LINEDOWN: self.__QScintillaLineDown, |
|
205 |
|
206 QsciScintilla.SCI_CHARLEFTEXTEND: self.__QScintillaCharLeftExtend, |
|
207 QsciScintilla.SCI_CHARRIGHTEXTEND: self.extendSelectionRight, |
|
208 QsciScintilla.SCI_WORDLEFTEXTEND: self.__QScintillaWordLeftExtend, |
|
209 QsciScintilla.SCI_WORDRIGHTEXTEND: self.extendSelectionWordRight, |
|
210 QsciScintilla.SCI_VCHOMEEXTEND: self.__QScintillaVCHomeExtend, |
|
211 QsciScintilla.SCI_LINEENDEXTEND: self.extendSelectionToEOL, |
|
212 } |
|
213 |
|
214 self.__ioEncoding = Preferences.getSystem("IOEncoding") |
|
215 |
|
216 self.__process = QProcess() |
|
217 self.__process.setProcessChannelMode(QProcess.MergedChannels) |
|
218 self.__process.setReadChannel(QProcess.StandardOutput) |
|
219 |
|
220 self.__process.readyReadStandardOutput.connect(self.__readOutput) |
|
221 self.__process.started.connect(self.__started) |
|
222 self.__process.finished.connect(self.__finished) |
|
223 |
|
224 self.__ctrl = {} |
|
225 for ascii_number, letter in enumerate("abcdefghijklmnopqrstuvwxyz"): |
|
226 self.__ctrl[letter] = chr(ascii_number + 1) |
|
227 |
|
228 self.__lastPos = (0, 0) |
|
229 |
|
230 self.grabGesture(Qt.PinchGesture) |
|
231 |
|
232 self.__startShell() |
|
233 |
|
234 def __readOutput(self): |
|
235 """ |
|
236 Private method to process the output of the shell. |
|
237 """ |
|
238 output = str(self.__process.readAllStandardOutput(), |
|
239 self.__ioEncoding, 'replace') |
|
240 self.__write(self.ansi_re.sub("", output)) |
|
241 self.__lastPos = self.__getEndPos() |
|
242 |
|
243 def __started(self): |
|
244 """ |
|
245 Private method called, when the shell process has started. |
|
246 """ |
|
247 if not Utilities.isWindowsPlatform(): |
|
248 QTimer.singleShot(250, self.clear) |
|
249 |
|
250 self.__startAct.setEnabled(False) |
|
251 self.__stopAct.setEnabled(True) |
|
252 self.__resetAct.setEnabled(True) |
|
253 self.__ctrlAct.setEnabled(True) |
|
254 |
|
255 def __finished(self): |
|
256 """ |
|
257 Private method called, when the shell process has finished. |
|
258 """ |
|
259 super().clear() |
|
260 |
|
261 self.__startAct.setEnabled(True) |
|
262 self.__stopAct.setEnabled(False) |
|
263 self.__resetAct.setEnabled(False) |
|
264 self.__ctrlAct.setEnabled(False) |
|
265 |
|
266 def __send(self, data): |
|
267 """ |
|
268 Private method to send data to the shell process. |
|
269 |
|
270 @param data data to be sent to the shell process (string) |
|
271 """ |
|
272 pdata = QByteArray() |
|
273 pdata.append(bytes(data, encoding="utf-8")) |
|
274 self.__process.write(pdata) |
|
275 |
|
276 def __sendCtrl(self, cmd): |
|
277 """ |
|
278 Private slot to send a control command to the shell process. |
|
279 |
|
280 @param the control command to be sent (integer) |
|
281 """ |
|
282 self.__send(chr(cmd)) |
|
283 |
|
284 def closeTerminal(self): |
|
285 """ |
|
286 Public method to shutdown the terminal. |
|
287 """ |
|
288 self.__stopShell() |
|
289 self.saveHistory() |
|
290 |
|
291 def __bindLexer(self): |
|
292 """ |
|
293 Private slot to set the lexer. |
|
294 """ |
|
295 if Utilities.isWindowsPlatform(): |
|
296 self.language = "Batch" |
|
297 else: |
|
298 self.language = "Bash" |
|
299 if Preferences.getTerminal("SyntaxHighlightingEnabled"): |
|
300 from . import Lexers |
|
301 self.lexer_ = Lexers.getLexer(self.language, self) |
|
302 else: |
|
303 self.lexer_ = None |
|
304 |
|
305 if self.lexer_ is None: |
|
306 self.setLexer(None) |
|
307 font = Preferences.getTerminal("MonospacedFont") |
|
308 self.monospacedStyles(font) |
|
309 return |
|
310 |
|
311 # get the font for style 0 and set it as the default font |
|
312 key = 'Scintilla/{0}/style0/font'.format(self.lexer_.language()) |
|
313 fdesc = Preferences.Prefs.settings.value(key) |
|
314 if fdesc is not None: |
|
315 font = QFont(fdesc[0], int(fdesc[1])) |
|
316 self.lexer_.setDefaultFont(font) |
|
317 self.setLexer(self.lexer_) |
|
318 self.lexer_.readSettings(Preferences.Prefs.settings, "Scintilla") |
|
319 |
|
320 self.lexer_.setDefaultColor(self.lexer_.color(0)) |
|
321 self.lexer_.setDefaultPaper(self.lexer_.paper(0)) |
|
322 |
|
323 def __setMargin0(self): |
|
324 """ |
|
325 Private method to configure margin 0. |
|
326 """ |
|
327 # set the settings for all margins |
|
328 self.setMarginsFont(Preferences.getTerminal("MarginsFont")) |
|
329 self.setMarginsForegroundColor(Preferences.getEditorColour("MarginsForeground")) |
|
330 self.setMarginsBackgroundColor(Preferences.getEditorColour("MarginsBackground")) |
|
331 |
|
332 # set margin 0 settings |
|
333 linenoMargin = Preferences.getTerminal("LinenoMargin") |
|
334 self.setMarginLineNumbers(0, linenoMargin) |
|
335 if linenoMargin: |
|
336 self.setMarginWidth(0, ' ' + '8' * Preferences.getTerminal("LinenoWidth")) |
|
337 else: |
|
338 self.setMarginWidth(0, 0) |
|
339 |
|
340 # disable margins 1 and 2 |
|
341 self.setMarginWidth(1, 0) |
|
342 self.setMarginWidth(2, 0) |
|
343 |
|
344 def __setTextDisplay(self): |
|
345 """ |
|
346 Private method to configure the text display. |
|
347 """ |
|
348 self.setTabWidth(Preferences.getEditor("TabWidth")) |
|
349 if Preferences.getEditor("ShowWhitespace"): |
|
350 self.setWhitespaceVisibility(QsciScintilla.WsVisible) |
|
351 try: |
|
352 self.setWhitespaceForegroundColor( |
|
353 Preferences.getEditorColour("WhitespaceForeground")) |
|
354 self.setWhitespaceBackgroundColor( |
|
355 Preferences.getEditorColour("WhitespaceBackground")) |
|
356 self.setWhitespaceSize( |
|
357 Preferences.getEditor("WhitespaceSize")) |
|
358 except AttributeError: |
|
359 # QScintilla before 2.5 doesn't support this |
|
360 pass |
|
361 else: |
|
362 self.setWhitespaceVisibility(QsciScintilla.WsInvisible) |
|
363 self.setEolVisibility(Preferences.getEditor("ShowEOL")) |
|
364 if Preferences.getEditor("BraceHighlighting"): |
|
365 self.setBraceMatching(QsciScintilla.SloppyBraceMatch) |
|
366 else: |
|
367 self.setBraceMatching(QsciScintilla.NoBraceMatch) |
|
368 self.setMatchedBraceForegroundColor( |
|
369 Preferences.getEditorColour("MatchingBrace")) |
|
370 self.setMatchedBraceBackgroundColor( |
|
371 Preferences.getEditorColour("MatchingBraceBack")) |
|
372 self.setUnmatchedBraceForegroundColor( |
|
373 Preferences.getEditorColour("NonmatchingBrace")) |
|
374 self.setUnmatchedBraceBackgroundColor( |
|
375 Preferences.getEditorColour("NonmatchingBraceBack")) |
|
376 if Preferences.getEditor("CustomSelectionColours"): |
|
377 self.setSelectionBackgroundColor( |
|
378 Preferences.getEditorColour("SelectionBackground")) |
|
379 else: |
|
380 self.setSelectionBackgroundColor( |
|
381 QApplication.palette().color(QPalette.Highlight)) |
|
382 if Preferences.getEditor("ColourizeSelText"): |
|
383 self.resetSelectionForegroundColor() |
|
384 elif Preferences.getEditor("CustomSelectionColours"): |
|
385 self.setSelectionForegroundColor( |
|
386 Preferences.getEditorColour("SelectionForeground")) |
|
387 else: |
|
388 self.setSelectionForegroundColor( |
|
389 QApplication.palette().color(QPalette.HighlightedText)) |
|
390 self.setSelectionToEol(Preferences.getEditor("ExtendSelectionToEol")) |
|
391 self.setCaretForegroundColor( |
|
392 Preferences.getEditorColour("CaretForeground")) |
|
393 self.setCaretLineBackgroundColor( |
|
394 Preferences.getEditorColour("CaretLineBackground")) |
|
395 self.setCaretLineVisible(Preferences.getEditor("CaretLineVisible")) |
|
396 self.caretWidth = Preferences.getEditor("CaretWidth") |
|
397 self.setCaretWidth(self.caretWidth) |
|
398 self.setWrapMode(QsciScintilla.WrapNone) |
|
399 self.useMonospaced = Preferences.getTerminal("UseMonospacedFont") |
|
400 self.__setMonospaced(self.useMonospaced) |
|
401 |
|
402 self.setCursorFlashTime(QApplication.cursorFlashTime()) |
|
403 |
|
404 if Preferences.getEditor("OverrideEditAreaColours"): |
|
405 self.setColor(Preferences.getEditorColour("EditAreaForeground")) |
|
406 self.setPaper(Preferences.getEditorColour("EditAreaBackground")) |
|
407 |
|
408 def __setMonospaced(self, on): |
|
409 """ |
|
410 Private method to set/reset a monospaced font. |
|
411 |
|
412 @param on flag to indicate usage of a monospace font (boolean) |
|
413 """ |
|
414 if on: |
|
415 f = Preferences.getTerminal("MonospacedFont") |
|
416 self.monospacedStyles(f) |
|
417 else: |
|
418 if not self.lexer_: |
|
419 self.clearStyles() |
|
420 self.__setMargin0() |
|
421 self.setFont(Preferences.getTerminal("MonospacedFont")) |
|
422 |
|
423 self.useMonospaced = on |
|
424 |
|
425 def loadHistory(self): |
|
426 """ |
|
427 Public method to load the history. |
|
428 """ |
|
429 hl = Preferences.Prefs.settings.value("Terminal/History") |
|
430 if hl is not None: |
|
431 self.history = hl[-self.maxHistoryEntries:] |
|
432 else: |
|
433 self.history = [] |
|
434 |
|
435 def reloadHistory(self): |
|
436 """ |
|
437 Public method to reload the history. |
|
438 """ |
|
439 self.loadHistory(self.clientType) |
|
440 self.history = self.historyLists[self.clientType] |
|
441 self.histidx = -1 |
|
442 |
|
443 def saveHistory(self): |
|
444 """ |
|
445 Public method to save the history. |
|
446 """ |
|
447 Preferences.Prefs.settings.setValue("Terminal/History", self.history) |
|
448 |
|
449 def getHistory(self): |
|
450 """ |
|
451 Public method to get the history. |
|
452 |
|
453 @return reference to the history list (list of strings) |
|
454 """ |
|
455 return self.history |
|
456 |
|
457 def __clearHistory(self): |
|
458 """ |
|
459 Private slot to clear the current history. |
|
460 """ |
|
461 self.history = [] |
|
462 |
|
463 def __selectHistory(self): |
|
464 """ |
|
465 Private slot to select a history entry to execute. |
|
466 """ |
|
467 cmd, ok = QInputDialog.getItem( |
|
468 self, |
|
469 self.trUtf8("Select History"), |
|
470 self.trUtf8("Select the history entry to execute (most recent shown last)."), |
|
471 self.history, |
|
472 0, False) |
|
473 if ok: |
|
474 self.__insertHistory(cmd) |
|
475 |
|
476 def __showHistory(self): |
|
477 """ |
|
478 Private slot to show the shell history dialog. |
|
479 """ |
|
480 from .ShellHistoryDialog import ShellHistoryDialog |
|
481 dlg = ShellHistoryDialog(self.history, self.vm, self) |
|
482 if dlg.exec_() == QDialog.Accepted: |
|
483 self.history = dlg.getHistory() |
|
484 self.histidx = -1 |
|
485 |
|
486 def __getEndPos(self): |
|
487 """ |
|
488 Private method to return the line and column of the last character. |
|
489 |
|
490 @return tuple of two values (int, int) giving the line and column |
|
491 """ |
|
492 line = self.lines() - 1 |
|
493 return (line, len(self.text(line))) |
|
494 |
|
495 def __write(self, s): |
|
496 """ |
|
497 Private method to display some text. |
|
498 |
|
499 @param s text to be displayed (string) |
|
500 """ |
|
501 line, col = self.__getEndPos() |
|
502 self.setCursorPosition(line, col) |
|
503 self.insert(s) |
|
504 self.prline, self.prcol = self.getCursorPosition() |
|
505 self.ensureCursorVisible() |
|
506 self.ensureLineVisible(self.prline) |
|
507 |
|
508 def __clearCurrentLine(self): |
|
509 """ |
|
510 Private method to clear the line containing the cursor. |
|
511 """ |
|
512 line, col = self.getCursorPosition() |
|
513 if self.text(line).startswith(sys.ps1): |
|
514 col = len(sys.ps1) |
|
515 elif self.text(line).startswith(sys.ps2): |
|
516 col = len(sys.ps2) |
|
517 else: |
|
518 col = 0 |
|
519 self.setCursorPosition(line, col) |
|
520 self.deleteLineRight() |
|
521 |
|
522 def __insertText(self, s): |
|
523 """ |
|
524 Private method to insert some text at the current cursor position. |
|
525 |
|
526 @param s text to be inserted (string) |
|
527 """ |
|
528 line, col = self.getCursorPosition() |
|
529 self.insertAt(s, line, col) |
|
530 self.setCursorPosition(line, col + len(s)) |
|
531 |
|
532 def __insertTextAtEnd(self, s): |
|
533 """ |
|
534 Private method to insert some text at the end of the command line. |
|
535 |
|
536 @param s text to be inserted (string) |
|
537 """ |
|
538 line, col = self.__getEndPos() |
|
539 self.setCursorPosition(line, col) |
|
540 self.insert(s) |
|
541 self.prline, self.prcol = self.getCursorPosition() |
|
542 |
|
543 def mousePressEvent(self, event): |
|
544 """ |
|
545 Protected method to handle the mouse press event. |
|
546 |
|
547 @param event the mouse press event (QMouseEvent) |
|
548 """ |
|
549 self.setFocus() |
|
550 super().mousePressEvent(event) |
|
551 |
|
552 def wheelEvent(self, evt): |
|
553 """ |
|
554 Protected method to handle wheel events. |
|
555 |
|
556 @param evt reference to the wheel event (QWheelEvent) |
|
557 """ |
|
558 if evt.modifiers() & Qt.ControlModifier: |
|
559 if evt.delta() < 0: |
|
560 self.zoomOut() |
|
561 else: |
|
562 self.zoomIn() |
|
563 evt.accept() |
|
564 return |
|
565 |
|
566 super().wheelEvent(evt) |
|
567 |
|
568 |
|
569 def event(self, evt): |
|
570 """ |
|
571 Protected method handling events. |
|
572 |
|
573 @param evt reference to the event (QEvent) |
|
574 @return flag indicating, if the event was handled (boolean) |
|
575 """ |
|
576 if evt.type() == QEvent.Gesture: |
|
577 self.gestureEvent(evt) |
|
578 return True |
|
579 |
|
580 return super().event(evt) |
|
581 |
|
582 def gestureEvent(self, evt): |
|
583 """ |
|
584 Protected method handling gesture events. |
|
585 |
|
586 @param evt reference to the gesture event (QGestureEvent |
|
587 """ |
|
588 pinch = evt.gesture(Qt.PinchGesture) |
|
589 if pinch: |
|
590 if pinch.state() == Qt.GestureStarted: |
|
591 zoom = (self.getZoom() + 10) / 10.0 |
|
592 pinch.setScaleFactor(zoom) |
|
593 else: |
|
594 zoom = int(pinch.scaleFactor() * 10) - 10 |
|
595 if zoom <= -9: |
|
596 zoom = -9 |
|
597 pinch.setScaleFactor(0.1) |
|
598 elif zoom >= 20: |
|
599 zoom = 20 |
|
600 pinch.setScaleFactor(3.0) |
|
601 self.zoomTo(zoom) |
|
602 evt.accept() |
|
603 |
|
604 def editorCommand(self, cmd): |
|
605 """ |
|
606 Public method to perform an editor command. |
|
607 |
|
608 @param cmd the scintilla command to be performed |
|
609 """ |
|
610 try: |
|
611 self.supportedEditorCommands[cmd]() |
|
612 except TypeError: |
|
613 self.supportedEditorCommands[cmd](cmd) |
|
614 except KeyError: |
|
615 pass |
|
616 |
|
617 def __isCursorOnLastLine(self): |
|
618 """ |
|
619 Private method to check, if the cursor is on the last line. |
|
620 """ |
|
621 cline, ccol = self.getCursorPosition() |
|
622 return cline == self.lines() - 1 |
|
623 |
|
624 def keyPressEvent(self, ev): |
|
625 """ |
|
626 Re-implemented to handle the user input a key at a time. |
|
627 |
|
628 @param ev key event (QKeyEvent) |
|
629 """ |
|
630 txt = ev.text() |
|
631 |
|
632 # See it is text to insert. |
|
633 if len(txt) and txt >= " ": |
|
634 if not self.__isCursorOnLastLine(): |
|
635 line, col = self.__getEndPos() |
|
636 self.setCursorPosition(line, col) |
|
637 self.prline, self.prcol = self.getCursorPosition() |
|
638 super().keyPressEvent(ev) |
|
639 self.incrementalSearchActive = True |
|
640 else: |
|
641 ev.ignore() |
|
642 |
|
643 def __QScintillaLeftDeleteCommand(self, method): |
|
644 """ |
|
645 Private method to handle a QScintilla delete command working to the left. |
|
646 |
|
647 @param method shell method to execute |
|
648 """ |
|
649 if self.__isCursorOnLastLine(): |
|
650 line, col = self.getCursorPosition() |
|
651 if col > self.__lastPos[1]: |
|
652 method() |
|
653 |
|
654 def __QScintillaDeleteBack(self): |
|
655 """ |
|
656 Private method to handle the Backspace key. |
|
657 """ |
|
658 self.__QScintillaLeftDeleteCommand(self.deleteBack) |
|
659 |
|
660 def __QScintillaDeleteWordLeft(self): |
|
661 """ |
|
662 Private method to handle the Delete Word Left command. |
|
663 """ |
|
664 self.__QScintillaLeftDeleteCommand(self.deleteWordLeft) |
|
665 |
|
666 def __QScintillaDelete(self): |
|
667 """ |
|
668 Private method to handle the delete command. |
|
669 """ |
|
670 if self.__isCursorOnLastLine(): |
|
671 if self.hasSelectedText(): |
|
672 lineFrom, indexFrom, lineTo, indexTo = self.getSelection() |
|
673 if indexFrom >= self.__lastPos[1]: |
|
674 self.delete() |
|
675 self.setSelection(lineTo, indexTo, lineTo, indexTo) |
|
676 else: |
|
677 self.delete() |
|
678 |
|
679 def __QScintillaDeleteLineLeft(self): |
|
680 """ |
|
681 Private method to handle the Delete Line Left command. |
|
682 """ |
|
683 if self.__isCursorOnLastLine(): |
|
684 if self.isListActive(): |
|
685 self.cancelList() |
|
686 |
|
687 line, col = self.getCursorPosition() |
|
688 prompt = self.text(line)[:self.__lastPos[1]] |
|
689 self.deleteLineLeft() |
|
690 self.insertAt(prompt, line, 0) |
|
691 self.setCursorPosition(line, len(prompt)) |
|
692 |
|
693 def __QScintillaNewline(self, cmd): |
|
694 """ |
|
695 Private method to handle the Return key. |
|
696 |
|
697 @param cmd QScintilla command |
|
698 """ |
|
699 if self.__isCursorOnLastLine(): |
|
700 self.incrementalSearchString = "" |
|
701 self.incrementalSearchActive = False |
|
702 line, col = self.__getEndPos() |
|
703 self.setCursorPosition(line, col) |
|
704 self.setSelection(*(self.__lastPos + self.getCursorPosition())) |
|
705 buf = self.selectedText() |
|
706 self.setCursorPosition(line, col) # select nothin |
|
707 self.insert('\n') |
|
708 self.__executeCommand(buf) |
|
709 |
|
710 def __QScintillaLeftCommand(self, method, allLinesAllowed=False): |
|
711 """ |
|
712 Private method to handle a QScintilla command working to the left. |
|
713 |
|
714 @param method shell method to execute |
|
715 """ |
|
716 if self.__isCursorOnLastLine() or allLinesAllowed: |
|
717 line, col = self.getCursorPosition() |
|
718 if col > self.__lastPos[1]: |
|
719 method() |
|
720 |
|
721 def __QScintillaCharLeft(self): |
|
722 """ |
|
723 Private method to handle the Cursor Left command. |
|
724 """ |
|
725 self.__QScintillaLeftCommand(self.moveCursorLeft) |
|
726 |
|
727 def __QScintillaWordLeft(self): |
|
728 """ |
|
729 Private method to handle the Cursor Word Left command. |
|
730 """ |
|
731 self.__QScintillaLeftCommand(self.moveCursorWordLeft) |
|
732 |
|
733 def __QScintillaRightCommand(self, method): |
|
734 """ |
|
735 Private method to handle a QScintilla command working to the right. |
|
736 |
|
737 @param method shell method to execute |
|
738 """ |
|
739 if self.__isCursorOnLastLine(): |
|
740 method() |
|
741 |
|
742 def __QScintillaCharRight(self): |
|
743 """ |
|
744 Private method to handle the Cursor Right command. |
|
745 """ |
|
746 self.__QScintillaRightCommand(self.moveCursorRight) |
|
747 |
|
748 def __QScintillaWordRight(self): |
|
749 """ |
|
750 Private method to handle the Cursor Word Right command. |
|
751 """ |
|
752 self.__QScintillaRightCommand(self.moveCursorWordRight) |
|
753 |
|
754 def __QScintillaDeleteWordRight(self): |
|
755 """ |
|
756 Private method to handle the Delete Word Right command. |
|
757 """ |
|
758 self.__QScintillaRightCommand(self.deleteWordRight) |
|
759 |
|
760 def __QScintillaDeleteLineRight(self): |
|
761 """ |
|
762 Private method to handle the Delete Line Right command. |
|
763 """ |
|
764 self.__QScintillaRightCommand(self.deleteLineRight) |
|
765 |
|
766 def __QScintillaVCHome(self, cmd): |
|
767 """ |
|
768 Private method to handle the Home key. |
|
769 |
|
770 @param cmd QScintilla command |
|
771 """ |
|
772 self.setCursorPosition(*self.__lastPos) |
|
773 |
|
774 def __QScintillaLineEnd(self, cmd): |
|
775 """ |
|
776 Private method to handle the End key. |
|
777 |
|
778 @param cmd QScintilla command |
|
779 """ |
|
780 self.moveCursorToEOL() |
|
781 |
|
782 def __QScintillaLineUp(self, cmd): |
|
783 """ |
|
784 Private method to handle the Up key. |
|
785 |
|
786 @param cmd QScintilla command |
|
787 """ |
|
788 line, col = self.__getEndPos() |
|
789 buf = self.text(line)[self.__lastPos[1]:] |
|
790 if buf and self.incrementalSearchActive: |
|
791 if self.incrementalSearchString: |
|
792 idx = self.__rsearchHistory(self.incrementalSearchString, |
|
793 self.histidx) |
|
794 if idx >= 0: |
|
795 self.histidx = idx |
|
796 self.__useHistory() |
|
797 else: |
|
798 idx = self.__rsearchHistory(buf) |
|
799 if idx >= 0: |
|
800 self.histidx = idx |
|
801 self.incrementalSearchString = buf |
|
802 self.__useHistory() |
|
803 else: |
|
804 if self.histidx < 0: |
|
805 self.histidx = len(self.history) |
|
806 if self.histidx > 0: |
|
807 self.histidx = self.histidx - 1 |
|
808 self.__useHistory() |
|
809 |
|
810 def __QScintillaLineDown(self, cmd): |
|
811 """ |
|
812 Private method to handle the Down key. |
|
813 |
|
814 @param cmd QScintilla command |
|
815 """ |
|
816 line, col = self.__getEndPos() |
|
817 buf = self.text(line)[self.__lastPos[1]:] |
|
818 if buf and self.incrementalSearchActive: |
|
819 if self.incrementalSearchString: |
|
820 idx = self.__searchHistory(self.incrementalSearchString, self.histidx) |
|
821 if idx >= 0: |
|
822 self.histidx = idx |
|
823 self.__useHistory() |
|
824 else: |
|
825 idx = self.__searchHistory(buf) |
|
826 if idx >= 0: |
|
827 self.histidx = idx |
|
828 self.incrementalSearchString = buf |
|
829 self.__useHistory() |
|
830 else: |
|
831 if self.histidx >= 0 and self.histidx < len(self.history): |
|
832 self.histidx += 1 |
|
833 self.__useHistory() |
|
834 |
|
835 def __QScintillaCharLeftExtend(self): |
|
836 """ |
|
837 Private method to handle the Extend Selection Left command. |
|
838 """ |
|
839 self.__QScintillaLeftCommand(self.extendSelectionLeft, True) |
|
840 |
|
841 def __QScintillaWordLeftExtend(self): |
|
842 """ |
|
843 Private method to handle the Extend Selection Left one word command. |
|
844 """ |
|
845 self.__QScintillaLeftCommand(self.extendSelectionWordLeft, True) |
|
846 |
|
847 def __QScintillaVCHomeExtend(self): |
|
848 """ |
|
849 Private method to handle the Extend Selection to start of line command. |
|
850 """ |
|
851 col = self.__lastPos[1] |
|
852 self.extendSelectionToBOL() |
|
853 while col > 0: |
|
854 self.extendSelectionRight() |
|
855 col -= 1 |
|
856 |
|
857 def __executeCommand(self, cmd): |
|
858 """ |
|
859 Private slot to execute a command. |
|
860 |
|
861 @param cmd command to be executed by debug client (string) |
|
862 """ |
|
863 if not cmd: |
|
864 cmd = '' |
|
865 if len(self.history) == 0 or self.history[-1] != cmd: |
|
866 if len(self.history) == self.maxHistoryEntries: |
|
867 del self.history[0] |
|
868 self.history.append(cmd) |
|
869 self.histidx = -1 |
|
870 |
|
871 if cmd.lower() in ["clear", "cls"]: |
|
872 self.clear() |
|
873 return |
|
874 else: |
|
875 if not cmd.endswith("\n"): |
|
876 cmd = "{0}\n".format(cmd) |
|
877 self.__send(cmd) |
|
878 |
|
879 def __useHistory(self): |
|
880 """ |
|
881 Private method to display a command from the history. |
|
882 """ |
|
883 if self.histidx < len(self.history): |
|
884 cmd = self.history[self.histidx] |
|
885 else: |
|
886 cmd = "" |
|
887 self.incrementalSearchString = "" |
|
888 self.incrementalSearchActive = False |
|
889 |
|
890 self.__insertHistory(cmd) |
|
891 |
|
892 def __insertHistory(self, cmd): |
|
893 """ |
|
894 Private method to insert a command selected from the history. |
|
895 |
|
896 @param cmd history entry to be inserted (string) |
|
897 """ |
|
898 self.setCursorPosition(self.prline, self.prcol) |
|
899 self.setSelection(self.prline, self.prcol,\ |
|
900 self.prline, self.lineLength(self.prline)) |
|
901 self.removeSelectedText() |
|
902 self.__insertText(cmd) |
|
903 |
|
904 def __searchHistory(self, txt, startIdx=-1): |
|
905 """ |
|
906 Private method used to search the history. |
|
907 |
|
908 @param txt text to match at the beginning (string) |
|
909 @param startIdx index to start search from (integer) |
|
910 @return index of found entry (integer) |
|
911 """ |
|
912 if startIdx == -1: |
|
913 idx = 0 |
|
914 else: |
|
915 idx = startIdx + 1 |
|
916 while idx < len(self.history) and \ |
|
917 not self.history[idx].startswith(txt): |
|
918 idx += 1 |
|
919 return idx |
|
920 |
|
921 def __rsearchHistory(self, txt, startIdx=-1): |
|
922 """ |
|
923 Private method used to reverse search the history. |
|
924 |
|
925 @param txt text to match at the beginning (string) |
|
926 @param startIdx index to start search from (integer) |
|
927 @return index of found entry (integer) |
|
928 """ |
|
929 if startIdx == -1: |
|
930 idx = len(self.history) - 1 |
|
931 else: |
|
932 idx = startIdx - 1 |
|
933 while idx >= 0 and \ |
|
934 not self.history[idx].startswith(txt): |
|
935 idx -= 1 |
|
936 return idx |
|
937 |
|
938 def contextMenuEvent(self, ev): |
|
939 """ |
|
940 Reimplemented to show our own context menu. |
|
941 |
|
942 @param ev context menu event (QContextMenuEvent) |
|
943 """ |
|
944 self.menu.popup(ev.globalPos()) |
|
945 ev.accept() |
|
946 |
|
947 def clear(self): |
|
948 """ |
|
949 Public slot to clear the display. |
|
950 """ |
|
951 super().clear() |
|
952 self.__send("\n") |
|
953 |
|
954 def __reset(self): |
|
955 """ |
|
956 Private slot to handle the 'reset' context menu entry. |
|
957 """ |
|
958 self.__stopShell() |
|
959 self.__startShell() |
|
960 |
|
961 def __startShell(self): |
|
962 """ |
|
963 Private slot to start the shell process. |
|
964 """ |
|
965 args = [] |
|
966 if Utilities.isWindowsPlatform(): |
|
967 args.append("/Q") |
|
968 self.__process.start("cmd.exe", args) |
|
969 else: |
|
970 shell = Preferences.getTerminal("Shell") |
|
971 if not shell: |
|
972 shell = os.environ.get('SHELL') |
|
973 if shell is None: |
|
974 self.__insertText(self.trUtf8("No shell has been configured.")) |
|
975 return |
|
976 if Preferences.getTerminal("ShellInteractive"): |
|
977 args.append("-i") |
|
978 self.__process.start(shell, args) |
|
979 |
|
980 def __stopShell(self): |
|
981 """ |
|
982 Private slot to stop the shell process. |
|
983 """ |
|
984 self.__process.kill() |
|
985 self.__process.waitForFinished(3000) |
|
986 |
|
987 def handlePreferencesChanged(self): |
|
988 """ |
|
989 Public slot to handle the preferencesChanged signal. |
|
990 """ |
|
991 # rebind the lexer |
|
992 self.__bindLexer() |
|
993 self.recolor() |
|
994 |
|
995 # set margin 0 configuration |
|
996 self.__setTextDisplay() |
|
997 self.__setMargin0() |
|
998 |
|
999 # do the history related stuff |
|
1000 self.maxHistoryEntries = Preferences.getTerminal("MaxHistoryEntries") |
|
1001 self.history = self.history[-self.maxHistoryEntries:] |
|
1002 |
|
1003 # do the I/O encoding |
|
1004 self.__ioEncoding = Preferences.getSystem("IOEncoding") |
|
1005 |
|
1006 def focusInEvent(self, event): |
|
1007 """ |
|
1008 Public method called when the shell receives focus. |
|
1009 |
|
1010 @param event the event object (QFocusEvent) |
|
1011 """ |
|
1012 if not self.__actionsAdded: |
|
1013 self.addActions(self.vm.editorActGrp.actions()) |
|
1014 self.addActions(self.vm.copyActGrp.actions()) |
|
1015 self.addActions(self.vm.viewActGrp.actions()) |
|
1016 self.__searchShortcut = QShortcut(self.vm.searchAct.shortcut(), self, |
|
1017 self.__find, self.__find) |
|
1018 self.__searchNextShortcut = QShortcut(self.vm.searchNextAct.shortcut(), self, |
|
1019 self.__searchNext, self.__searchNext) |
|
1020 self.__searchPrevShortcut = QShortcut(self.vm.searchPrevAct.shortcut(), self, |
|
1021 self.__searchPrev, self.__searchPrev) |
|
1022 |
|
1023 try: |
|
1024 self.vm.editActGrp.setEnabled(False) |
|
1025 self.vm.editorActGrp.setEnabled(True) |
|
1026 self.vm.copyActGrp.setEnabled(True) |
|
1027 self.vm.viewActGrp.setEnabled(True) |
|
1028 self.vm.searchActGrp.setEnabled(False) |
|
1029 except AttributeError: |
|
1030 pass |
|
1031 self.__searchShortcut.setEnabled(True) |
|
1032 self.__searchNextShortcut.setEnabled(True) |
|
1033 self.__searchPrevShortcut.setEnabled(True) |
|
1034 self.setCaretWidth(self.caretWidth) |
|
1035 self.setCursorFlashTime(QApplication.cursorFlashTime()) |
|
1036 |
|
1037 super().focusInEvent(event) |
|
1038 |
|
1039 def focusOutEvent(self, event): |
|
1040 """ |
|
1041 Public method called when the shell loses focus. |
|
1042 |
|
1043 @param event the event object (QFocusEvent) |
|
1044 """ |
|
1045 try: |
|
1046 self.vm.editorActGrp.setEnabled(False) |
|
1047 except AttributeError: |
|
1048 pass |
|
1049 self.__searchShortcut.setEnabled(False) |
|
1050 self.__searchNextShortcut.setEnabled(False) |
|
1051 self.__searchPrevShortcut.setEnabled(False) |
|
1052 self.setCaretWidth(0) |
|
1053 super().focusOutEvent(event) |
|
1054 |
|
1055 def insert(self, txt): |
|
1056 """ |
|
1057 Public slot to insert text at the current cursor position. |
|
1058 |
|
1059 The cursor is advanced to the end of the inserted text. |
|
1060 |
|
1061 @param txt text to be inserted (string) |
|
1062 """ |
|
1063 l = len(txt) |
|
1064 line, col = self.getCursorPosition() |
|
1065 self.insertAt(txt, line, col) |
|
1066 if re.search(self.linesepRegExp, txt) is not None: |
|
1067 line += 1 |
|
1068 self.setCursorPosition(line, col + l) |
|
1069 |
|
1070 def __configure(self): |
|
1071 """ |
|
1072 Private method to open the configuration dialog. |
|
1073 """ |
|
1074 e5App().getObject("UserInterface").showPreferences("terminalPage") |
|
1075 |
|
1076 def __find(self): |
|
1077 """ |
|
1078 Private slot to show the find widget. |
|
1079 """ |
|
1080 txt = self.selectedText() |
|
1081 self.__mainWindow.showFind(txt) |
|
1082 |
|
1083 def __searchNext(self): |
|
1084 """ |
|
1085 Private method to search for the next occurrence. |
|
1086 """ |
|
1087 if self.__lastSearch: |
|
1088 self.searchNext(*self.__lastSearch) |
|
1089 |
|
1090 def searchNext(self, txt, caseSensitive, wholeWord): |
|
1091 """ |
|
1092 Public method to search the next occurrence of the given text. |
|
1093 |
|
1094 @param txt text to search for (string) |
|
1095 @param caseSensitive flag indicating to perform a case sensitive |
|
1096 search (boolean) |
|
1097 @param wholeWord flag indicating to search for whole words |
|
1098 only (boolean) |
|
1099 """ |
|
1100 self.__lastSearch = (txt, caseSensitive, wholeWord) |
|
1101 ok = self.findFirst(txt, False, caseSensitive, wholeWord, False, forward=True) |
|
1102 self.searchStringFound.emit(ok) |
|
1103 |
|
1104 def __searchPrev(self): |
|
1105 """ |
|
1106 Private method to search for the next occurrence. |
|
1107 """ |
|
1108 if self.__lastSearch: |
|
1109 self.searchPrev(*self.__lastSearch) |
|
1110 |
|
1111 def searchPrev(self, txt, caseSensitive, wholeWord): |
|
1112 """ |
|
1113 Public method to search the previous occurrence of the given text. |
|
1114 |
|
1115 @param txt text to search for (string) |
|
1116 @param caseSensitive flag indicating to perform a case sensitive |
|
1117 search (boolean) |
|
1118 @param wholeWord flag indicating to search for whole words |
|
1119 only (boolean) |
|
1120 """ |
|
1121 self.__lastSearch = (txt, caseSensitive, wholeWord) |
|
1122 if self.hasSelectedText(): |
|
1123 line, index = self.getSelection()[:2] |
|
1124 else: |
|
1125 line, index = -1, -1 |
|
1126 ok = self.findFirst(txt, False, caseSensitive, wholeWord, False, forward=False, |
|
1127 line=line, index=index) |
|
1128 self.searchStringFound.emit(ok) |
|