More unit tests
[codimension.git] / thirdparty / pyqtermwidget / pyqterm / frontend.py
blobe24c3f1ebb5445089dc12ef2c231041d0818a3d3
1 # -*- coding: utf-8 -*-
2 import sys
3 import time
5 from PyQt4.QtCore import QRect, Qt, pyqtSignal
6 from PyQt4.QtGui import (
7 QApplication, QClipboard, QWidget, QPainter, QFont, QBrush, QColor,
8 QPen, QPixmap, QImage, QContextMenuEvent)
10 from .backend import Session
13 DEBUG = False
16 class TerminalWidget(QWidget):
18 foreground_color_map = {
19 0: "#000",
20 1: "#b00",
21 2: "#0b0",
22 3: "#bb0",
23 4: "#00b",
24 5: "#b0b",
25 6: "#0bb",
26 7: "#bbb",
27 8: "#666",
28 9: "#f00",
29 10: "#0f0",
30 11: "#ff0",
31 12: "#00f", # concelaed
32 13: "#f0f",
33 14: "#000", # negative
34 15: "#fff", # default
36 background_color_map = {
37 0: "#000",
38 1: "#b00",
39 2: "#0b0",
40 3: "#bb0",
41 4: "#00b",
42 5: "#b0b",
43 6: "#0bb",
44 7: "#bbb",
45 12: "#aaa", # cursor
46 14: "#000", # default
47 15: "#fff", # negative
49 keymap = {
50 Qt.Key_Backspace: chr(127),
51 Qt.Key_Escape: chr(27),
52 Qt.Key_AsciiTilde: "~~",
53 Qt.Key_Up: "~A",
54 Qt.Key_Down: "~B",
55 Qt.Key_Left: "~D",
56 Qt.Key_Right: "~C",
57 Qt.Key_PageUp: "~1",
58 Qt.Key_PageDown: "~2",
59 Qt.Key_Home: "~H",
60 Qt.Key_End: "~F",
61 Qt.Key_Insert: "~3",
62 Qt.Key_Delete: "~4",
63 Qt.Key_F1: "~a",
64 Qt.Key_F2: "~b",
65 Qt.Key_F3: "~c",
66 Qt.Key_F4: "~d",
67 Qt.Key_F5: "~e",
68 Qt.Key_F6: "~f",
69 Qt.Key_F7: "~g",
70 Qt.Key_F8: "~h",
71 Qt.Key_F9: "~i",
72 Qt.Key_F10: "~j",
73 Qt.Key_F11: "~k",
74 Qt.Key_F12: "~l",
77 session_closed = pyqtSignal()
79 def __init__(self, parent=None, command="/bin/bash",
80 font_name="Monospace", font_size=18):
81 super(TerminalWidget, self).__init__(parent)
82 self.parent().setTabOrder(self, self)
83 self.setFocusPolicy(Qt.WheelFocus)
84 self.setAutoFillBackground(False)
85 self.setAttribute(Qt.WA_OpaquePaintEvent, True)
86 self.setCursor(Qt.IBeamCursor)
87 font = QFont(font_name)
88 font.setPixelSize(font_size)
89 self.setFont(font)
90 self._session = None
91 self._last_update = None
92 self._screen = []
93 self._text = []
94 self._cursor_rect = None
95 self._cursor_col = 0
96 self._cursor_row = 0
97 self._dirty = False
98 self._blink = False
99 self._press_pos = None
100 self._selection = None
101 self._clipboard = QApplication.clipboard()
102 QApplication.instance().lastWindowClosed.connect(Session.close_all)
103 if command:
104 self.execute()
106 def execute(self, command="/bin/bash"):
107 self._session = Session()
108 self._session.start(command)
109 self._timer_id = None
110 # start timer either with high or low priority
111 if self.hasFocus():
112 self.focusInEvent(None)
113 else:
114 self.focusOutEvent(None)
116 def send(self, s):
117 self._session.write(s)
119 def stop(self):
120 self._session.stop()
122 def pid(self):
123 return self._session.pid()
125 def setFont(self, font):
126 super(TerminalWidget, self).setFont(font)
127 self._update_metrics()
129 def focusNextPrevChild(self, next):
130 if not self._session.is_alive():
131 return True
132 return False
134 def focusInEvent(self, event):
135 if not self._session.is_alive():
136 return
137 if self._timer_id is not None:
138 self.killTimer(self._timer_id)
139 self._timer_id = self.startTimer(250)
140 self.update_screen()
142 def focusOutEvent(self, event):
143 if not self._session.is_alive():
144 return
145 # reduced update interval
146 # -> slower screen updates
147 # -> but less load on main app which results in better responsiveness
148 if self._timer_id is not None:
149 self.killTimer(self._timer_id)
150 self._timer_id = self.startTimer(750)
152 def resizeEvent(self, event):
153 if not self._session.is_alive():
154 return
155 self._columns, self._rows = self._pixel2pos(
156 self.width(), self.height())
157 self._session.resize(self._columns, self._rows)
159 def closeEvent(self, event):
160 if not self._session.is_alive():
161 return
162 self._session.close()
164 def timerEvent(self, event):
165 if not self._session.is_alive():
166 if self._timer_id is not None:
167 self.killTimer(self._timer_id)
168 self._timer_id = None
169 if DEBUG:
170 print "Session closed"
171 self.session_closed.emit()
172 return
173 last_change = self._session.last_change()
174 if not last_change:
175 return
176 if not self._last_update or last_change > self._last_update:
177 self._last_update = last_change
178 old_screen = self._screen
179 (self._cursor_col, self._cursor_row), self._screen = self._session.dump()
180 self._update_cursor_rect()
181 if old_screen != self._screen:
182 self._dirty = True
183 if self.hasFocus():
184 self._blink = not self._blink
185 self.update()
187 def _update_metrics(self):
188 fm = self.fontMetrics()
189 self._char_height = fm.height()
190 self._char_width = fm.width("W")
192 def _update_cursor_rect(self):
193 cx, cy = self._pos2pixel(self._cursor_col, self._cursor_row)
194 self._cursor_rect = QRect(cx, cy, self._char_width, self._char_height)
196 def _reset(self):
197 self._update_metrics()
198 self._update_cursor_rect()
199 self.resizeEvent(None)
200 self.update_screen()
202 def update_screen(self):
203 self._dirty = True
204 self.update()
206 def paintEvent(self, event):
207 painter = QPainter(self)
208 if self._dirty:
209 self._dirty = False
210 self._paint_screen(painter)
211 else:
212 if self._cursor_rect and not self._selection:
213 self._paint_cursor(painter)
214 if self._selection:
215 self._paint_selection(painter)
216 self._dirty = True
218 def _pixel2pos(self, x, y):
219 col = int(round(x / self._char_width))
220 row = int(round(y / self._char_height))
221 return col, row
223 def _pos2pixel(self, col, row):
224 x = col * self._char_width
225 y = row * self._char_height
226 return x, y
228 def _paint_cursor(self, painter):
229 if self._blink:
230 color = "#aaa"
231 else:
232 color = "#fff"
233 painter.setPen(QPen(QColor(color)))
234 painter.drawRect(self._cursor_rect)
236 def _paint_screen(self, painter):
237 # Speed hacks: local name lookups are faster
238 vars().update(QColor=QColor, QBrush=QBrush, QPen=QPen, QRect=QRect)
239 background_color_map = self.background_color_map
240 foreground_color_map = self.foreground_color_map
241 char_width = self._char_width
242 char_height = self._char_height
243 painter_drawText = painter.drawText
244 painter_fillRect = painter.fillRect
245 painter_setPen = painter.setPen
246 align = Qt.AlignTop | Qt.AlignLeft
247 # set defaults
248 background_color = background_color_map[14]
249 foreground_color = foreground_color_map[15]
250 brush = QBrush(QColor(background_color))
251 painter_fillRect(self.rect(), brush)
252 pen = QPen(QColor(foreground_color))
253 painter_setPen(pen)
254 y = 0
255 text = []
256 text_append = text.append
257 for row, line in enumerate(self._screen):
258 col = 0
259 text_line = ""
260 for item in line:
261 if isinstance(item, basestring):
262 x = col * char_width
263 length = len(item)
264 rect = QRect(
265 x, y, x + char_width * length, y + char_height)
266 painter_fillRect(rect, brush)
267 painter_drawText(rect, align, item)
268 col += length
269 text_line += item
270 else:
271 foreground_color_idx, background_color_idx, underline_flag = item
272 foreground_color = foreground_color_map[
273 foreground_color_idx]
274 background_color = background_color_map[
275 background_color_idx]
276 pen = QPen(QColor(foreground_color))
277 brush = QBrush(QColor(background_color))
278 painter_setPen(pen)
279 # painter.setBrush(brush)
280 y += char_height
281 text_append(text_line)
282 self._text = text
284 def _paint_selection(self, painter):
285 pcol = QColor(200, 200, 200, 50)
286 pen = QPen(pcol)
287 bcol = QColor(230, 230, 230, 50)
288 brush = QBrush(bcol)
289 painter.setPen(pen)
290 painter.setBrush(brush)
291 for (start_col, start_row, end_col, end_row) in self._selection:
292 x, y = self._pos2pixel(start_col, start_row)
293 width, height = self._pos2pixel(
294 end_col - start_col, end_row - start_row)
295 rect = QRect(x, y, width, height)
296 # painter.drawRect(rect)
297 painter.fillRect(rect, brush)
299 def zoom_in(self):
300 font = self.font()
301 font.setPixelSize(font.pixelSize() + 2)
302 self.setFont(font)
303 self._reset()
305 def zoom_out(self):
306 font = self.font()
307 font.setPixelSize(font.pixelSize() - 2)
308 self.setFont(font)
309 self._reset()
311 return_pressed = pyqtSignal()
313 def keyPressEvent(self, event):
314 text = unicode(event.text())
315 key = event.key()
316 modifiers = event.modifiers()
317 ctrl = modifiers == Qt.ControlModifier
318 if ctrl and key == Qt.Key_Plus:
319 self.zoom_in()
320 elif ctrl and key == Qt.Key_Minus:
321 self.zoom_out()
322 else:
323 if text and key != Qt.Key_Backspace:
324 self.send(text.encode("utf-8"))
325 else:
326 s = self.keymap.get(key)
327 if s:
328 self.send(s.encode("utf-8"))
329 elif DEBUG:
330 print "Unkonwn key combination"
331 print "Modifiers:", modifiers
332 print "Key:", key
333 for name in dir(Qt):
334 if not name.startswith("Key_"):
335 continue
336 value = getattr(Qt, name)
337 if value == key:
338 print "Symbol: Qt.%s" % name
339 print "Text: %r" % text
340 event.accept()
341 if key in (Qt.Key_Enter, Qt.Key_Return):
342 self.return_pressed.emit()
344 def mousePressEvent(self, event):
345 button = event.button()
346 if button == Qt.RightButton:
347 ctx_event = QContextMenuEvent(QContextMenuEvent.Mouse, event.pos())
348 self.contextMenuEvent(ctx_event)
349 self._press_pos = None
350 elif button == Qt.LeftButton:
351 self._press_pos = event.pos()
352 self._selection = None
353 self.update_screen()
354 elif button == Qt.MiddleButton:
355 self._press_pos = None
356 self._selection = None
357 text = unicode(self._clipboard.text(QClipboard.Selection))
358 self.send(text.encode("utf-8"))
359 # self.update_screen()
361 def mouseReleaseEvent(self, QMouseEvent):
362 pass # self.update_screen()
364 def _selection_rects(self, start_pos, end_pos):
365 sx, sy = start_pos.x(), start_pos.y()
366 start_col, start_row = self._pixel2pos(sx, sy)
367 ex, ey = end_pos.x(), end_pos.y()
368 end_col, end_row = self._pixel2pos(ex, ey)
369 if start_row == end_row:
370 if ey > sy or end_row == 0:
371 end_row += 1
372 else:
373 end_row -= 1
374 if start_col == end_col:
375 if ex > sx or end_col == 0:
376 end_col += 1
377 else:
378 end_col -= 1
379 if start_row > end_row:
380 start_row, end_row = end_row, start_row
381 if start_col > end_col:
382 start_col, end_col = end_col, start_col
383 if end_row - start_row == 1:
384 return [(start_col, start_row, end_col, end_row)]
385 else:
386 return [
387 (start_col, start_row, self._columns, start_row + 1),
388 (0, start_row + 1, self._columns, end_row - 1),
389 (0, end_row - 1, end_col, end_row)
392 def text(self, rect=None):
393 if rect is None:
394 return "\n".join(self._text)
395 else:
396 text = []
397 (start_col, start_row, end_col, end_row) = rect
398 for row in range(start_row, end_row):
399 text.append(self._text[row][start_col:end_col])
400 return text
402 def text_selection(self):
403 text = []
404 for (start_col, start_row, end_col, end_row) in self._selection:
405 for row in range(start_row, end_row):
406 text.append(self._text[row][start_col:end_col])
407 return "\n".join(text)
409 def column_count(self):
410 return self._columns
412 def row_count(self):
413 return self._rows
415 def mouseMoveEvent(self, event):
416 if self._press_pos:
417 move_pos = event.pos()
418 self._selection = self._selection_rects(self._press_pos, move_pos)
420 sel = self.text_selection()
421 if DEBUG:
422 print "%r copied to xselection" % sel
423 self._clipboard.setText(sel, QClipboard.Selection)
425 self.update_screen()
427 def mouseDoubleClickEvent(self, event):
428 self._press_pos = None
429 # double clicks create a selection for the word under the cursor
430 pos = event.pos()
431 x, y = pos.x(), pos.y()
432 col, row = self._pixel2pos(x, y)
433 line = self._text[row]
434 # find start of word
435 start_col = col
436 found_left = 0
437 while start_col > 0:
438 char = line[start_col]
439 if not char.isalnum() and char not in ("_",):
440 found_left = 1
441 break
442 start_col -= 1
443 # find end of word
444 end_col = col
445 found_right = 0
446 while end_col < self._columns:
447 char = line[end_col]
448 if not char.isalnum() and char not in ("_",):
449 found_right = 1
450 break
451 end_col += 1
452 self._selection = [
453 (start_col + found_left, row, end_col - found_right + 1, row + 1)]
455 sel = self.text_selection()
456 if DEBUG:
457 print "%r copied to xselection" % sel
458 self._clipboard.setText(sel, QClipboard.Selection)
460 self.update_screen()
462 def is_alive(self):
463 return (self._session and self._session.is_alive()) or False