-
Notifications
You must be signed in to change notification settings - Fork 31
/
Copy pathtermwidget.py
253 lines (208 loc) · 8.5 KB
/
termwidget.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
"""
Terminal emulator widget.
Shows intput and output text. Allows to enter commands. Supports history.
"""
import cgi
from PyQt4.QtCore import pyqtSignal, Qt
from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \
QSizePolicy, QTextCursor, QTextEdit, \
QVBoxLayout, QWidget
class _ExpandableTextEdit(QTextEdit):
"""
Class implements edit line, which expands themselves automatically
"""
historyNext = pyqtSignal()
historyPrev = pyqtSignal()
def __init__(self, termwidget, *args):
QTextEdit.__init__(self, *args)
self.setStyleSheet("font: 9pt \"Courier\";")
self._fittedHeight = 1
self.textChanged.connect(self._fit_to_document)
self._fit_to_document()
self._termWidget = termwidget
def sizeHint(self):
"""
QWidget sizeHint impelemtation
"""
hint = QTextEdit.sizeHint(self)
hint.setHeight(self._fittedHeight)
return hint
def _fit_to_document(self):
"""
Update widget height to fit all text
"""
documentsize = self.document().size().toSize()
self._fittedHeight = documentsize.height() + (self.height() - self.viewport().height())
self.setMaximumHeight(self._fittedHeight)
self.updateGeometry()
def keyPressEvent(self, event):
"""
Catch keyboard events. Process Enter, Up, Down
"""
if event.matches(QKeySequence.InsertParagraphSeparator):
text = self.toPlainText()
if self._termWidget.is_command_complete(text):
self._termWidget.exec_current_command()
return
elif event.matches(QKeySequence.MoveToNextLine):
text = self.toPlainText()
cursor_pos = self.textCursor().position()
textBeforeEnd = text[cursor_pos:]
# if len(textBeforeEnd.splitlines()) <= 1:
if len(textBeforeEnd.split('\n')) <= 1:
self.historyNext.emit()
return
elif event.matches(QKeySequence.MoveToPreviousLine):
text = self.toPlainText()
cursor_pos = self.textCursor().position()
text_before_start = text[:cursor_pos]
# lineCount = len(textBeforeStart.splitlines())
line_count = len(text_before_start.split('\n'))
if len(text_before_start) > 0 and \
(text_before_start[-1] == '\n' or text_before_start[-1] == '\r'):
line_count += 1
if line_count <= 1:
self.historyPrev.emit()
return
elif event.matches(QKeySequence.MoveToNextPage) or \
event.matches(QKeySequence.MoveToPreviousPage):
return self._termWidget.browser().keyPressEvent(event)
QTextEdit.keyPressEvent(self, event)
def insertFromMimeData(self, mime_data):
# Paste only plain text.
self.insertPlainText(mime_data.text())
class TermWidget(QWidget):
"""
Widget wich represents terminal. It only displays text and allows to enter text.
All highlevel logic should be implemented by client classes
User pressed Enter. Client class should decide, if command must be executed or user may continue edit it
"""
def __init__(self, *args):
QWidget.__init__(self, *args)
self._browser = QTextEdit(self)
self._browser.setStyleSheet("font: 9pt \"Courier\";")
self._browser.setReadOnly(True)
self._browser.document().setDefaultStyleSheet(
self._browser.document().defaultStyleSheet() +
"span {white-space:pre;}")
self._edit = _ExpandableTextEdit(self, self)
self._edit.historyNext.connect(self._on_history_next)
self._edit.historyPrev.connect(self._on_history_prev)
self.setFocusProxy(self._edit)
layout = QVBoxLayout(self)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._browser)
layout.addWidget(self._edit)
self._history = [''] # current empty line
self._historyIndex = 0
self._edit.setFocus()
def open_proccessing(self, detail=None):
"""
Open processing and disable using shell commands again until all commands are finished
:param detail: text detail about what is currently called from TCL to python
:return: None
"""
self._edit.setTextColor(Qt.white)
self._edit.setTextBackgroundColor(Qt.darkGreen)
if detail is None:
self._edit.setPlainText("...proccessing...")
else:
self._edit.setPlainText("...proccessing... [%s]" % detail)
self._edit.setDisabled(True)
def close_proccessing(self):
"""
Close processing and enable using shell commands again
:return:
"""
self._edit.setTextColor(Qt.black)
self._edit.setTextBackgroundColor(Qt.white)
self._edit.setPlainText('')
self._edit.setDisabled(False)
def _append_to_browser(self, style, text):
"""
Convert text to HTML for inserting it to browser
"""
assert style in ('in', 'out', 'err')
text = cgi.escape(text)
text = text.replace('\n', '<br/>')
if style == 'in':
text = '<span style="font-weight: bold;">%s</span>' % text
elif style == 'err':
text = '<span style="font-weight: bold; color: red;">%s</span>' % text
else:
text = '<span>%s</span>' % text # without span <br/> is ignored!!!
scrollbar = self._browser.verticalScrollBar()
old_value = scrollbar.value()
scrollattheend = old_value == scrollbar.maximum()
self._browser.moveCursor(QTextCursor.End)
self._browser.insertHtml(text)
"""TODO When user enters second line to the input, and input is resized, scrollbar changes its positon
and stops moving. As quick fix of this problem, now we always scroll down when add new text.
To fix it correctly, srcoll to the bottom, if before intput has been resized,
scrollbar was in the bottom, and remove next lien
"""
scrollattheend = True
if scrollattheend:
scrollbar.setValue(scrollbar.maximum())
else:
scrollbar.setValue(old_value)
def exec_current_command(self):
"""
Save current command in the history. Append it to the log. Clear edit line
Reimplement in the child classes to actually execute command
"""
text = str(self._edit.toPlainText())
self._append_to_browser('in', '> ' + text + '\n')
if len(self._history) < 2 or\
self._history[-2] != text: # don't insert duplicating items
if text[-1] == '\n':
self._history.insert(-1, text[:-1])
else:
self._history.insert(-1, text)
self._historyIndex = len(self._history) - 1
self._history[-1] = ''
self._edit.clear()
if not text[-1] == '\n':
text += '\n'
self.child_exec_command(text)
def child_exec_command(self, text):
"""
Reimplement in the child classes
"""
pass
def add_line_break_to_input(self):
self._edit.textCursor().insertText('\n')
def append_output(self, text):
"""Appent text to output widget
"""
self._append_to_browser('out', text)
def append_error(self, text):
"""Appent error text to output widget. Text is drawn with red background
"""
self._append_to_browser('err', text)
def is_command_complete(self, text):
"""
Executed by _ExpandableTextEdit. Reimplement this function in the child classes.
"""
return True
def browser(self):
return self._browser
def _on_history_next(self):
"""
Down pressed, show next item from the history
"""
if (self._historyIndex + 1) < len(self._history):
self._historyIndex += 1
self._edit.setPlainText(self._history[self._historyIndex])
self._edit.moveCursor(QTextCursor.End)
def _on_history_prev(self):
"""
Up pressed, show previous item from the history
"""
if self._historyIndex > 0:
if self._historyIndex == (len(self._history) - 1):
self._history[-1] = self._edit.toPlainText()
self._historyIndex -= 1
self._edit.setPlainText(self._history[self._historyIndex])
self._edit.moveCursor(QTextCursor.End)