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

1034 statements  

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() 

181 

182import array 

183import queue 

184import threading 

185import time 

186 

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 * 

193 

194CURSOR_OFF=0 

195CURSOR_INSERT=1 

196CURSOR_OVERWRITE=2 

197 

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): 

223 

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 

254 

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) 

264 

265 def disable_keyboard(self): 

266 self.terminalwidget.set_kbdfunc(None) 

267 

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) 

279 

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): 

342 

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)]) 

369 

370# 

371# boundingRect and setPos are necessary for custim graphics items 

372# 

373 def boundingRect(self): 

374 return self.rect 

375 

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) 

385 

386# 

387# terminal cursor custom class ------------------------------------------------------ 

388# 

389class TermCursor(QtWidgets.QGraphicsItem): 

390 

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 

436 

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) 

451 

452# 

453# non scrolled terminal widget (front end) class ------------------------------------ 

454# 

455 

456class QTerminalWidget(QtWidgets.QGraphicsView): 

457 

458# color scheme: foreground, background, transparent (for selection area only) 

459 

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 ] 

466 

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) 

513 

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 

548 

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) 

680 

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): 

729 

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() 

768 

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() 

785 

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) 

859 

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) 

889 

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) 

1071 

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) 

1084 

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: 

1095 

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() 

1176 

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 

1183 

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 

1192 

1193 # Cursor position 

1194 self.cx = 0 

1195 self.cy = 0 

1196 self.movecursor=0 

1197 

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] 

1242 

1243 def poke(self, y, x, s): 

1244 pos = self.w * y + x 

1245 self.screen[pos:pos + len(s)] = s 

1246 

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)) 

1250 

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) 

1387 

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) 

1394 

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) 

1406 

1407 

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) 

1419 

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) 

1472 

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) 

1503 

1504 def cursor_set_x(self, x): 

1505 self.cx = max(0, x) 

1506 

1507 def cursor_set_y(self, y): 

1508 self.cy = max(0, min(self.h - 1, y)) 

1509 

1510 def cursor_set(self, y, x): 

1511 self.cursor_set_x(x) 

1512 self.cursor_set_y(y) 

1513 

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) 

1519 

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) 

1614 

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): 

1658 

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 

1728 

1729 else: 

1730# 

1731# Move cursor sequence part 1 

1732# 

1733 if self.movecursor == 1: 

1734 self.movecursor=2 

1735 self.movecol=t 

1736 

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 

1769 

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)