Coverage for pyilper/pilqterm.py: 88%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2# pyILPER 1.2.1 for Linux
3#
4# An emulator for virtual HP-IL devices for the PIL-Box
5# derived from ILPER 1.4.5 for Windows
6# Copyright (c) 2008-2013 Jean-Francois Garnier
7# C++ version (c) 2013 Christoph Gießelink
8# Python Version (c) 2015 Joachim Siebold
9#
10# This program is free software; you can redistribute it and/or
11# modify it under the terms of the GNU General Public License
12# as published by the Free Software Foundation; either version 2
13# of the License, or (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program; if not, write to the Free Software
22# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
23#
24# Object classes of terminal widget ----------------------------------------
25#
26# This code was derived from the pyqterm widget of Henning Schröder
27#
28# Changelog
29# 06.10.2015 jsi:
30# - class statement syntax update
31# - adjust super statements to python3+ syntax
32# 21.11.2015 jsi:
33# - disabled F7
34# - enhanced error messages about unhandled escape sequences
35# 20.02.2016 jsi:
36# - _kbdfunc escape sequence handling improved
37# 26.02.2016 jsi:
38# - keyboard input delay introduced to improve stability
39# - do not update terminal window if not visible
40# 28.02.2016 jsi:
41# - removed dead code
42# - scroll line left and right does now work for wrapped lines
43# - implemented block and arrow cursor, removed indicator function
44# 01.03.2016 jsi:
45# - removed more unneeded code, introduced first color scheme support
46# 05.03.2016 jsi:
47# - removed more unneeded code
48# - disable autorepeat
49# - fixed BS and ESC % handling
50# 06.03.2016 jsi:
51# - refactoring
52# - use non blocking get in terminal output queue
53# 07.03.2016 jsi:
54# - fixed alt key, MAC OSX fix
55# - introduced OS X like ALT shortcuts for { [ ] } @
56# 09.03.2016 jsi:
57# - introduced shortcut ALT-I to toggle insert mode
58# 10.03.2016 jsi:
59# - introduced transparent cursor
60# 12.03.2016 jsi:
61# - removed keyboard delay
62# 20.03.2016 jsi:
63# - improved insert cursor, use transform for cursor positioning
64# 21.03.2016 jsi:
65# - blinking cursor
66# - refactoring
67# 22.03.2016 jsi:
68# - set blink on after updating cursor_rect
69# - use single shot timer for processing the terminal output queue
70# 29.03.2016 jsi:
71# - enable ALT keycodes 200-255
72# 31.03.2016 jsi:
73# - revert last change, keys > 128 are mapped to characters < 128
74# 17.04.2016 jsi:
75# - reduce keyboard rate for autorepeated function keys to prevent hang up:
76# key in: vers$[Return] and then press left arrow for three seconds
77# 05.05.2016 jsi:
78# - introduced new Object class for scrollable terminal widget
79# 07.05.2016 jsi:
80# - introduce parameter for scroll up buffersize
81# 11.07.2016 jsi:
82# - refactoring: move some constants to pilcore.py
83# 05.10.2016 jsi:
84# - implemented redraw method to refres terminal when parent widget was resized
85# 14.10.2016 jsi:
86# - consistent use of namespaces, removed vars.update call in _paint screen
87# 31.01.2016 jsi:
88# - do not issue paint events where nothing gets painted (this cleared the display
89# on qt5 (MAC OS)
90# - catch index error in HPTerminal.dump()
91# 03.02.2017 jsi:
92# - do not fire cursor paint event, if cursor is off
93# 30.08.2017 jsi:
94# - make number of rows depend of window size
95# 09.09.2017 jsi:
96# - fixed incorrect cursor positioning for certain font sizes
97# - fixed incorrect initialization of terminal if not visible at program start
98# 12.09.2017 jsi
99# - partial rewrite of the backend code, various bug fixes and documentation
100# improvements
101# 14.09.2017 jsi
102# - rewrite of the frontend code, use QGraphicsView and QGraphicsScene
103# - refactoring
104# 16.10.2017 jsi
105# - charconv always returns one character
106# - fixed error in ctrl_CR
107# - check for line length overflows
108# 20.09.2017 jsi:
109# - make number of comumns (80/120), font size and color scheme configurable at runtime
110# 22.09.2017 jsi:
111# - get_cols method introduced (needed by scope)
112# 23.09.2017 jsi:
113# - do not update scene, if tab is invisible. Redraw view if tab becomes visible
114# 24.09.2017 jsi:
115# - added mouse wheel scrolling support
116# 27.09.2017 jsi:
117# - renamed method queueOutput to putDataToHPIL
118# 30.09.2017 jsi:
119# - added copy and paste support
120# 05.10.2017 jsi:
121# - initial terminal line buffer with TERMINAL_MINIMUM_ROWS on start up
122# because hidden terminal tabs get their resize event not until they
123# become visible.
124# - fixes to scrollbar parameter settings
125# 06.10.2017 jsi:
126# - fixed bugs of insert mode
127# 28.12.2017 jsi:
128# - fix crash in getSelection if self.move_row/self.move_col is None
129# - introduced autoscroll when moving selection
130# 16.01.2018 jsi:
131# - colorscheme and display width are now local parameters
132# 20.01.2018 jsi
133# - scrollupbuffersize is now a local parameter and does not require a restart
134# 29.01.2018 jsi
135# - added stretches to center terminal widget
136# - shrink parent widgets to minimum size if font size or number of columns
137# was changed
138# 04.02.2018 jsi
139# - added margin when determining row of selection area from mouse position
140# - fixed bug in swapping coordinates in selectionMove
141# 19.02.2018 jsi
142# - introduced "paper" color scheme
143# 22.02.2018 jsi
144# - disabled shrinking parent widgets because of errors in reconfiguration
145# 11.08.2018 jsi
146# - custom keyboard shortcuts added
147# 15.08.2018 jsi
148# - SHORTCUT_INSERT shortcut type added
149# 14.01.2019 jsi
150# - fix: do not switch to insert mode on ESC Q
151# - fix: do not increase line length if we overwrite existing text
152# - added HP-75 keyboard support
153# 16.01.2019 jsi
154# - out_terminal now requires int instead of char to avoid multiple conversions
155# - character attribute handling rewritten, added underline attribute
156# 20.01.2019 jsi
157# - keyboard handler rewritten, keydefs are now in pilkeysym.py
158# - local keys PageUp and PageDown scroll window up and down one page
159# 21.01.2019 jsi
160# - various bug fixes (keyboard handling and page up/page down scrolling)
161# 23.01.2019 jsi
162# - bug fix, restore cursor type
163# 25.01.2019 jsi
164# - removed special Mac key lookup
165# - update self.actual_h on "delete to end of display"
166# 28.01.2019 jsi
167# - autorepeat delay improved
168# 10.02.2019 jsi
169# - disable tab widget switching in terminal widget and enable TAB key for
170# keyboard input
171# 11.02.2019 jsi
172# - prevent possible crash in shortcut lookup
173# - use inverse video instead of underline for HP-75 character set
174# 06.12.2021 jsi
175# - fixed _kbdfunc call in paste
176# 18.04.2022 jsi
177# - cast coorinates to int to avoid crash using Python 3.10
178#
179# to do:
180# fix the reason for a possible index error in HPTerminal.dump()
182import array
183import queue
184import threading
185import time
187from PySide6 import QtCore, QtGui, QtWidgets
188from .pilcharconv import icharconv, CHARSET_HP71, CHARSET_HP75, CHARSET_HP41
189from .pilcore import UPDATE_TIMER, CURSOR_BLINK, TERMINAL_MINIMUM_ROWS,FONT, AUTOSCROLL_RATE, isMACOS, KEYBOARD_DELAY
190from .shortcutconfig import SHORTCUTCONFIG, SHORTCUT_EXEC, SHORTCUT_EDIT, SHORTCUT_INSERT
191from .pilconfig import PILCONFIG
192from .pilkeymap import *
194CURSOR_OFF=0
195CURSOR_INSERT=1
196CURSOR_OVERWRITE=2
198AUTOSCROLL_OFF=0
199AUTOSCROLL_UP=1
200AUTOSCROLL_DOWN=2
201#
202# character attributes
203#
204CHAR_ATTRIB_NONE= 0x00000000
205CHAR_ATTRIB_INVERSE= 0x020000000
206CHAR_ATTRIB_INVERSE_SHORT= CHAR_ATTRIB_INVERSE >> 16
207CHAR_ATTRIB_UNDERLINE= 0x01000000
208CHAR_ATTRIB_UNDERLINE_SHORT= CHAR_ATTRIB_UNDERLINE >> 16
209#
210# character attribute lookup table for ord(c) > 127
211#
212CHAR_ATTRIB = [
213 CHAR_ATTRIB_INVERSE, # HP-71 charset
214 CHAR_ATTRIB_INVERSE, # HP-41 charset
215# CHAR_ATTRIB_UNDERLINE, # HP-75 charset
216 CHAR_ATTRIB_INVERSE , # HP-75 charset
217 CHAR_ATTRIB_NONE # Roman-8 charset
218]
219#
220# scrolled terminal widget class ---------------------------------------------
221#
222class QScrolledTerminalWidget(QtWidgets.QWidget):
224 def __init__(self,parent,name):
225 super().__init__(parent)
226 self.pildevice= None
227 self.name= name
228 self.tabwidget=parent
229#
230# create terminal window and scrollbar
231#
232 self.hbox= QtWidgets.QHBoxLayout()
233 self.hbox.addStretch(1)
234 self.terminalwidget= QTerminalWidget(self,self.name,self.tabwidget)
235 self.hbox.addWidget(self.terminalwidget)
236 self.scrollbar= QtWidgets.QScrollBar()
237 self.hbox.addWidget(self.scrollbar)
238 self.hbox.addStretch(1)
239 self.setLayout(self.hbox)
240 self.HPTerminal= HPTerminal(self,self.name)
241 self.terminalwidget.setHPTerminal(self.HPTerminal)
242#
243# initialize scrollbar
244#
245 self.scrollbar.valueChanged.connect(self.do_scrollbar)
246 self.scrollbar.setEnabled(False)
247#
248# enable/disable
249#
250 def enable(self):
251 self.scrollbar.setEnabled(True)
252 self.HPTerminal.enable()
253 return
255 def disable(self):
256 self.scrollbar.setEnabled(False)
257 self.HPTerminal.disable()
258 return
259#
260# enable/disable keyboard input
261#
262 def enable_keyboard(self):
263 self.terminalwidget.set_kbdfunc(self.pildevice.putDataToHPIL)
265 def disable_keyboard(self):
266 self.terminalwidget.set_kbdfunc(None)
268#
269# scrollbar value changed action
270#
271 def do_scrollbar(self):
272 self.HPTerminal.scroll_to(self.scrollbar.value())
273 self.scrollbar.setEnabled(True)
274#
275# setting function pass through to backend
276#
277 def set_charset(self,charset):
278 self.HPTerminal.set_charset(charset)
280#
281# set keyboard type
282#
283 def set_keyboardtype(self,t):
284 self.terminalwidget.set_keyboardtype(t)
285#
286# tab becomes visible, call appropriate methods to:
287# - stop cursor blink
288# - disable update of the graphics scene (the buffer gets still updated)
289#
290 def becomes_visible(self):
291 self.terminalwidget.becomes_visible()
292 self.HPTerminal.becomes_visible()
293#
294# tab becomes invisible, call appropriate methods to:
295# - restart cursor blink
296# - enable update of the graphics scene
297# - redraw graphics scene
298#
299 def becomes_invisible(self):
300 self.terminalwidget.becomes_invisible()
301 self.HPTerminal.becomes_invisible()
302#
303# output character to terminal
304#
305 def out_terminal(self,t):
306 self.HPTerminal.out_terminal(t)
307#
308# reset terminal
309#
310 def reset_terminal(self):
311 self.HPTerminal.reset_terminal()
312#
313# redraw do nothing
314#
315 def redraw(self):
316 return
317#
318# set pildevice
319#
320 def set_pildevice(self,device):
321 self.pildevice= device
322#
323# reconfigure
324#
325 def reconfigure(self):
326 self.terminalwidget.reconfigure()
327 self.HPTerminal.reconfigure()
328#
329# get actual number of columns
330#
331 def get_cols(self):
332 return self.terminalwidget.get_cols()
333#
334# get actual number of rows
335#
336 def get_rows(self):
337 return self.terminalwidget.get_rows()
338#
339# Select area custom class ---------------------------------------------
340#
341class cls_SelectArea(QtWidgets.QGraphicsItem):
343 def __init__(self,start_row, start_col, end_row, end_col,cols, char_height, true_w,fillcolor):
344 super().__init__()
345 area_rows= end_row - start_row +1
346 w= true_w[cols]
347 h= area_rows * char_height
348 self.rect=QtCore.QRectF(0,0,w,h)
349 self.brush=QtGui.QBrush(fillcolor)
350#
351# construct select area polygon
352#
353 if start_row== end_row:
354 self.areapolygon= QtGui.QPolygon([QtCore.QPoint(true_w[start_col],0),
355 QtCore.QPoint(true_w[end_col+1],0),
356 QtCore.QPoint(true_w[end_col+1],char_height),
357 QtCore.QPoint(true_w[start_col],char_height),
358 QtCore.QPoint(true_w[start_col],0)])
359 else:
360 self.areapolygon= QtGui.QPolygon([QtCore.QPoint(true_w[start_col],0),
361 QtCore.QPoint(true_w[cols],0),
362 QtCore.QPoint(true_w[cols], (area_rows-1)* char_height),
363 QtCore.QPoint(true_w[end_col+1],(area_rows-1)* char_height),
364 QtCore.QPoint(true_w[end_col+1], area_rows* char_height),
365 QtCore.QPoint(0,area_rows* char_height),
366 QtCore.QPoint(0,char_height),
367 QtCore.QPoint(true_w[start_col],char_height),
368 QtCore.QPoint(true_w[start_col],0)])
370#
371# boundingRect and setPos are necessary for custim graphics items
372#
373 def boundingRect(self):
374 return self.rect
376 def setPos(self,x,y):
377 super().setPos(x,y)
378 return
379#
380# paint select area
381#
382 def paint(self,painter,option,widget):
383 painter.setBrush(self.brush)
384 painter.drawPolygon(self.areapolygon)
386#
387# terminal cursor custom class ------------------------------------------------------
388#
389class TermCursor(QtWidgets.QGraphicsItem):
391 def __init__(self,w,h,cursortype,foregroundcolor):
392 super().__init__()
393 self.w=w
394 self.h=h
395 self.rect=QtCore.QRectF(0,0,w,h)
396 self.cursorcolor=foregroundcolor
397 self.cursortype=cursortype
398 self.brush=QtGui.QBrush(foregroundcolor)
399 self.draw=True
400 self.blink_timer=QtCore.QTimer()
401 self.blink_timer.setInterval(CURSOR_BLINK)
402 self.blink_timer.timeout.connect(self.do_blink)
403 self.blink_timer.start()
404# fixed DEPRECATED use of float argument for int parameter
405 self.insertpolygon=QtGui.QPolygon([QtCore.QPoint(0,int(self.h/2)),
406 QtCore.QPoint(int(self.w*0.8),self.h),
407 QtCore.QPoint(int(self.w*0.8),int(self.h*0.67)),
408 QtCore.QPoint(self.w,int(self.h*0.67)),
409 QtCore.QPoint(self.w,int(self.h*0.33)),
410 QtCore.QPoint(int(self.w*0.8),int(self.h*0.33)),
411 QtCore.QPoint(int(self.w*0.8),0),
412 QtCore.QPoint(0,int(self.h/2))])
413#
414# called when terminal widget becomes invisible, stop cursor blink
415#
416 def stop(self):
417 self.blink_timer.stop()
418#
419# called when terminal widget becomes visible, start cursor blink
420#
421 def start(self):
422 self.blink_timer.start()
423#
424# timeout procedure, switch self.draw and fire draw event
425#
426 def do_blink(self):
427 if not self.scene():
428 return
429 self.draw= not self.draw
430 self.update()
431#
432# boundingRect and setPos are necessary for custim graphics items
433#
434 def boundingRect(self):
435 return self.rect
437 def setPos(self,x,y):
438 super().setPos(x,y)
439 return
440#
441# paint cursor (insert or overwrite cursor)
442#
443 def paint(self,painter,option,widget):
444 if self.draw:
445 if self.cursortype== CURSOR_OVERWRITE:
446 painter.setBrush(self.brush)
447 painter.fillRect(0,0,self.w,self.h,self.cursorcolor)
448 if self.cursortype== CURSOR_INSERT:
449 painter.setBrush(self.brush)
450 painter.drawPolygon(self.insertpolygon)
452#
453# non scrolled terminal widget (front end) class ------------------------------------
454#
456class QTerminalWidget(QtWidgets.QGraphicsView):
458# color scheme: foreground, background, transparent (for selection area only)
460 color_schemes= [
461 [ QtGui.QColor("#000"),QtGui.QColor("#fff"), QtGui.QColor(0xff,0xff, 0xff,0xc0) ],
462 [ QtGui.QColor("#000"), QtGui.QColor("#ffbe00"), QtGui.QColor(0xff, 0xbe, 0x00,0xc0) ],
463 [ QtGui.QColor("#000"), QtGui.QColor("#18f018"), QtGui.QColor(0x00,0xff,0x00,0xc0) ],
464 [ QtGui.QColor("#fff"), QtGui.QColor("#000"), QtGui.QColor(0xff,0xff,0xff,0xc0) ],
465 ]
467 def __init__(self,parent,name,tabwidget):
468 super().__init__(parent)
469#
470# initialize Variables
471#
472 self._name= name # name of tab
473 self._tabwidget=tabwidget # widget of tab
474 self._cols= -1 # terminal config, initialized in reconfigure
475 self._rows= -1 # rows, calculated at resize event
476 self._font_size=size= -1 # dto
477 self._color_scheme_index= -1 # dto
478 self._scrollupbuffersize= -1 # dto
479 self._keyboard_type=-1 # keyboard type (HP-71 or HP-75)
480 self._HPTerminal= None # backend object,set by setHPterminal
481 self._screen = [] # frontend screen buffer
482 self._cursor_col = 0 # cursor position
483 self._cursor_row = 0 # dto.
484 self._kbdfunc= None # function that is called to send keyboard input
485 # to the loop, set by setkbdfunc
486 self._alt_sequence= False # Variables for parsing ALT keyboard sequences
487 self._alt_seq_length=0 #
488 self._alt_seq_value=0 #
489 self._alt_modifier= False # Alt key pressed
490 self._shift_modifier=False # Shift key pressed
491 self._ctrl_modifier=False # Ctrl key pressed
492 self._modifier_flags=0 # Modifier flags for look up
493 self._cursortype= CURSOR_OFF # cursor mode (off, overwrite, insert)
494 self._cursor_char= 0x20 # character at cursor position
495 self._cursor_attr=-1 # attribute at cursor position
496 self._font=QtGui.QFont(FONT) # monospaced font
497 self._isVisible= False # visible state
498 self._press_pos= None # mouse click position
499 self._selectionText="" # text of selection
500 self._scrollUpAreaY=0 # display area that activates scroll up
501 self._scrollDownAreaY=0 # display area that activates scroll down
502 self._saved_pos=None # saved last cursor move position
503#
504# If this timer is active, autorpeat keyboard entries are ignored
505#
506 self._inhibitAutorepeatTimer= QtCore.QTimer()
507 self._inhibitAutorepeatTimer.setInterval(KEYBOARD_DELAY)
508 self._inhibitAutorepeatTimer.setSingleShot(True)
509 self._autoscrollMode= AUTOSCROLL_OFF
510 self._autoscrollTimer=QtCore.QTimer()
511 self._autoscrollTimer.setInterval(AUTOSCROLL_RATE)
512 self._autoscrollTimer.timeout.connect(self.do_autoscroll)
514#
515# Initialize graphics view and screne, set view background
516#
517 self._scene= QtWidgets.QGraphicsScene()
518 self.setScene(self._scene)
519 self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
520 self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
521 self._cursorItem=None
522#
523# widget cursor type
524#
525 self.setCursor(QtCore.Qt.IBeamCursor)
526 self.setFocusPolicy(QtCore.Qt.ClickFocus)
527#
528# system clipboard object
529#
530 self._clipboard= QtWidgets.QApplication.clipboard()
531#
532# now configure
533#
534 self.reconfigure()
535#
536# reconfigure
537#
538 def reconfigure(self):
539#
540# get current configuration
541#
542 self._cols=PILCONFIG.get(self._name,"terminalwidth")
543 self._color_scheme_index=PILCONFIG.get(self._name,"colorscheme")
544 self._font_size=PILCONFIG.get_dual(self._name,"terminalcharsize")
545 self._scrollupbuffersize=PILCONFIG.get(self._name,"scrollupbuffersize")
546 if self._scrollupbuffersize < TERMINAL_MINIMUM_ROWS:
547 self._scrollupbuffersize= TERMINAL_MINIMUM_ROWS
549#
550# determine font metrics and character size in pixel
551#
552 self._font.setPixelSize(self._font_size)
553 metrics= QtGui.QFontMetrics(self._font)
554 self._char_width=metrics.maxWidth()
555 self._char_height=metrics.height()
556 self._ScrollUpAreaY= self._char_height
557#
558# we can't relay that the nth column begins at (n-1)*self._char_width
559# because of rounding errors, so determine the correct column position
560# the last element is used to determine the true line width
561#
562 s=""
563 self._true_w= []
564 for i in range(self._cols+1):
565 self._true_w.append(metrics.boundingRect(s).width())
566 s+="A"
567#
568# set minimum dimensions for "cols" columns and 24 rows
569#
570 self._minw= self._true_w[len(self._true_w)-1] # true width
571 self._minh= self._char_height* TERMINAL_MINIMUM_ROWS
572#
573# calculate size hints
574#
575 self._sizew= self._minw
576 self._sizeh= self._char_height* self._scrollupbuffersize
577 self.setMaximumSize(self._sizew,self._sizeh)
578#
579# initialize selected color scheme
580#
581# self._color_scheme=self.color_schemes[self.color_scheme_names[self._colorscheme]]
582 self._color_scheme=self.color_schemes[self._color_scheme_index]
583 self._cursor_color=self._color_scheme[2]
584 self.setBackgroundBrush(QtGui.QBrush(self._color_scheme[0]))
585#
586# now shrink all parent windows to minimum size
587#
588# w=self.parentWidget()
589# while w is not None:
590# w.adjustSize()
591# w=w.parentWidget()
592# return
593#
594# overwrite standard methods
595#
596# minimum size hint
597#
598 def minimumSizeHint(self):
599 return QtCore.QSize(self._minw,self._minh)
600#
601# resize event, adjust number of rows and reconfigure graphicsscene and backend
602#
603 def resizeEvent(self, event):
604 rows= self.height() // self._char_height
605 self._HPTerminal.resize_rows(rows)
606 self._scene.setSceneRect(0,0,self._sizew, self._char_height* rows)
607 self.fitInView(0,0,self._sizew, self._char_height* rows)
608 self._ScrollDownAreaY= (self._char_height-1) * rows
609 self._rows= rows
610 self._tabwidget.update_status(rows,self._cols)
611#
612# context menu event (pops up when right button clicked)
613#
614 def contextMenuEvent(self,event):
615 menu=QtWidgets.QMenu()
616#
617# show copy menu entry only if selection text is available
618#
619 copyAction=None
620 if self._selectionText != "":
621 copyAction=menu.addAction("Copy")
622 pasteAction=None
623#
624# show paste menu entry only if a keyboard is emulated
625#
626 if self._kbdfunc is not None:
627 pasteAction=menu.addAction("Paste")
628 action=menu.exec(self.mapToGlobal(event.pos()))
629 if action is not None:
630#
631# copy to system clipboard
632#
633 if action == copyAction:
634 self._clipboard.setText(self._selectionText, QtGui.QClipboard.Clipboard)
635#
636# paste, only send printable characters, replace lf with ENDLINE
637#
638 if action == pasteAction:
639 paste_text = self._clipboard.text(QtGui.QClipboard.Clipboard)
640 for c in paste_text:
641 t=ord(c)
642 if t >= 0x20 and t <= 0x7E:
643 self._kbdfunc(t)
644 elif t == 0x0A:
645 self._kbdfunc(0x1B)
646 self._kbdfunc(82)
647 self._press_pos = None
648 self._HPTerminal.selectionStop()
649 self._selectionText=""
650 event.accept()
651 return
652#
653# Mouse press event we only process the left button, right button is context menu
654#
655 def mousePressEvent(self, event):
656 button = event.button()
657#
658# left button, quit selection and start a new one
659#
660 if button == QtCore.Qt.LeftButton:
661 self._HPTerminal.selectionStop()
662 self._selectionText=""
663#DEPRECATED pos call
664 self._press_pos = event.pos()
665 if not self._HPTerminal.selectionStart(self._press_pos,self._true_w, self._char_height):
666 self._press_pos = None
667#
668# Mouse move event, draw selection area and get selection text
669#
670 def mouseMoveEvent(self, event):
671 if self._press_pos:
672 move_pos = event.pos()
673 self._saved_pos= move_pos
674 if move_pos.y() < self._ScrollUpAreaY and move_pos.y() >=0 :
675 self.set_autoscroll(AUTOSCROLL_UP)
676 elif move_pos.y() > self._ScrollDownAreaY and move_pos.y() <=self.height() :
677 self.set_autoscroll(AUTOSCROLL_DOWN)
678 else:
679 self.set_autoscroll(AUTOSCROLL_OFF)
681 if self._HPTerminal.selectionMove(move_pos,self._true_w, self._char_height):
682 self._selectionText= self._HPTerminal.getSelectionText()
683#
684# Mouse release event, stop autoscroll unconditionally
685#
686 def mouseReleaseEvent(self, event):
687 self.set_autoscroll(AUTOSCROLL_OFF)
688#
689# autoscroll control function
690#
691 def set_autoscroll(self,mode):
692 if mode != self._autoscrollMode:
693 self._autoscrollMode= mode
694 if self._autoscrollMode==AUTOSCROLL_OFF:
695 self._autoscrollTimer.stop()
696 else:
697 self._autoscrollTimer.start()
698#
699# autoscroll timer function
700#
701 def do_autoscroll(self):
702 if self._autoscrollMode==AUTOSCROLL_DOWN:
703 self._HPTerminal.scroll_view_down()
704 if self._autoscrollMode==AUTOSCROLL_UP:
705 self._HPTerminal.scroll_view_up()
706 if self._HPTerminal.selectionMove(self._saved_pos,self._true_w, self._char_height):
707 self._selectionText= self._HPTerminal.getSelectionText()
708#
709# Mouse wheel event: scroll
710#
711 def wheelEvent(self,event):
712 numDegrees= event.angleDelta()/8
713 if numDegrees.y() is not None:
714 if numDegrees.y() < 0:
715 self._HPTerminal.scroll_view_down()
716 if numDegrees.y() > 0:
717 self._HPTerminal.scroll_view_up()
718 event.accept()
719#
720# overwriting this method prevents finding the next child widget and
721# enables the TAB key for normal keyboard input
722#
723 def focusNextPrevChild(self,flag):
724 return False
725#
726# focus out event, reset keyboard
727#
728 def focusOutEvent(self,event):
730 self._alt_modifier=False
731 self._alt_sequence= False
732 self._ctrl_modifier=False
733 self._shift_modifier=False
734 self._modifier_flags=0
735 return
736#
737# keyboard release event, only hanlde modifier keys
738#
739 def keyReleaseEvent(self, event):
740 key = event.key()
741#
742# handle modifier keys
743#
744 if key == QtCore.Qt.Key_Alt:
745 self._alt_modifier= False
746 self._alt_sequence= False
747 self._modifier_flags= self._modifier_flags & 0x600000000
748 return
749 elif key == QtCore.Qt.Key_Shift:
750 self._shift_modifier= False
751 self._modifier_flags= self._modifier_flags & 0x500000000
752 return
753 elif key == QtCore.Qt.Key_Control:
754 if not isMACOS():
755 self._ctrl_modifier= False
756 self._modifier_flags= self._modifier_flags & 0x300000000
757 return
758 elif key == QtCore.Qt.Key_Meta:
759 if isMACOS():
760 self._ctrl_modifier= False
761 event.accept()
762 return
763#
764# inhibit autorepeat if timer is running
765#
766 def inhibitAutorepeat(self):
767 self._inhibitAutorepeatTimer.start()
769 def isAutorepeatInhibited(self):
770 return self._inhibitAutorepeatTimer.isActive()
771#
772# keyboard pressed event, process keys and put them into the HP-IL outdata buffer
773#
774 def keyPressEvent(self, event):
775 if self._kbdfunc is None:
776 event.accept()
777 return
778 if event.isAutoRepeat():
779 if self.isAutorepeatInhibited():
780 event.accept()
781 return
782 self.inhibitAutorepeat()
783 text = event.text()
784 key = event.key()
786#
787# handle modifier keys first
788#
789 if key == QtCore.Qt.Key_Alt:
790 self._alt_modifier= True
791 self._modifier_flags= self._modifier_flags | KEYBOARD_ALT
792 elif key == QtCore.Qt.Key_Shift:
793 self._shift_modifier= True
794 self._modifier_flags= self._modifier_flags | KEYBOARD_SHIFT
795 elif key == QtCore.Qt.Key_Control:
796 self._modifier_flags= self._modifier_flags | KEYBOARD_CTRL
797 if not isMACOS():
798 self._ctrl_modifier=True
799 elif key == QtCore.Qt.Key_Meta:
800 if isMACOS():
801 self._ctrl_modifier=True
802 self._modifier_flags= self._modifier_flags | KEYBOARD_CTRL
803#
804# Alt pressed without Shift or Ctrl
805#
806 elif self._modifier_flags== KEYBOARD_ALT:
807#
808# process Altnnn
809#
810 if self._alt_sequence:
811 if key >= QtCore.Qt.Key_0 and key <= QtCore.Qt.Key_9:
812 self._alt_seq_value*=10
813 self._alt_seq_value+= key - QtCore.Qt.Key_0
814 self._alt_seq_length+=1
815 if self._alt_seq_length == 3:
816 if self._alt_seq_value <= 127:
817# print("keyboard alt value: ",self._alt_seq_value)
818 self._kbdfunc(self._alt_seq_value)
819 self._alt_sequence= False
820 else:
821 self._alt_sequence= False
822 else:
823#
824# check if a new alt sequence begins
825#
826 if key== QtCore.Qt.Key_1 or key == QtCore.Qt.Key_0 :
827 self._alt_sequence=True
828 self._alt_seq_length=1
829 self._alt_seq_value=key - QtCore.Qt.Key_0
830#
831# no alt sequence, do key lookup (macOS only)
832#
833 else:
834 alt_mode_lookup=[]
835 if isMACOS():
836# print("keyboard ALT mode lookup for ",key)
837 alt_mode_lookup= keyboard_lookup(key | self._modifier_flags, self._keyboard_type)
838# print("lookup result",alt_mode_lookup)
839 for i in alt_mode_lookup:
840 self._kbdfunc(i)
841#
842# proces shortcuts
843#
844 if not alt_mode_lookup :
845 if key >= QtCore.Qt.Key_A and key <= QtCore.Qt.Key_Z:
846 shortcut_index= key- QtCore.Qt.Key_A
847 shortcut_text,shortcut_flag= SHORTCUTCONFIG.get_shortcut(shortcut_index)
848 self.kbdstring(shortcut_text)
849# print("Shortcut look up ",shortcut_text)
850 if shortcut_flag== SHORTCUT_EXEC:
851 self.fake_key(QtCore.Qt.Key_Return)
852 elif shortcut_flag== SHORTCUT_EDIT:
853 self.fake_key(QtCore.Qt.Key_Left)
854 self.fake_key(QtCore.Qt.Key_Left)
855 elif shortcut_flag== SHORTCUT_INSERT:
856 self.fake_key(QtCore.Qt.Key_Left)
857 self.fake_key(QtCore.Qt.Key_Left)
858 self.fake_key(QtCore.Qt.Key_Insert)
860 else: # all other keyboard input
861#
862# local keys (not table driven at the moment)
863#
864 if key== QtCore.Qt.Key_PageUp:
865 self._HPTerminal.out_terminal(0x1B)
866 self._HPTerminal.out_terminal(0x56)
867 elif key== QtCore.Qt.Key_PageDown:
868 self._HPTerminal.out_terminal(0x1B)
869 self._HPTerminal.out_terminal(0x55)
870#
871# all other remote keys
872#
873 else:
874 lookup= keyboard_lookup(key | self._modifier_flags, self._keyboard_type)
875#
876# found key replacement, send it
877#
878# print("Keyboard lookup ", lookup, event.isAutoRepeat())
879 if lookup:
880 for i in lookup:
881 self._kbdfunc(i)
882#
883# no keyboard replacement, use text (if any)
884#
885 else:
886# print("keyboard: text ",text)
887 if text:
888 self.kbdstring(text)
890 event.accept()
891 return
892#
893# send string from keyboard to HP-IL, but only printable lower ASCII
894#
895 def kbdstring(self,s):
896 for c in s:
897 t=ord(c)
898# if t >= 0x20 and t <= 0x7E:
899 if t <= 0x7E:
900 self._kbdfunc(t)
901 return
902#
903# send a faked key to HP-IL
904#
905 def fake_key(self,key):
906 lookup= keyboard_lookup(key, self._keyboard_type)
907 for c in lookup:
908 self._kbdfunc(c)
909 return
910#
911# External interface
912#
913# make backend known to frontend
914#
915 def setHPTerminal(self,hpterminal):
916 self._HPTerminal= hpterminal
917#
918# initial terminal size
919#
920 self._HPTerminal.resize_rows(TERMINAL_MINIMUM_ROWS)
921#
922# set keyboard type
923#
924 def set_keyboardtype(self,t):
925 self._keyboard_type=t
926#
927# get cursor type (insert, replace, off)
928#
929 def getCursorType(self):
930 return(self._cursortype)
931#
932# set cursor type (insert, replace, off)
933#
934 def setCursorType(self,t):
935 self._cursortype=t
936#
937# register external function to process keyboard requests
938#
939 def set_kbdfunc(self,func):
940 self._kbdfunc= func
941#
942# get actual number of columns
943#
944 def get_cols(self):
945 return (self._cols)
946#
947# get actual number of rows
948#
949 def get_rows(self):
950 return (self._rows)
951#
952# tab becomes invisible, stop cursor blink, set self._isVisible to false to disable
953# updates of the graphics scene
954#
955 def becomes_invisible(self):
956 if self._cursorItem is not None:
957 self._cursorItem.stop()
958 self._isVisible=False
959#
960# tab becomes visible, start cursor blink, set self._isVisible to true to enable
961# updates of the graphics scene
962#
963 def becomes_visible(self):
964 if self._cursorItem is not None:
965 self._cursorItem.start()
966 self._isVisible=True
967#
968# draw terminal content, this is called by the backend
969#
970 def update_term(self,dump):
971#
972# do nothing if not visible
973#
974 if not self._isVisible:
975 return
976#
977# fetch screen buffer dump from backend
978#
979 (self._cursor_col, self._cursor_row, self._cursor_attr, start_row, start_col, end_row, end_col), self._screen = dump()
980#
981# clear scene, remove and delete display items
982#
983 olditemlist= self._scene.items()
984 for item in olditemlist:
985 self._scene.removeItem(item)
986 item=None
987 self._cursorItem=None
988 y=0
989 text=[]
990#
991# initialize attributes (bg/fg-color, invers attribute
992#
993 background_color = self._color_scheme[0]
994 foreground_color = self._color_scheme[1]
995 fgbrush=QtGui.QBrush(foreground_color)
996 bgbrush=QtGui.QBrush(background_color)
997 self._font.setUnderline(False)
998 invers_flag= False
999#
1000# loop over each row
1001#
1002 for row,line in enumerate(self._screen):
1003 col=0
1004 text_line=""
1005#
1006# loop over each item in a row
1007#
1008 for item in line:
1009#
1010# item is a string
1011#
1012 if isinstance (item,str):
1013 x=self._true_w[col]
1014 length= len(item)
1015#
1016# if inverse flag set, add background rectangle to scene
1017#
1018 if invers_flag:
1019 fillItem=QtWidgets.QGraphicsRectItem(0,0,self._true_w[col+length]- self._true_w[col],self._char_height)
1020 fillItem.setBrush(bgbrush)
1021 fillItem.setPos(x,y)
1022 self._scene.addItem(fillItem)
1023#
1024# add text to scene
1025#
1026 txtItem=QtWidgets.QGraphicsSimpleTextItem(item)
1027 txtItem.setFont(self._font)
1028 txtItem.setPos(x,y)
1029 txtItem.setBrush(fgbrush)
1030 self._scene.addItem(txtItem)
1031 col += length
1032 else:
1033#
1034# item is attribute, set fg/bg color
1035#
1036 if item== CHAR_ATTRIB_INVERSE_SHORT:
1037 background_color = self._color_scheme[1]
1038 foreground_color = self._color_scheme[0]
1039 invers_flag= True
1040 else:
1041 background_color = self._color_scheme[0]
1042 foreground_color = self._color_scheme[1]
1043 invers_flag= False
1044 fgbrush=QtGui.QBrush(foreground_color)
1045 bgbrush=QtGui.QBrush(background_color)
1046#
1047# underline
1048#
1049 self._font.setUnderline(False)
1050 if item == CHAR_ATTRIB_UNDERLINE_SHORT:
1051 self._font.setUnderline(True)
1052 y += self._char_height
1053#
1054# add selection area to scene
1055#
1056 cursor_in_selection= False
1057 if start_row is not None:
1058#
1059# check if cursor is in selection
1060#
1061 startidx= start_row* self._cols+ start_col
1062 endidx= end_row* self._cols+ end_col
1063 cursoridx= self._cursor_row* self._cols + self._cursor_col
1064 if cursoridx >= startidx and cursoridx <= endidx:
1065 cursor_in_selection= True
1066 selectAreaItem= cls_SelectArea(start_row, start_col, end_row, end_col,
1067 self._cols, self._char_height, self._true_w,
1068 self._cursor_color)
1069 selectAreaItem.setPos(0,start_row* self._char_height)
1070 self._scene.addItem(selectAreaItem)
1072#
1073# add cursor at cursor position to scene
1074#
1075 if self._cursortype != CURSOR_OFF and not cursor_in_selection:
1076 if self._cursor_attr:
1077 cursor_foreground_color= self._color_scheme[0]
1078 else:
1079 cursor_foreground_color= self._color_scheme[1]
1080 self._cursorItem= TermCursor(self._char_width,self._char_height,self._cursortype, cursor_foreground_color)
1081 self._cursorItem.setPos(self._true_w[self._cursor_col],self._cursor_row*self._char_height)
1082 self._scene.addItem(self._cursorItem)
1083 self._font.setUnderline(False)
1085#
1086# Terminal backend class -----------------------------------------------------------
1087#
1088# The terminal backend class maintains the terminal line buffer. The terminal
1089# line buffer content is arranged by the input of text and escape sequences that
1090# control editing and and the cursor position.
1091# The backend class also maintains the boundaries of the visible part of the
1092# line buffer that is rendered by the frontend widget and sets the scrollbar parameters.
1093#
1094class HPTerminal:
1096 def __init__(self, win, name):
1097 self.name=name # name of tab
1098 self.w = -1 # terminal/buffer width (characters)
1099 self.h = -1 # buffer size (lines)
1100 # both variables are initialized in
1101 # reconfigure
1102 self.actual_h=0 # number of lines in the buffer
1103 self.fesc= False # status indicator for parsing esc
1104 # sequences
1105 self.movecursor= 0 # status indicator for parsing the
1106 # move cursor escape sequence
1107 self.movecol=0 # same as above
1108 self.win=win # terminal widget
1109 self.view_h= 0 # terminal size (lines)
1110 # initialized if window was resized
1111 self.view_y0=0 # bottom of buffer view
1112 self.view_y1=0 # top of buffer view
1113 self.blink_counter=0 # counter that controls cursor blink
1114 self.charset=CHARSET_HP71 # character set used for output
1115 self.cx=0 # actual cursor position
1116 self.cy=0
1117 self.insert=False # inser mode flag
1118 self.saved_cursortype=CURSOR_OVERWRITE # saved cursor type
1119 self.attr = CHAR_ATTRIB_NONE # character attribute
1120 self.screen=None # Terminal line buffer array
1121 # initialized by reset_screen
1122 self.linelength=None # Vector with length of each line
1123 # in the buffer array, initialized
1124 # by reset screen
1125 # a value of -1 indicates a continuation
1126 # line of a wrapped line
1127 self.needsUpdate =False # indicator that a screen update is needed
1128 # triggered by:
1129 # reconfigure, becomes_visible,
1130 # scroll_view_up, scroll_view_down,
1131 # terminal output
1132 self.press_row= 0 # row, col of selection press position
1133 self.press_col=0
1134 self.start_row= 0 # row, col of selection (upper left)
1135 self.start_col= 0
1136 self.move_row= 0 # row, col of selection (upper right)
1137 self.move_col= 0
1138 self.showSelection=False # display a selection area
1139 self.reconfigure()
1140#
1141# Queue with input data that will be displayed
1142#
1143 self.termqueue= queue.Queue()
1144 self.termqueue_lock= threading.Lock()
1145#
1146# Timer that triggers the processing of the input queue
1147#
1148 self.UpdateTimer= QtCore.QTimer()
1149 self.UpdateTimer.setSingleShot(True)
1150 self.UpdateTimer.timeout.connect(self.process_queue)
1151#
1152# reconfigure: changing the number of columns or the scrollup buffersize
1153# result in a hard reset of the screen
1154#
1155 def reconfigure(self):
1156 h = PILCONFIG.get(self.name,"scrollupbuffersize")
1157 if h < TERMINAL_MINIMUM_ROWS:
1158 h= TERMINAL_MINIMUM_ROWS
1159 if h != self.h:
1160 self.h=h
1161 self.reset_hard()
1162 w=PILCONFIG.get(self.name,"terminalwidth")
1163 if w != self.w:
1164 self.w= w
1165 self.reset_hard()
1166 self.needsUpdate=True
1167 return
1168#
1169# Reset functions
1170#
1171 def reset_hard(self):
1172 self.attr = CHAR_ATTRIB_NONE
1173 # Invoke other resets
1174 self.reset_screen()
1175 self.reset_soft()
1177 def reset_soft(self):
1178 self.attr = CHAR_ATTRIB_NONE
1179 # Modes
1180 self.insert = False
1181 self.movecursor=0
1182 self.showSelection=False
1184 def reset_screen(self):
1185 # Screen
1186 self.screen = array.array('i', [CHAR_ATTRIB_NONE | 0x20] * self.w * self.h)
1187 self.linelength= array.array('i', [0] * self.h)
1188 self.linewrapped= array.array('i',[False] * self.h)
1189 # Scroll parameters
1190 self.view_y0=0
1191 self.view_y1= self.view_h-1
1193 # Cursor position
1194 self.cx = 0
1195 self.cy = 0
1196 self.movecursor=0
1198 # Number of lines and scroll bar
1199 self.actual_h=0
1200 self.win.scrollbar.setMinimum(0)
1201 self.win.scrollbar.setMaximum(0)
1202 self.win.scrollbar.setSingleStep(1)
1203 self.win.scrollbar.setPageStep(self.view_h)
1204 self.saved_cursortype= CURSOR_OVERWRITE
1205#
1206# enable: start update timer (one shot timer)
1207#
1208 def enable(self):
1209 self.UpdateTimer.start(UPDATE_TIMER)
1210 pass
1211#
1212# disable: do nothing
1213#
1214 def disable(self):
1215 pass
1216#
1217# Terminal window was resized, update display and scrollbar
1218#
1219 def resize_rows(self,rows):
1220 if self.view_h == rows:
1221 return
1222 self.view_h= rows
1223 if self.view_y1 == -1:
1224 self.view_y1 = self.view_h-1
1225 else:
1226 self.view_y1= self.view_y0 +rows -1
1227# if self.view_y1 > rows:
1228# self.view_y1= rows-1
1229# self.view_y0= self.view_y1-rows
1230# if self.view_y0<0:
1231# self.view_y0=0
1232# self.view_y1= self.view_y0+rows-1 ## fix
1233 if self.actual_h >= self.view_h:
1234 self.win.scrollbar.setMaximum(self.actual_h-self.view_h+1)
1235 self.win.scrollbar.setPageStep(self.view_h)
1236 self.needsUpdate=True
1237#
1238# Low-level terminal functions on terminal line buffer
1239#
1240 def peek(self, y0, x0, y1, x1):
1241 return self.screen[self.w * y0 + x0:self.w * (y1 - 1) + x1]
1243 def poke(self, y, x, s):
1244 pos = self.w * y + x
1245 self.screen[pos:pos + len(s)] = s
1247 def fill(self, y0, x0, y1, x1, char):
1248 n = self.w * (y1 - y0 - 1) + (x1 - x0)
1249 self.poke(y0, x0, array.array('i', [char] * n))
1251 def clear(self, y0, x0, y1, x1):
1252 self.fill(y0, x0, y1, x1, CHAR_ATTRIB_NONE | 0x20)
1253#
1254# utility functions for wrapped lines
1255#
1256# get length of a wrapped line
1257#
1258 def get_wrapped_linelength(self,cy):
1259 if self.linelength[cy]==-1:
1260 return(self.linelength[cy-1])
1261 else:
1262 return(self.linelength[cy])
1263#
1264# set length of a wrapped line
1265#
1266 def set_wrapped_linelength(self,cy,ll):
1267 if self.linelength[cy]==-1:
1268 self.linelength[cy-1]= ll
1269 else:
1270 self.linelength[cy]= ll
1271#
1272# get the position of the cursor in the wrapped line
1273#
1274 def get_wrapped_cursor_x(self,cx,cy):
1275 if self.linelength[cy]==-1:
1276 return(cx+self.w)
1277 else:
1278 return(cx)
1279#
1280# check if line is wrapped, cursor may or may not be in the wrapped part
1281#
1282 def is_wrapped(self,cy):
1283 if self.linelength[cy]==-1:
1284 return(self.linewrapped[cy-1])
1285 else:
1286 return(self.linewrapped[cy])
1287#
1288# check, if cursor position is in the wrapped part
1289#
1290 def in_wrapped_part(self,cy):
1291 return(self.linelength[cy]== -1)
1292#
1293# add wrapped part of a line, cy must be in the wrapped part
1294#
1295 def add_wrapped_part(self,cy):
1296 self.linewrapped[cy-1]=True
1297 self.linelength[cy]=-1
1298 self.linewrapped[cy]=False
1299#
1300# remove wrapped part of a line, cy must be in the non wrapped part
1301#
1302 def remove_wrapped_part(self,cy):
1303 self.linewrapped[cy]=False
1304 self.linelength[cy+1]=0
1305 self.linewrapped[cy+1]=False
1306#
1307# scroll screen buffer up if no room to add a new line
1308# note: this adjust the object variables self.cy and self.actual_h
1309# the method returns the new value of self.cy
1310#
1311 def scroll_screenbuffer_up(self,n):
1312 self.poke(0, 0, self.peek(0 + n, 0, self.h, self.w))
1313 self.clear(self.h - n, 0, self.h, self.w)
1314 self.linelength[0:self.h-n]=self.linelength[n:]
1315 self.linewrapped[0:self.h-n]=self.linewrapped[n:]
1316 for i in range(n):
1317 self.linelength[self.h-n]=0
1318 self.linewrapped[self.h-n]=False
1319 self.cy-=n
1320 self.actual_h-=n
1321 return(self.cy)
1322#
1323# Scroll line right, add wrapped part if needed. If we exceed the length of the
1324# wrapped line, ignore characters
1325#
1326 def scroll_line_right(self, y, x):
1327 wx= self.get_wrapped_cursor_x(x,y)
1328 oldlinelength= self.get_wrapped_linelength(y)
1329 if oldlinelength == self.w*2:
1330 return
1331 newlinelength= oldlinelength+1
1332 self.set_wrapped_linelength(y,newlinelength)
1333 if wx < oldlinelength:
1334#
1335# move characters in wrapped part, add wrapped part if needed
1336#
1337 if self.get_wrapped_linelength(y) > self.w:
1338 if oldlinelength== self.w:
1339 sav_cy= self.cy
1340 self.cy=self.add_bufferline(self.cy)
1341 self.add_wrapped_part(self.cy)
1342#
1343# reset cy if cursor was not at end of display row
1344#
1345 if sav_cy != self.w-1:
1346 self.cy= sav_cy
1347 y=self.cy
1348 self.poke(y+1,1,self.peek(y+1, 0, y + 2, self.w))
1349 self.poke(y+1,0,self.peek(y,self.w-1,y+1,self.w))
1350 self.poke(y, x + 1, self.peek(y, x, y + 1, self.w - 1))
1351 self.clear(y, x, y + 1, x + 1)
1352#
1353# scroll line left, remove character at cursor position
1354#
1355 def scroll_line_left(self, y, x):
1356 wx= self.get_wrapped_cursor_x(x,y)
1357 oldlinelength= self.get_wrapped_linelength(y)
1358 if wx < oldlinelength:
1359#
1360# remove character, shift left, adjust line length
1361#
1362 self.poke(y, x, self.peek(y, x + 1, y + 1, self.w))
1363 self.clear(y, self.w - 1, y + 1, self.w)
1364 self.set_wrapped_linelength(y,oldlinelength-1)
1365#
1366# we are in a wrapped line and must move the wrapped part too.
1367#
1368 if oldlinelength > self.w:
1369 self.poke(y,self.w-1,self.peek(y+1,0,y+2,1))
1370 self.poke(y+1, 0, self.peek(y+1, 1, y + 2, self.w))
1371#
1372# if the wrapped part is empty and the cursor is in the non wrapped part
1373# remove the wrapped part
1374#
1375 if oldlinelength-1<= self.w and wx< self.w and self.is_wrapped(y):
1376 self.remove_wrapped_part(y)
1377 self.cy=self.remove_bufferline(y+1)
1378#
1379# View functions, scroll up and down
1380#
1381 def scroll_view_down(self):
1382 if self.view_y1< self.cy:
1383 self.view_y0+=1
1384 self.view_y1+=1
1385 self.needsUpdate=True
1386 self.win.scrollbar.setValue(self.view_y0)
1388 def scroll_view_up(self):
1389 if self.view_y0 >0:
1390 self.view_y0-=1
1391 self.view_y1-=1
1392 self.needsUpdate=True
1393 self.win.scrollbar.setValue(self.view_y0)
1395 def scroll_page_down(self):
1396 if self.view_y1>= self.cy:
1397 return
1398 if self.view_y1+ self.view_h > self.cy:
1399 self.view_y1= self.cy
1400 self.view_y0= self.view_y1-self.view_h+1
1401 else:
1402 self.view_y1+= self.view_h
1403 self.view_y0+= self.view_h
1404 self.needsUpdate=True
1405 self.win.scrollbar.setValue(self.view_y0)
1408 def scroll_page_up(self):
1409 if self.view_y0 == 0:
1410 return
1411 if self.view_y0 - self.view_h < 0:
1412 self.view_y0= 0
1413 self.view_y1= self.view_h-1
1414 else:
1415 self.view_y0-= self.view_h
1416 self.view_y1-= self.view_h
1417 self.needsUpdate=True
1418 self.win.scrollbar.setValue(self.view_y0)
1420#
1421# scroll to the last line of the buffer
1422#
1423 def scroll_view_to_bottom(self):
1424 self.win.scrollbar.setValue(self.win.scrollbar.maximum())
1425 return
1426#
1427# add new buffer line, scroll up the screen line buffer if needed
1428# note: this adjusts the object variables self.cy and self.last_h
1429# the scrollbar is adjusted to the new size of the screen line buffer
1430#
1431 def add_bufferline(self,cy):
1432 n=1
1433# if self.is_wrapped(0):
1434# n=2
1435 newcy=cy+1
1436 if newcy < self.h:
1437 self.actual_h= max(self.actual_h, newcy)
1438 if newcy == self.h:
1439 newcy=self.scroll_screenbuffer_up(n)+1
1440 self.actual_h+=1
1441 self.update_scrollbar()
1442 return(newcy)
1443#
1444# remove buffer line
1445#
1446 def remove_bufferline(self,cy):
1447 newcy= cy-1
1448 self.actual_h= newcy
1449 self.update_scrollbar()
1450 return(newcy)
1451#
1452# Update scroll bar parameters
1453#
1454 def update_scrollbar(self):
1455 if self.actual_h < self.view_h:
1456 self.win.scrollbar.setMaximum(0)
1457 else:
1458 self.win.scrollbar.setMaximum(self.actual_h-self.view_h+1)
1459 self.win.scrollbar.setValue(self.actual_h-self.view_h+1) ## fix
1460#
1461# clear from cursor to end of display
1462#
1463 def clear_to_eod(self):
1464 self.clear(self.cy,self.cx,self.h, self.w)
1465 self.actual_h= self.cy
1466 self.update_scrollbar()
1467#
1468# Cursor functions, up and down
1469#
1470 def cursor_up(self):
1471 self.cy = max(0, self.cy - 1)
1473 def cursor_down(self):
1474 self.cy = min(self.h - 1, self.cy + 1)
1475#
1476# Move cursor left, if the cursor is at the beginning of the wrapped part of a line
1477# then remove that wrapped part
1478#
1479 def cursor_left(self):
1480 self.cx= self.cx -1
1481 if self.cx < 0:
1482 self.cx= self.w-1
1483 self.cy= self.cy-1
1484 if self.cy < 0:
1485 self.cy=0
1486 self.cx=0
1487 if self.get_wrapped_linelength(self.cy) <= self.w and self.is_wrapped(self.cy):
1488 self.remove_wrapped_part(self.cy)
1489 self.cy=self.remove_bufferline(self.cy+1)
1490#
1491# move cursor right, add wrapped part of a line if needed
1492#
1493 def cursor_right(self):
1494 self.cx= self.cx +1
1495 if self.cx == self.w:
1496 self.cx=0
1497 if self.is_wrapped(self.cy):
1498 self.cy+=1
1499 else:
1500 if self.get_wrapped_linelength(self.cy) == self.w:
1501 self.cy=self.add_bufferline(self.cy)
1502 self.add_wrapped_part(self.cy)
1504 def cursor_set_x(self, x):
1505 self.cx = max(0, x)
1507 def cursor_set_y(self, y):
1508 self.cy = max(0, min(self.h - 1, y))
1510 def cursor_set(self, y, x):
1511 self.cursor_set_x(x)
1512 self.cursor_set_y(y)
1514 def cursor_far_left(self):
1515 if self.linelength[self.cy] == -1:
1516 self.cursor_set(self.cy-1,0)
1517 else:
1518 self.cursor_set(self.cy,0)
1520 def cursor_far_right(self):
1521 if self.linelength[self.cy] == -1:
1522 self.cursor_set(self.cy, (self.linelength[self.cy-1]-self.w-1))
1523 else:
1524 self.cursor_set(self.cy, (self.linelength[self.cy]-1))
1525#
1526# Dumb terminal output
1527#
1528# Backspace
1529#
1530 def ctrl_BS(self):
1531 cx= self.cx-1
1532 cy= self.cy
1533 if cx < 0:
1534 cy= cy-1
1535 if cy < 0:
1536 cx=0
1537 cy=0
1538 else:
1539 cx=self.w-1
1540 self.cursor_set(cy, cx)
1541#
1542# Linefeed
1543#
1544 def ctrl_LF(self):
1545 if self.is_wrapped(self.cy) and not self.in_wrapped_part(self.cy):
1546 self.cy+=1
1547 self.cy=self.add_bufferline(self.cy)
1548#
1549# Carriage Return
1550#
1551 def ctrl_CR(self):
1552 if self.is_wrapped(self.cy) and self.in_wrapped_part(self.cy):
1553 self.cy-=1
1554 self.cursor_set_x(0)
1555#
1556# Dumb echo, if we exceed the wrapped part then issue a CR/LF
1557#
1558 def dumb_echo(self, char):
1559# print("dumb_echo ",char)
1560 if self.insert:
1561 self.scroll_line_right(self.cy, self.cx)
1562 self.poke(self.cy, self.cx, array.array('i', [self.attr | char]))
1563 else:
1564 oldwrappedlinelength=self.get_wrapped_linelength(self.cy)
1565# print("dumb echo ",oldwrappedlinelength, self.w*2)
1566 if oldwrappedlinelength == self.w*2:
1567# print("dumb_echo cr/lf")
1568 self.ctrl_CR()
1569 self.ctrl_LF()
1570 self.poke(self.cy, self.cx, array.array('i', [self.attr | char]))
1571#
1572# do not increase line length if we overwrite existing text
1573#
1574 cursor_pos=self.get_wrapped_cursor_x(self.cx,self.cy)
1575 if cursor_pos== oldwrappedlinelength:
1576 self.set_wrapped_linelength(self.cy,oldwrappedlinelength+1)
1577 self.cursor_right()
1578#
1579# dump screen to terminal window, the data are painted during a paint event
1580#
1581 def dump(self):
1582 screen = []
1583 attr_ = -1
1584 cursor_char=0x20
1585 cursor_attr= -1
1586 cx, cy = min(self.cx, self.w - 1), self.cy
1587 for y in range(self.view_y0, self.view_y1+1):
1588 wx = 0
1589 line = [""]
1590 for x in range(0, self.w):
1591 try:
1592 d = self.screen[y * self.w + x] # fix possible index out of range error
1593 except IndexError:
1594 print("self.screen Index error %d %d"% (y,x))
1595 continue
1596 char = d & 0xffff
1597 attr = d >> 16
1598 if x== cx and y== cy:
1599 cursor_char=char
1600 cursor_attr=attr
1601#
1602# Attributes
1603#
1604 if attr != attr_:
1605 if attr_ != -1:
1606 line.append("")
1607 line.append(attr)
1608 line.append("")
1609 attr_ = attr
1610 wx += 1
1611 if wx <= self.w:
1612 line[-1] += chr(char)
1613 screen.append(line)
1615 if cy >= self.view_y0 and cy <= self.view_y1:
1616 cy= cy- self.view_y0
1617 else:
1618 cy= -1
1619 cx= -1
1620 if self.showSelection:
1621 (start_row, start_col, end_row, end_col)= self.getSelection()
1622 else:
1623 (start_row, start_col, end_row, end_col)= (None,None,None,None)
1624 return (cx, cy, cursor_attr,start_row, start_col, end_row, end_col), screen
1625#
1626# process terminal output queue and refresh display
1627#
1628 def process_queue(self):
1629#
1630# get items from terminal input queue
1631#
1632 items=[]
1633 self.termqueue_lock.acquire()
1634 while True:
1635 try:
1636 i=self.termqueue.get_nowait()
1637 items.append(i)
1638 self.termqueue.task_done()
1639 except queue.Empty:
1640 break
1641 self.termqueue_lock.release()
1642#
1643# process items and generate new terminal screen dump
1644#
1645 if len(items):
1646 self.needsUpdate=True
1647 for c in items:
1648 self.process(c)
1649 if self.needsUpdate:
1650 self.win.terminalwidget.update_term(self.dump)
1651 self.needsUpdate=False
1652 self.UpdateTimer.start(UPDATE_TIMER)
1653 return
1654#
1655# process output to display
1656#
1657 def process(self,t):
1659#
1660# start of ESC sequence, set flag and return
1661#
1662 if t == 27:
1663 self.fesc= True
1664 return
1665#
1666# process escape sequences, translate to pyqterm
1667#
1668 if self.fesc:
1669# print("Esc sequence %d %s " % (t,chr(t)))
1670 if t== 67: # cursor right (ESC C)
1671 self.cursor_right()
1672 elif t== 68: # cursor left (ESC D)
1673 self.cursor_left()
1674 elif t== 65: # cursor up (ESC A)
1675 self.cursor_up()
1676 elif t== 66: # cursor down (ESC B)
1677 self.cursor_down()
1678 elif t== 72: # move cursor to home position (ESC H)
1679 self.cursor_set(0,0)
1680 elif t== 74: # erase from cursor to end of screen (ESC J)
1681# adjustion of more display parameters required
1682 self.clear_to_eod()
1683 elif t== 75: # erase from cursor to end of the line (ESC K)
1684 self.clear(self.cy,self.cx,self.cy+1,self.w)
1685 elif t== 62: # Cursor on (ESC >)
1686 self.win.terminalwidget.setCursorType(self.saved_cursortype)
1687 elif t== 60: # Cursor off (ESC <)
1688 self.win.terminalwidget.setCursorType(CURSOR_OFF)
1689 elif t== 69: # Reset (ESC E)
1690 self.reset_soft()
1691 self.reset_screen()
1692 elif t== 80: # Clear Character (ESC P) ??
1693 self.clear(self.cy,self.cx,self.cy+1,self.cx+1)
1694 elif t== 79: # Clear Character with wrap back (ESC O)
1695 self.scroll_line_left(self.cy, self.cx)
1696 elif t== 81: # switch to insert cursor (ESC Q)
1697 self.saved_cursortype= CURSOR_INSERT
1698 self.win.terminalwidget.setCursorType(CURSOR_INSERT)
1699 elif t== 78: # swicht to insert cursor and insert mode (ESC N)
1700 self.saved_cursortype= CURSOR_INSERT
1701 self.insert = True
1702 self.win.terminalwidget.setCursorType(CURSOR_INSERT)
1703 elif t== 82: # switch to replace cursor and replace mode (ESC R)
1704 self.saved_cursortype= CURSOR_OVERWRITE
1705 self.insert = False
1706 self.win.terminalwidget.setCursorType(CURSOR_OVERWRITE)
1707 elif t== 83: # roll up (ESC S)
1708 self.scroll_view_up()
1709 elif t== 84: # roll down (ESC T)
1710 self.scroll_view_down()
1711 elif t== 85: # display next page (ESC U)
1712 self.scroll_page_down()
1713 elif t==86: # display previous page (ESC V)
1714 self.scroll_page_up()
1715 elif t== 101: # reset hard (ESC e)
1716 self.reset_hard()
1717 elif t== 3: # move cursor far right (ESC Ctrl c)
1718 self.cursor_far_right()
1719 elif t== 4: # move cursor far left (ESC ctrl d)
1720 self.cursor_far_left()
1721 elif t== 37: # Move Cursor to display position (ESC %)
1722 self.movecursor=1
1723 elif t==122: # reset
1724 self.reset_hard()
1725 else:
1726 print("terminal: unhandled escape sequence %d" % t)
1727 self.fesc= False
1729 else:
1730#
1731# Move cursor sequence part 1
1732#
1733 if self.movecursor == 1:
1734 self.movecursor=2
1735 self.movecol=t
1737#
1738# Move cursor sequence part 2
1739#
1740 elif self.movecursor == 2:
1741 self.movecursor=0
1742 if self.movecol < self.w and t < self.h:
1743 self.cursor_set(t,self.movecol)
1744#
1745# single character processing
1746#
1747 else:
1748 self.scroll_view_to_bottom()
1749 if t == 0xD: # CR
1750 self.ctrl_CR()
1751 elif t == 0xA: # LF
1752 self.ctrl_LF()
1753 elif t== 0x08: # BS
1754 self.ctrl_BS()
1755 elif t== 0x7F: # DEL
1756 self.clear(self.cy, self.cx, self.cy+1, self.cx+1)
1757 else:
1758#
1759# convert to unicode and set character attribute for the upper half
1760# of the code table
1761#
1762 cc= icharconv(t,self.charset)
1763 if t > 127:
1764 self.attr= CHAR_ATTRIB[self.charset]
1765 else:
1766 self.attr= CHAR_ATTRIB_NONE
1767 self.dumb_echo(ord(cc))
1768 return
1770#
1771# External interface
1772#
1773#
1774# set character set
1775#
1776 def set_charset(self,charset):
1777 self.charset= charset
1778#
1779# put character into terminal output buffer
1780#
1781 def out_terminal (self,t):
1782 self.termqueue_lock.acquire()
1783 self.termqueue.put(t)
1784 self.termqueue_lock.release()
1785#
1786# reset terminal, send ESC e
1787#
1788 def reset_terminal(self):
1789 self.termqueue_lock.acquire()
1790 self.termqueue.put(0x1b)
1791 self.termqueue.put(0x65)
1792 self.termqueue_lock.release()
1793#
1794# becomes visible, call update_term to redraw the view
1795#
1796 def becomes_visible(self):
1797 self.needsUpdate=True
1798#
1799# becomes_invisible: nothing to do
1800#
1801 def becomes_invisible(self):
1802 pass
1803#
1804# callback for scrollbar
1805#
1806 def scroll_to(self,value):
1807 self.view_y0= value
1808 self.view_y1= value + self.view_h-1
1809 self.needsUpdate=True
1810#
1811# Get true (scrolled!) row column in terminal line buffer from click position
1812#
1813 def row_col_from_px(self,pos,true_w,char_h):
1814 x=pos.x()
1815 y=pos.y()
1816#
1817# get column, use true_w
1818#
1819 col=0
1820 for i in range(self.w):
1821 if true_w[i]>x:
1822 col=i-1
1823 break
1824#
1825# get row, add margin
1826#
1827 y=y+ round(char_h/2)
1828 row=int(round((y- char_h) /char_h))+ self.view_y0
1829 if row < 0:
1830 row=0
1831#
1832# return NULL if position is beyond the last line
1833#
1834 if row > self.actual_h:
1835 return (None, None)
1836 else:
1837 return (row,col)
1838#
1839# Selection start, process press pos
1840#
1841 def selectionStart(self,pos,true_w,char_h):
1842 (self.press_row, self.press_col)= self.row_col_from_px(pos,true_w,char_h)
1843#
1844# return false if we have an illegal click position
1845#
1846 if self.press_row is None and self.press_col is None:
1847 return False
1848 else:
1849 return True
1850#
1851# Selection stop, do not display selection area any more
1852#
1853 def selectionStop(self):
1854 self.showSelection=False
1855 self.needsUpdate=True
1856#
1857# Selection move, process actual position, show selection
1858#
1859 def selectionMove(self,pos,true_w,char_h):
1860 (self.move_row, self.move_col)= self.row_col_from_px(pos,true_w,char_h)
1861#
1862# return false if we have an illegal click position
1863#
1864 if self.move_row is None or self.move_col is None:
1865 return False
1866 self.showSelection=True
1867 self.start_row= self.press_row
1868 self.start_col= self.press_col
1869#
1870# swap coordinates if necessary
1871#
1872 if (self.start_row* self.w+ self.start_col) > (self.move_row* self.w + self.move_col):
1873 temp=self.start_col
1874 self.start_col= self.move_col
1875 self.move_col=temp
1876 temp=self.start_row
1877 self.start_row= self.move_row
1878 self.move_row=temp
1879 self.needsUpdate=True
1880 return True
1881#
1882# return Text of selection
1883#
1884 def getSelectionText(self):
1885 selection_text=""
1886 rowcount= self.start_row
1887 row_text=""
1888 while rowcount <= self.move_row:
1889 if rowcount== self.start_row:
1890 colcount= self.start_col
1891 else:
1892 colcount=0
1893 if rowcount== self.move_row:
1894 colend= self.move_col
1895 else:
1896 colend= self.w-1
1897 while colcount <= colend:
1898 row_text+= (chr(self.screen[self.w*rowcount+colcount] & 0xFFFF))
1899 colcount+=1
1900 if self.is_wrapped(rowcount) and not self.in_wrapped_part(rowcount):
1901 rowcount+=1
1902 continue
1903 row_text=row_text.rstrip()
1904 if rowcount < self.move_row:
1905 row_text+="\n"
1906 rowcount+=1
1907 selection_text+= row_text
1908 row_text=""
1909 return selection_text
1910#
1911# return screen rows, columns of selection, returns None if outside
1912#
1913 def getSelection(self):
1914 if self.move_row is None or self.move_col is None:
1915 return (None, None, None, None)
1916 if self.start_row > self.view_y1 or self.move_row < self.view_y0:
1917 return (None, None, None, None)
1918 start_row= self.start_row
1919 start_col= self.start_col
1920 move_row= self.move_row
1921 move_col= self.move_col
1922 if self.start_row < self.view_y0:
1923 start_row= self.view_y0
1924 start_col=0
1925 if self.move_row > self.view_y1:
1926 move_row= self.view_y1
1927 move_col= self.w-1
1928 start_row -= self.view_y0
1929 move_row -= self.view_y0
1930 return (start_row, start_col, move_row, move_col)