Coverage for C:\leo.repo\leo-editor\leo\plugins\qt_text.py: 24%

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

1199 statements  

1# -*- coding: utf-8 -*- 

2#@+leo-ver=5-thin 

3#@+node:ekr.20140831085423.18598: * @file ../plugins/qt_text.py 

4#@@first 

5"""Text classes for the Qt version of Leo""" 

6import time 

7assert time 

8from leo.core import leoGlobals as g 

9from leo.core.leoQt import isQt6, QtCore, QtGui, Qsci, QtWidgets 

10from leo.core.leoQt import ContextMenuPolicy, Key, KeyboardModifier, Modifier 

11from leo.core.leoQt import MouseButton, MoveMode, MoveOperation 

12from leo.core.leoQt import Shadow, Shape, SliderAction, WindowType, WrapMode 

13 

14QColor = QtGui.QColor 

15FullWidthSelection = 0x06000 # works for both Qt5 and Qt6 

16 

17#@+others 

18#@+node:ekr.20191001084541.1: ** zoom commands 

19#@+node:tbrown.20130411145310.18857: *3* @g.command("zoom-in") 

20@g.command("zoom-in") 

21def zoom_in(event=None, delta=1): 

22 """increase body font size by one 

23 

24 @font-size-body must be present in the stylesheet 

25 """ 

26 zoom_helper(event, delta=1) 

27#@+node:ekr.20191001084646.1: *3* @g.command("zoom-out") 

28@g.command("zoom-out") 

29def zoom_out(event=None): 

30 """decrease body font size by one 

31 

32 @font-size-body must be present in the stylesheet 

33 """ 

34 # zoom_in(event=event, delta=-1) 

35 zoom_helper(event=event, delta=-1) 

36#@+node:ekr.20191001084612.1: *3* zoom_helper 

37def zoom_helper(event, delta): 

38 """ 

39 Common helper for zoom commands. 

40 """ 

41 c = event.get('c') 

42 if not c: 

43 return 

44 if not c.config.getBool('allow-text-zoom', default=True): 

45 if 'zoom' in g.app.debug: 

46 g.trace('text zoom disabled') 

47 return 

48 wrapper = c.frame.body.wrapper 

49 # 

50 # For performance, don't call c.styleSheetManager.reload_style_sheets(). 

51 # Apply to body widget directly 

52 c._style_deltas['font-size-body'] += delta 

53 ssm = c.styleSheetManager 

54 sheet = ssm.expand_css_constants(c.active_stylesheet) 

55 wrapper.widget.setStyleSheet(sheet) 

56 # 

57 # #490: Honor language-specific settings. 

58 colorizer = getattr(c.frame.body, 'colorizer', None) 

59 if not colorizer: 

60 return 

61 c.zoom_delta = delta 

62 colorizer.configure_fonts() 

63 wrapper.setAllText(wrapper.getAllText()) 

64 # Recolor everything. 

65#@+node:tom.20210904233317.1: ** Show Hilite Settings command 

66# Add item to known "help-for" commands 

67hilite_doc = r''' 

68Changing The Current Line Highlighting Color 

69-------------------------------------------- 

70 

71The highlight color will be computed based on the Leo theme in effect, unless the `line-highlight-color` setting is set to a non-blank string. 

72 

73The setting will always override the color computation. If the setting is changed, after the settings are reloaded the new color will take effect the next time the cursor is moved. 

74 

75Settings for Current Line Highlighting 

76--------------------------------------- 

77\@bool highlight-body-line -- if True, highlight current line. 

78 

79\@string line-highlight-color -- override highlight color with css value. 

80Valid values are standard css color names like `lightgrey`, and css rgb values like `#1234ad`. 

81''' 

82 

83@g.command('help-for-highlight-current-line') 

84def helpForLineHighlight(self, event=None): 

85 """Displays Settings used by current line highlighter.""" 

86 self.c.putHelpFor(hilite_doc) 

87 

88#@+node:ekr.20140901062324.18719: ** class QTextMixin 

89class QTextMixin: 

90 """A minimal mixin class for QTextEditWrapper and QScintillaWrapper classes.""" 

91 #@+others 

92 #@+node:ekr.20140901062324.18732: *3* qtm.ctor & helper 

93 def __init__(self, c=None): 

94 """Ctor for QTextMixin class""" 

95 self.c = c 

96 self.changingText = False # A lockout for onTextChanged. 

97 self.enabled = True 

98 # A flag for k.masterKeyHandler and isTextWrapper. 

99 self.supportsHighLevelInterface = True 

100 self.tags = {} 

101 self.permanent = True # False if selecting the minibuffer will make the widget go away. 

102 self.configDict = {} # Keys are tags, values are colors (names or values). 

103 self.configUnderlineDict = {} # Keys are tags, values are True 

104 # self.formatDict = {} # Keys are tags, values are actual QTextFormat objects. 

105 self.useScintilla = False # This is used! 

106 self.virtualInsertPoint = None 

107 if c: 

108 self.injectIvars(c) 

109 #@+node:ekr.20140901062324.18721: *4* qtm.injectIvars 

110 def injectIvars(self, name='1', parentFrame=None): 

111 """Inject standard leo ivars into the QTextEdit or QsciScintilla widget.""" 

112 w = self 

113 p = self.c.currentPosition() 

114 if name == '1': 

115 w.leo_p = None # Will be set when the second editor is created. 

116 else: 

117 w.leo_p = p and p.copy() 

118 w.leo_active = True 

119 # New in Leo 4.4.4 final: inject the scrollbar items into the text widget. 

120 w.leo_bodyBar = None 

121 w.leo_bodyXBar = None 

122 w.leo_chapter = None 

123 w.leo_frame = None 

124 w.leo_name = name 

125 w.leo_label = None 

126 return w 

127 #@+node:ekr.20140901062324.18825: *3* qtm.getName 

128 def getName(self): 

129 return self.name # Essential. 

130 #@+node:ekr.20140901122110.18733: *3* qtm.Event handlers 

131 # These are independent of the kind of Qt widget. 

132 #@+node:ekr.20140901062324.18716: *4* qtm.onCursorPositionChanged 

133 def onCursorPositionChanged(self, event=None): 

134 

135 c = self.c 

136 name = c.widget_name(self) 

137 # Apparently, this does not cause problems 

138 # because it generates no events in the body pane. 

139 if not name.startswith('body'): 

140 return 

141 if hasattr(c.frame, 'statusLine'): 

142 c.frame.statusLine.update() 

143 #@+node:ekr.20140901062324.18714: *4* qtm.onTextChanged 

144 def onTextChanged(self): 

145 """ 

146 Update Leo after the body has been changed. 

147 

148 tree.tree_select_lockout is True during the entire selection process. 

149 """ 

150 # Important: usually w.changingText is True. 

151 # This method very seldom does anything. 

152 w = self 

153 c, p = self.c, self.c.p 

154 tree = c.frame.tree 

155 if w.changingText: 

156 return 

157 if tree.tree_select_lockout: 

158 g.trace('*** LOCKOUT', g.callers()) 

159 return 

160 if not p: 

161 return 

162 newInsert = w.getInsertPoint() 

163 newSel = w.getSelectionRange() 

164 newText = w.getAllText() # Converts to unicode. 

165 # Get the previous values from the VNode. 

166 oldText = p.b 

167 if oldText == newText: 

168 # This can happen as the result of undo. 

169 # g.error('*** unexpected non-change') 

170 return 

171 i, j = p.v.selectionStart, p.v.selectionLength 

172 oldSel = (i, i + j) 

173 c.undoer.doTyping(p, 'Typing', oldText, newText, 

174 oldSel=oldSel, oldYview=None, newInsert=newInsert, newSel=newSel) 

175 #@+node:ekr.20140901122110.18734: *3* qtm.Generic high-level interface 

176 # These call only wrapper methods. 

177 #@+node:ekr.20140902181058.18645: *4* qtm.Enable/disable 

178 def disable(self): 

179 self.enabled = False 

180 

181 def enable(self, enabled=True): 

182 self.enabled = enabled 

183 #@+node:ekr.20140902181058.18644: *4* qtm.Clipboard 

184 def clipboard_append(self, s): 

185 s1 = g.app.gui.getTextFromClipboard() 

186 g.app.gui.replaceClipboardWith(s1 + s) 

187 

188 def clipboard_clear(self): 

189 g.app.gui.replaceClipboardWith('') 

190 #@+node:ekr.20140901062324.18698: *4* qtm.setFocus 

191 def setFocus(self): 

192 """QTextMixin""" 

193 if 'focus' in g.app.debug: 

194 print('BaseQTextWrapper.setFocus', self.widget) 

195 # Call the base class 

196 assert isinstance(self.widget, ( 

197 QtWidgets.QTextBrowser, 

198 QtWidgets.QLineEdit, 

199 QtWidgets.QTextEdit, 

200 Qsci and Qsci.QsciScintilla, 

201 )), self.widget 

202 QtWidgets.QTextBrowser.setFocus(self.widget) 

203 #@+node:ekr.20140901062324.18717: *4* qtm.Generic text 

204 #@+node:ekr.20140901062324.18703: *5* qtm.appendText 

205 def appendText(self, s): 

206 """QTextMixin""" 

207 s2 = self.getAllText() 

208 self.setAllText(s2 + s) 

209 self.setInsertPoint(len(s2)) 

210 #@+node:ekr.20140901141402.18706: *5* qtm.delete 

211 def delete(self, i, j=None): 

212 """QTextMixin""" 

213 i = self.toPythonIndex(i) 

214 if j is None: 

215 j = i + 1 

216 j = self.toPythonIndex(j) 

217 # This allows subclasses to use this base class method. 

218 if i > j: 

219 i, j = j, i 

220 s = self.getAllText() 

221 self.setAllText(s[:i] + s[j:]) 

222 # Bug fix: Significant in external tests. 

223 self.setSelectionRange(i, i, insert=i) 

224 #@+node:ekr.20140901062324.18827: *5* qtm.deleteTextSelection 

225 def deleteTextSelection(self): 

226 """QTextMixin""" 

227 i, j = self.getSelectionRange() 

228 self.delete(i, j) 

229 #@+node:ekr.20110605121601.18102: *5* qtm.get 

230 def get(self, i, j=None): 

231 """QTextMixin""" 

232 # 2012/04/12: fix the following two bugs by using the vanilla code: 

233 # https://bugs.launchpad.net/leo-editor/+bug/979142 

234 # https://bugs.launchpad.net/leo-editor/+bug/971166 

235 s = self.getAllText() 

236 i = self.toPythonIndex(i) 

237 j = self.toPythonIndex(j) 

238 return s[i:j] 

239 #@+node:ekr.20140901062324.18704: *5* qtm.getLastPosition & getLength 

240 def getLastPosition(self, s=None): 

241 """QTextMixin""" 

242 return len(self.getAllText()) if s is None else len(s) 

243 

244 def getLength(self, s=None): 

245 """QTextMixin""" 

246 return len(self.getAllText()) if s is None else len(s) 

247 #@+node:ekr.20140901062324.18705: *5* qtm.getSelectedText 

248 def getSelectedText(self): 

249 """QTextMixin""" 

250 i, j = self.getSelectionRange() 

251 if i == j: 

252 return '' 

253 s = self.getAllText() 

254 return s[i:j] 

255 #@+node:ekr.20140901141402.18702: *5* qtm.insert 

256 def insert(self, i, s): 

257 """QTextMixin""" 

258 s2 = self.getAllText() 

259 i = self.toPythonIndex(i) 

260 self.setAllText(s2[:i] + s + s2[i:]) 

261 self.setInsertPoint(i + len(s)) 

262 return i 

263 #@+node:ekr.20140902084950.18634: *5* qtm.seeInsertPoint 

264 def seeInsertPoint(self): 

265 """Ensure the insert point is visible.""" 

266 self.see(self.getInsertPoint()) 

267 # getInsertPoint defined in client classes. 

268 #@+node:ekr.20140902135648.18668: *5* qtm.selectAllText 

269 def selectAllText(self, s=None): 

270 """QTextMixin.""" 

271 self.setSelectionRange(0, self.getLength(s)) 

272 #@+node:ekr.20140901141402.18710: *5* qtm.toPythonIndex 

273 def toPythonIndex(self, index, s=None): 

274 """QTextMixin""" 

275 if s is None: 

276 s = self.getAllText() 

277 i = g.toPythonIndex(s, index) 

278 return i 

279 #@+node:ekr.20140901141402.18704: *5* qtm.toPythonIndexRowCol 

280 def toPythonIndexRowCol(self, index): 

281 """QTextMixin""" 

282 s = self.getAllText() 

283 i = self.toPythonIndex(index) 

284 row, col = g.convertPythonIndexToRowCol(s, i) 

285 return i, row, col 

286 #@+node:ekr.20140901062324.18729: *4* qtm.rememberSelectionAndScroll 

287 def rememberSelectionAndScroll(self): 

288 

289 w = self 

290 v = self.c.p.v # Always accurate. 

291 v.insertSpot = w.getInsertPoint() 

292 i, j = w.getSelectionRange() 

293 if i > j: 

294 i, j = j, i 

295 assert i <= j 

296 v.selectionStart = i 

297 v.selectionLength = j - i 

298 v.scrollBarSpot = w.getYScrollPosition() 

299 #@+node:ekr.20140901062324.18712: *4* qtm.tag_configure 

300 def tag_configure(self, *args, **keys): 

301 

302 if len(args) == 1: 

303 key = args[0] 

304 self.tags[key] = keys 

305 val = keys.get('foreground') 

306 underline = keys.get('underline') 

307 if val: 

308 self.configDict[key] = val 

309 if underline: 

310 self.configUnderlineDict[key] = True 

311 else: 

312 g.trace('oops', args, keys) 

313 

314 tag_config = tag_configure 

315 #@-others 

316#@+node:ekr.20110605121601.18058: ** class QLineEditWrapper(QTextMixin) 

317class QLineEditWrapper(QTextMixin): 

318 """ 

319 A class to wrap QLineEdit widgets. 

320 

321 The QHeadlineWrapper class is a subclass that merely 

322 redefines the do-nothing check method here. 

323 """ 

324 #@+others 

325 #@+node:ekr.20110605121601.18060: *3* qlew.Birth 

326 def __init__(self, widget, name, c=None): 

327 """Ctor for QLineEditWrapper class.""" 

328 super().__init__(c) 

329 self.widget = widget 

330 self.name = name 

331 self.baseClassName = 'QLineEditWrapper' 

332 

333 def __repr__(self): 

334 return f"<QLineEditWrapper: widget: {self.widget}" 

335 

336 __str__ = __repr__ 

337 #@+node:ekr.20140901191541.18599: *3* qlew.check 

338 def check(self): 

339 """ 

340 QLineEditWrapper. 

341 """ 

342 return True 

343 #@+node:ekr.20110605121601.18118: *3* qlew.Widget-specific overrides 

344 #@+node:ekr.20110605121601.18120: *4* qlew.getAllText 

345 def getAllText(self): 

346 """QHeadlineWrapper.""" 

347 if self.check(): 

348 w = self.widget 

349 return w.text() 

350 return '' 

351 #@+node:ekr.20110605121601.18121: *4* qlew.getInsertPoint 

352 def getInsertPoint(self): 

353 """QHeadlineWrapper.""" 

354 if self.check(): 

355 return self.widget.cursorPosition() 

356 return 0 

357 #@+node:ekr.20110605121601.18122: *4* qlew.getSelectionRange 

358 def getSelectionRange(self, sort=True): 

359 """QHeadlineWrapper.""" 

360 w = self.widget 

361 if self.check(): 

362 if w.hasSelectedText(): 

363 i = w.selectionStart() 

364 s = w.selectedText() 

365 j = i + len(s) 

366 else: 

367 i = j = w.cursorPosition() 

368 return i, j 

369 return 0, 0 

370 #@+node:ekr.20210104122029.1: *4* qlew.getYScrollPosition 

371 def getYScrollPosition(self): 

372 return 0 # #1801. 

373 #@+node:ekr.20110605121601.18123: *4* qlew.hasSelection 

374 def hasSelection(self): 

375 """QHeadlineWrapper.""" 

376 if self.check(): 

377 return self.widget.hasSelectedText() 

378 return False 

379 #@+node:ekr.20110605121601.18124: *4* qlew.see & seeInsertPoint 

380 def see(self, i): 

381 """QHeadlineWrapper.""" 

382 pass 

383 

384 def seeInsertPoint(self): 

385 """QHeadlineWrapper.""" 

386 pass 

387 #@+node:ekr.20110605121601.18125: *4* qlew.setAllText 

388 def setAllText(self, s): 

389 """Set all text of a Qt headline widget.""" 

390 if self.check(): 

391 w = self.widget 

392 w.setText(s) 

393 #@+node:ekr.20110605121601.18128: *4* qlew.setFocus 

394 def setFocus(self): 

395 """QHeadlineWrapper.""" 

396 if self.check(): 

397 g.app.gui.set_focus(self.c, self.widget) 

398 #@+node:ekr.20110605121601.18129: *4* qlew.setInsertPoint 

399 def setInsertPoint(self, i, s=None): 

400 """QHeadlineWrapper.""" 

401 if not self.check(): 

402 return 

403 w = self.widget 

404 if s is None: 

405 s = w.text() 

406 i = self.toPythonIndex(i) 

407 i = max(0, min(i, len(s))) 

408 w.setCursorPosition(i) 

409 #@+node:ekr.20110605121601.18130: *4* qlew.setSelectionRange 

410 def setSelectionRange(self, i, j, insert=None, s=None): 

411 """QHeadlineWrapper.""" 

412 if not self.check(): 

413 return 

414 w = self.widget 

415 if i > j: 

416 i, j = j, i 

417 if s is None: 

418 s = w.text() 

419 n = len(s) 

420 i = self.toPythonIndex(i) 

421 j = self.toPythonIndex(j) 

422 i = max(0, min(i, n)) 

423 j = max(0, min(j, n)) 

424 if insert is None: 

425 insert = j 

426 else: 

427 insert = self.toPythonIndex(insert) 

428 insert = max(0, min(insert, n)) 

429 if i == j: 

430 w.setCursorPosition(i) 

431 else: 

432 length = j - i 

433 # Set selection is a QLineEditMethod 

434 if insert < j: 

435 w.setSelection(j, -length) 

436 else: 

437 w.setSelection(i, length) 

438 # setSelectionRangeHelper = setSelectionRange 

439 #@-others 

440#@+node:ekr.20150403094619.1: ** class LeoLineTextWidget(QFrame) 

441class LeoLineTextWidget(QtWidgets.QFrame): # type:ignore 

442 """ 

443 A QFrame supporting gutter line numbers. 

444 

445 This class *has* a QTextEdit. 

446 """ 

447 #@+others 

448 #@+node:ekr.20150403094706.9: *3* __init__(LeoLineTextWidget) 

449 def __init__(self, c, e, *args): 

450 """Ctor for LineTextWidget.""" 

451 super().__init__(*args) 

452 self.c = c 

453 Raised = Shadow.Raised if isQt6 else self.StyledPanel 

454 NoFrame = Shape.NoFrame if isQt6 else self.NoFrame 

455 self.setFrameStyle(Raised) 

456 self.edit = e # A QTextEdit 

457 e.setFrameStyle(NoFrame) 

458 # e.setAcceptRichText(False) 

459 self.number_bar = NumberBar(c, e) 

460 hbox = QtWidgets.QHBoxLayout(self) 

461 hbox.setSpacing(0) 

462 hbox.setContentsMargins(0, 0, 0, 0) 

463 hbox.addWidget(self.number_bar) 

464 hbox.addWidget(e) 

465 e.installEventFilter(self) 

466 e.viewport().installEventFilter(self) 

467 #@+node:ekr.20150403094706.10: *3* eventFilter 

468 def eventFilter(self, obj, event): 

469 """ 

470 Update the line numbers for all events on the text edit and the viewport. 

471 This is easier than connecting all necessary signals. 

472 """ 

473 if obj in (self.edit, self.edit.viewport()): 

474 self.number_bar.update() 

475 return False 

476 return QtWidgets.QFrame.eventFilter(obj, event) 

477 #@-others 

478#@+node:ekr.20110605121601.18005: ** class LeoQTextBrowser (QtWidgets.QTextBrowser) 

479if QtWidgets: 

480 

481 

482 class LeoQTextBrowser(QtWidgets.QTextBrowser): # type:ignore 

483 """A subclass of QTextBrowser that overrides the mouse event handlers.""" 

484 #@+others 

485 #@+node:ekr.20110605121601.18006: *3* lqtb.ctor 

486 def __init__(self, parent, c, wrapper): 

487 """ctor for LeoQTextBrowser class.""" 

488 for attr in ('leo_c', 'leo_wrapper',): 

489 assert not hasattr(QtWidgets.QTextBrowser, attr), attr 

490 self.leo_c = c 

491 self.leo_s = '' # The cached text. 

492 self.leo_wrapper = wrapper 

493 self.htmlFlag = True 

494 super().__init__(parent) 

495 self.setCursorWidth(c.config.getInt('qt-cursor-width') or 1) 

496 

497 # Connect event handlers... 

498 if 0: # Not a good idea: it will complicate delayed loading of body text. 

499 # #1286 

500 self.textChanged.connect(self.onTextChanged) 

501 self.cursorPositionChanged.connect(self.highlightCurrentLine) 

502 self.textChanged.connect(self.highlightCurrentLine) 

503 self.setContextMenuPolicy(ContextMenuPolicy.CustomContextMenu) 

504 self.customContextMenuRequested.connect(self.onContextMenu) 

505 # This event handler is the easy way to keep track of the vertical scroll position. 

506 self.leo_vsb = vsb = self.verticalScrollBar() 

507 vsb.valueChanged.connect(self.onSliderChanged) 

508 # For QCompleter 

509 self.leo_q_completer = None 

510 self.leo_options = None 

511 self.leo_model = None 

512 

513 hl_color_setting = c.config.getString('line-highlight-color') or '' 

514 hl_color = QColor(hl_color_setting) 

515 self.hiliter_params = { 

516 'lastblock': -2, 'last_style_hash': 0, 

517 'last_color_setting': hl_color_setting, 

518 'last_fg': '', 'last_bg': '', 

519 'last_hl_color': hl_color 

520 } 

521 #@+node:ekr.20110605121601.18007: *3* lqtb. __repr__ & __str__ 

522 def __repr__(self): 

523 return f"(LeoQTextBrowser) {id(self)}" 

524 

525 __str__ = __repr__ 

526 #@+node:ekr.20110605121601.18008: *3* lqtb.Auto completion 

527 #@+node:ekr.20110605121601.18009: *4* class LeoQListWidget(QListWidget) 

528 class LeoQListWidget(QtWidgets.QListWidget): # type:ignore 

529 #@+others 

530 #@+node:ekr.20110605121601.18010: *5* lqlw.ctor 

531 def __init__(self, c): 

532 """ctor for LeoQListWidget class""" 

533 super().__init__() 

534 self.setWindowFlags(WindowType.Popup | self.windowFlags()) 

535 # Inject the ivars 

536 # A LeoQTextBrowser, a subclass of QtWidgets.QTextBrowser. 

537 self.leo_w = c.frame.body.wrapper.widget 

538 self.leo_c = c 

539 # A weird hack. 

540 self.leo_geom_set = False # When true, self.geom returns global coords! 

541 self.itemClicked.connect(self.select_callback) 

542 #@+node:ekr.20110605121601.18011: *5* lqlw.closeEvent 

543 def closeEvent(self, event): 

544 """Kill completion and close the window.""" 

545 self.leo_c.k.autoCompleter.abort() 

546 #@+node:ekr.20110605121601.18012: *5* lqlw.end_completer 

547 def end_completer(self): 

548 """End completion.""" 

549 c = self.leo_c 

550 c.in_qt_dialog = False 

551 # This is important: it clears the autocompletion state. 

552 c.k.keyboardQuit() 

553 c.bodyWantsFocusNow() 

554 try: 

555 self.deleteLater() 

556 except RuntimeError: 

557 # Avoid bug 1338773: Autocompleter error 

558 pass 

559 #@+node:ekr.20141024170936.7: *5* lqlw.get_selection 

560 def get_selection(self): 

561 """Return the presently selected item's text.""" 

562 return self.currentItem().text() 

563 #@+node:ekr.20110605121601.18013: *5* lqlw.keyPressEvent 

564 def keyPressEvent(self, event): 

565 """Handle a key event from QListWidget.""" 

566 c = self.leo_c 

567 w = c.frame.body.wrapper 

568 key = event.key() 

569 if event.modifiers() != Modifier.NoModifier and not event.text(): 

570 # A modifier key on it's own. 

571 pass 

572 elif key in (Key.Key_Up, Key.Key_Down): 

573 QtWidgets.QListWidget.keyPressEvent(self, event) 

574 elif key == Key.Key_Tab: 

575 self.tab_callback() 

576 elif key in (Key.Key_Enter, Key.Key_Return): 

577 self.select_callback() 

578 else: 

579 # Pass all other keys to the autocompleter via the event filter. 

580 w.ev_filter.eventFilter(obj=self, event=event) 

581 #@+node:ekr.20110605121601.18014: *5* lqlw.select_callback 

582 def select_callback(self): 

583 """ 

584 Called when user selects an item in the QListWidget. 

585 """ 

586 c = self.leo_c 

587 p = c.p 

588 w = c.k.autoCompleter.w or c.frame.body.wrapper 

589 oldSel = w.getSelectionRange() 

590 oldText = w.getAllText() 

591 # Replace the tail of the prefix with the completion. 

592 completion = self.currentItem().text() 

593 prefix = c.k.autoCompleter.get_autocompleter_prefix() 

594 parts = prefix.split('.') 

595 if len(parts) > 1: 

596 tail = parts[-1] 

597 else: 

598 tail = prefix 

599 if tail != completion: 

600 j = w.getInsertPoint() 

601 i = j - len(tail) 

602 w.delete(i, j) 

603 w.insert(i, completion) 

604 j = i + len(completion) 

605 c.setChanged() 

606 w.setInsertPoint(j) 

607 c.undoer.doTyping(p, 'Typing', oldText, 

608 newText=w.getAllText(), 

609 newInsert=w.getInsertPoint(), 

610 newSel=w.getSelectionRange(), 

611 oldSel=oldSel, 

612 ) 

613 self.end_completer() 

614 #@+node:tbrown.20111011094944.27031: *5* lqlw.tab_callback 

615 def tab_callback(self): 

616 """Called when user hits tab on an item in the QListWidget.""" 

617 c = self.leo_c 

618 w = c.k.autoCompleter.w or c.frame.body.wrapper # 2014/09/19 

619 if w is None: 

620 return 

621 # Replace the tail of the prefix with the completion. 

622 prefix = c.k.autoCompleter.get_autocompleter_prefix() 

623 parts = prefix.split('.') 

624 if len(parts) < 2: 

625 return 

626 i = j = w.getInsertPoint() 

627 s = w.getAllText() 

628 while (0 <= i < len(s) and s[i] != '.'): 

629 i -= 1 

630 i += 1 

631 if j > i: 

632 w.delete(i, j) 

633 w.setInsertPoint(i) 

634 c.k.autoCompleter.compute_completion_list() 

635 #@+node:ekr.20110605121601.18015: *5* lqlw.set_position 

636 def set_position(self, c): 

637 """Set the position of the QListWidget.""" 

638 

639 def glob(obj, pt): 

640 """Convert pt from obj's local coordinates to global coordinates.""" 

641 return obj.mapToGlobal(pt) 

642 

643 w = self.leo_w 

644 vp = self.viewport() 

645 r = w.cursorRect() 

646 geom = self.geometry() # In viewport coordinates. 

647 gr_topLeft = glob(w, r.topLeft()) 

648 # As a workaround to the Qt setGeometry bug, 

649 # The window is destroyed instead of being hidden. 

650 if self.leo_geom_set: 

651 g.trace('Error: leo_geom_set') 

652 return 

653 # This code illustrates the Qt bug... 

654 # if self.leo_geom_set: 

655 # # Unbelievable: geom is now in *global* coords. 

656 # gg_topLeft = geom.topLeft() 

657 # else: 

658 # # Per documentation, geom in local (viewport) coords. 

659 # gg_topLeft = glob(vp,geom.topLeft()) 

660 gg_topLeft = glob(vp, geom.topLeft()) 

661 delta_x = gr_topLeft.x() - gg_topLeft.x() 

662 delta_y = gr_topLeft.y() - gg_topLeft.y() 

663 # These offset are reasonable. Perhaps they should depend on font size. 

664 x_offset, y_offset = 10, 60 

665 # Compute the new geometry, setting the size by hand. 

666 geom2_topLeft = QtCore.QPoint( 

667 geom.x() + delta_x + x_offset, 

668 geom.y() + delta_y + y_offset) 

669 geom2_size = QtCore.QSize(400, 100) 

670 geom2 = QtCore.QRect(geom2_topLeft, geom2_size) 

671 # These tests fail once offsets are added. 

672 if x_offset == 0 and y_offset == 0: 

673 if self.leo_geom_set: 

674 if geom2.topLeft() != glob(w, r.topLeft()): 

675 g.trace( 

676 f"Error: geom.topLeft: {geom2.topLeft()}, " 

677 f"geom2.topLeft: {glob(w, r.topLeft())}") 

678 else: 

679 if glob(vp, geom2.topLeft()) != glob(w, r.topLeft()): 

680 g.trace( 

681 f"Error 2: geom.topLeft: {glob(vp, geom2.topLeft())}, " 

682 f"geom2.topLeft: {glob(w, r.topLeft())}") 

683 self.setGeometry(geom2) 

684 self.leo_geom_set = True 

685 #@+node:ekr.20110605121601.18016: *5* lqlw.show_completions 

686 def show_completions(self, aList): 

687 """Set the QListView contents to aList.""" 

688 self.clear() 

689 self.addItems(aList) 

690 self.setCurrentRow(0) 

691 self.activateWindow() 

692 self.setFocus() 

693 #@-others 

694 #@+node:ekr.20110605121601.18017: *4* lqtb.lqtb.init_completer 

695 def init_completer(self, options): 

696 """Connect a QCompleter.""" 

697 c = self.leo_c 

698 self.leo_qc = qc = self.LeoQListWidget(c) 

699 # Move the window near the body pane's cursor. 

700 qc.set_position(c) 

701 # Show the initial completions. 

702 c.in_qt_dialog = True 

703 qc.show() 

704 qc.activateWindow() 

705 c.widgetWantsFocusNow(qc) 

706 qc.show_completions(options) 

707 return qc 

708 #@+node:ekr.20110605121601.18018: *4* lqtb.redirections to LeoQListWidget 

709 def end_completer(self): 

710 if hasattr(self, 'leo_qc'): 

711 self.leo_qc.end_completer() 

712 delattr(self, 'leo_qc') 

713 

714 def show_completions(self, aList): 

715 if hasattr(self, 'leo_qc'): 

716 self.leo_qc.show_completions(aList) 

717 #@+node:tom.20210827230127.1: *3* lqtb Highlight Current Line 

718 #@+node:tom.20210827225119.3: *4* lqtb.parse_css 

719 #@@language python 

720 @staticmethod 

721 def parse_css(css_string, clas=''): 

722 """Extract colors from a css stylesheet string. 

723 

724 This is an extremely simple-minded function. It assumes 

725 that no quotation marks are being used, and that the 

726 first block in braces with the name clas is the controlling 

727 css for our widget. 

728 

729 Returns a tuple of strings (color, background). 

730 """ 

731 # Get first block with name matching "clas' 

732 block = css_string.split(clas, 1) 

733 block = block[1].split('{', 1) 

734 block = block[1].split('}', 1) 

735 

736 # Split into styles separated by ";" 

737 styles = block[0].split(';') 

738 

739 # Split into fields separated by ":" 

740 fields = [style.split(':') for style in styles if style.strip()] 

741 

742 # Only get fields whose names are "color" and "background" 

743 color = bg = '' 

744 for style, val in fields: 

745 style = style.strip() 

746 if style == 'color': 

747 color = val.strip() 

748 elif style == 'background': 

749 bg = val.strip() 

750 if color and bg: 

751 break 

752 return color, bg 

753 

754 #@+node:tom.20210827225119.4: *4* lqtb.assign_bg 

755 #@@language python 

756 @staticmethod 

757 def assign_bg(fg): 

758 """If fg or bg colors are missing, assign 

759 reasonable values. Can happen with incorrectly 

760 constructed themes, or no-theme color schemes. 

761 

762 Intended to be called when bg color is missing. 

763 

764 RETURNS 

765 a QColor object for the background color 

766 """ 

767 if not fg: 

768 fg = 'black' # QTextEdit default 

769 bg = 'white' # QTextEdit default 

770 if fg == 'black': 

771 bg = 'white' # QTextEdit default 

772 else: 

773 fg_color = QColor(fg) 

774 h, s, v, a = fg_color.getHsv() 

775 if v < 128: # dark foreground 

776 bg = 'white' 

777 else: 

778 bg = 'black' 

779 return QColor(bg) 

780 #@+node:tom.20210827225119.5: *4* lqtb.calc_hl 

781 #@@language python 

782 @staticmethod 

783 def calc_hl(bg_color): 

784 """Return the line highlight color. 

785 

786 ARGUMENT 

787 bg_color -- a QColor object for the background color 

788 

789 RETURNS 

790 a QColor object for the highlight color 

791 """ 

792 h, s, v, a = bg_color.getHsv() 

793 

794 if v < 40: 

795 v = 60 

796 bg_color.setHsv(h, s, v, a) 

797 elif v > 240: 

798 v = 220 

799 bg_color.setHsv(h, s, v, a) 

800 elif v < 128: 

801 bg_color = bg_color.lighter(130) 

802 else: 

803 bg_color = bg_color.darker(130) 

804 

805 return bg_color 

806 #@+node:tom.20210827225119.2: *4* lqtb.highlightCurrentLine 

807 #@@language python 

808 def highlightCurrentLine(self): 

809 """Highlight cursor line.""" 

810 c = self.leo_c 

811 params = self.hiliter_params 

812 editor = c.frame.body.wrapper.widget 

813 

814 if not c.config.getBool('highlight-body-line', True): 

815 editor.setExtraSelections([]) 

816 return 

817 

818 curs = editor.textCursor() 

819 blocknum = curs.blockNumber() 

820 

821 # Some cursor movements don't change the line: ignore them 

822 # if blocknum == params['lastblock'] and blocknum > 0: 

823 # return 

824 

825 if blocknum == 0: # invalid position 

826 blocknum = 1 

827 params['lastblock'] = blocknum 

828 

829 hl_color = params['last_hl_color'] 

830 

831 #@+<< Recalculate Color >> 

832 #@+node:tom.20210909124441.1: *5* << Recalculate Color >> 

833 config_setting = c.config.getString('line-highlight-color') \ 

834 or '' 

835 config_setting = (config_setting.replace("'", '') 

836 .replace('"', '').lower() 

837 .replace('none', '')) 

838 

839 last_color_setting = params['last_color_setting'] 

840 config_setting_changed = config_setting != last_color_setting 

841 

842 if config_setting: 

843 if config_setting_changed: 

844 hl_color = QColor(config_setting) 

845 params['last_hl_color'] = hl_color 

846 params['last_color_setting'] = config_setting 

847 else: 

848 hl_color = params['last_hl_color'] 

849 else: 

850 # Get current colors from the body editor widget 

851 wrapper = c.frame.body.wrapper 

852 w = wrapper.widget 

853 pallete = w.viewport().palette() 

854 fg_hex = pallete.text().color().rgb() 

855 bg_hex = pallete.window().color().rgb() 

856 fg = f'#{fg_hex:x}' 

857 bg = f'#{bg_hex:x}' 

858 

859 if (params['last_fg'] != fg or params['last_bg'] != bg): 

860 bg_color = QColor(bg) if bg else self.assign_bg(fg) 

861 hl_color = self.calc_hl(bg_color) 

862 #g.trace(f'fg: {fg}, bg: {bg}, hl_color: {hl_color.name()}') 

863 params['last_hl_color'] = hl_color 

864 params['last_fg'] = fg 

865 params['last_bg'] = bg 

866 #@-<< Recalculate Color >> 

867 #@+<< Apply Highlight >> 

868 #@+node:tom.20210909124551.1: *5* << Apply Highlight >> 

869 # Based on code from 

870 # https://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html 

871 

872 selection = editor.ExtraSelection() 

873 selection.format.setBackground(hl_color) 

874 selection.format.setProperty(FullWidthSelection, True) 

875 selection.cursor = curs 

876 selection.cursor.clearSelection() 

877 

878 editor.setExtraSelections([selection]) 

879 #@-<< Apply Highlight >> 

880 #@+node:tom.20210905130804.1: *4* Add Help Menu Item 

881 # Add entry to Help menu 

882 new_entry = ('@item', 'help-for-&highlight-current-line', '') 

883 

884 if g.app.config: 

885 for item in g.app.config.menusList: 

886 if 'Help' in item[0]: 

887 for entry in item[1]: 

888 if entry[0].lower() == '@menu &open help topics': 

889 menu_items = entry[1] 

890 menu_items.append(new_entry) 

891 menu_items.sort() 

892 break 

893 #@+node:ekr.20141103061944.31: *3* lqtb.get/setXScrollPosition 

894 def getXScrollPosition(self): 

895 """Get the horizontal scrollbar position.""" 

896 w = self 

897 sb = w.horizontalScrollBar() 

898 pos = sb.sliderPosition() 

899 return pos 

900 

901 def setXScrollPosition(self, pos): 

902 """Set the position of the horizontal scrollbar.""" 

903 if pos is not None: 

904 w = self 

905 sb = w.horizontalScrollBar() 

906 sb.setSliderPosition(pos) 

907 #@+node:ekr.20111002125540.7021: *3* lqtb.get/setYScrollPosition 

908 def getYScrollPosition(self): 

909 """Get the vertical scrollbar position.""" 

910 w = self 

911 sb = w.verticalScrollBar() 

912 pos = sb.sliderPosition() 

913 return pos 

914 

915 def setYScrollPosition(self, pos): 

916 """Set the position of the vertical scrollbar.""" 

917 w = self 

918 if pos is None: 

919 pos = 0 

920 sb = w.verticalScrollBar() 

921 sb.setSliderPosition(pos) 

922 #@+node:ekr.20110605121601.18019: *3* lqtb.leo_dumpButton 

923 def leo_dumpButton(self, event, tag): 

924 button = event.button() 

925 table = ( 

926 (MouseButton.NoButton, 'no button'), 

927 (MouseButton.LeftButton, 'left-button'), 

928 (MouseButton.RightButton, 'right-button'), 

929 (MouseButton.MiddleButton, 'middle-button'), 

930 ) 

931 for val, s in table: 

932 if button == val: 

933 kind = s 

934 break 

935 else: 

936 kind = f"unknown: {repr(button)}" 

937 return kind 

938 #@+node:ekr.20200304130514.1: *3* lqtb.onContextMenu 

939 def onContextMenu(self, point): 

940 """LeoQTextBrowser: Callback for customContextMenuRequested events.""" 

941 # #1286. 

942 c, w = self.leo_c, self 

943 g.app.gui.onContextMenu(c, w, point) 

944 #@+node:ekr.20120925061642.13506: *3* lqtb.onSliderChanged 

945 def onSliderChanged(self, arg): 

946 """Handle a Qt onSliderChanged event.""" 

947 c = self.leo_c 

948 p = c.p 

949 # Careful: changing nodes changes the scrollbars. 

950 if hasattr(c.frame.tree, 'tree_select_lockout'): 

951 if c.frame.tree.tree_select_lockout: 

952 return 

953 # Only scrolling in the body pane should set v.scrollBarSpot. 

954 if not c.frame.body or self != c.frame.body.wrapper.widget: 

955 return 

956 if p: 

957 p.v.scrollBarSpot = arg 

958 #@+node:ekr.20201204172235.1: *3* lqtb.paintEvent 

959 leo_cursor_width = 0 

960 

961 leo_vim_mode = None 

962 

963 def paintEvent(self, event): 

964 """ 

965 LeoQTextBrowser.paintEvent. 

966 

967 New in Leo 6.4: Draw a box around the cursor in command mode. 

968 This is as close as possible to vim's look. 

969 """ 

970 c, vc, w = self.leo_c, self.leo_c.vimCommands, self 

971 # 

972 # First, call the base class paintEvent. 

973 QtWidgets.QTextBrowser.paintEvent(self, event) 

974 

975 def set_cursor_width(width): 

976 """Set the cursor width, but only if necessary.""" 

977 if self.leo_cursor_width != width: 

978 self.leo_cursor_width = width 

979 w.setCursorWidth(width) 

980 

981 # 

982 # Are we in vim mode? 

983 if self.leo_vim_mode is None: 

984 self.leo_vim_mode = c.config.getBool('vim-mode', default=False) 

985 # 

986 # Are we in command mode? 

987 if self.leo_vim_mode: 

988 in_command = vc and vc.state == 'normal' # vim mode. 

989 else: 

990 in_command = c.k.unboundKeyAction == 'command' # vim emulation. 

991 # 

992 # Draw the box only in command mode, when w is the body pane, with focus. 

993 if ( 

994 not in_command 

995 or w != c.frame.body.widget 

996 or w != g.app.gui.get_focus() 

997 ): 

998 set_cursor_width(c.config.getInt('qt-cursor-width') or 1) 

999 return 

1000 # 

1001 # Set the width of the cursor. 

1002 font = w.currentFont() 

1003 cursor_width = QtGui.QFontMetrics(font).averageCharWidth() 

1004 set_cursor_width(cursor_width) 

1005 # 

1006 # Draw a box around the cursor. 

1007 qp = QtGui.QPainter() 

1008 qp.begin(self.viewport()) 

1009 qp.drawRect(w.cursorRect()) 

1010 qp.end() 

1011 #@+node:tbrown.20130411145310.18855: *3* lqtb.wheelEvent 

1012 def wheelEvent(self, event): 

1013 """Handle a wheel event.""" 

1014 if KeyboardModifier.ControlModifier & event.modifiers(): 

1015 d = {'c': self.leo_c} 

1016 try: # Qt5 or later. 

1017 point = event.angleDelta() 

1018 delta = point.y() or point.x() 

1019 except AttributeError: 

1020 delta = event.delta() # Qt4. 

1021 if delta < 0: 

1022 zoom_out(d) 

1023 else: 

1024 zoom_in(d) 

1025 event.accept() 

1026 return 

1027 QtWidgets.QTextBrowser.wheelEvent(self, event) 

1028 #@-others 

1029#@+node:ekr.20150403094706.2: ** class NumberBar(QFrame) 

1030class NumberBar(QtWidgets.QFrame): # type:ignore 

1031 #@+others 

1032 #@+node:ekr.20150403094706.3: *3* NumberBar.__init__ 

1033 def __init__(self, c, e, *args): 

1034 """Ctor for NumberBar class.""" 

1035 super().__init__(*args) 

1036 self.c = c 

1037 self.edit = e # A QTextEdit. 

1038 self.d = e.document() # A QTextDocument. 

1039 self.fm = self.fontMetrics() # A QFontMetrics 

1040 self.image = QtGui.QImage(g.app.gui.getImageImage( 

1041 g.os_path_finalize_join(g.app.loadDir, 

1042 '..', 'Icons', 'Tango', '16x16', 'actions', 'stop.png'))) 

1043 self.highest_line = 0 # The highest line that is currently visibile. 

1044 # Set the name to gutter so that the QFrame#gutter style sheet applies. 

1045 self.offsets = [] 

1046 self.setObjectName('gutter') 

1047 self.reloadSettings() 

1048 #@+node:ekr.20181005093003.1: *3* NumberBar.reloadSettings 

1049 def reloadSettings(self): 

1050 c = self.c 

1051 c.registerReloadSettings(self) 

1052 # Extra width for column. 

1053 self.w_adjust = c.config.getInt('gutter-w-adjust') or 12 

1054 # The y offset of the first line of the gutter. 

1055 self.y_adjust = c.config.getInt('gutter-y-adjust') or 10 

1056 #@+node:ekr.20181005085507.1: *3* NumberBar.mousePressEvent 

1057 def mousePressEvent(self, event): 

1058 

1059 c = self.c 

1060 

1061 def find_line(y): 

1062 n, last_y = 0, 0 

1063 for n, y2 in self.offsets: 

1064 if last_y <= y < y2: 

1065 return n 

1066 last_y = y2 

1067 return n if self.offsets else 0 

1068 

1069 xdb = getattr(g.app, 'xdb', None) 

1070 if not xdb: 

1071 return 

1072 path = xdb.canonic(g.fullPath(c, c.p)) 

1073 if not path: 

1074 return 

1075 n = find_line(event.y()) 

1076 if not xdb.checkline(path, n): 

1077 g.trace('FAIL checkline', path, n) 

1078 return 

1079 if xdb.has_breakpoint(path, n): 

1080 xdb.qc.put(f"clear {path}:{n}") 

1081 else: 

1082 xdb.qc.put(f"b {path}:{n}") 

1083 #@+node:ekr.20150403094706.5: *3* NumberBar.update 

1084 def update(self, *args): 

1085 """ 

1086 Updates the number bar to display the current set of numbers. 

1087 Also, adjusts the width of the number bar if necessary. 

1088 """ 

1089 # w_adjust is used to compensate for the current line being bold. 

1090 # Always allocate room for 2 columns 

1091 #width = self.fm.width(str(max(1000, self.highest_line))) + self.w_adjust 

1092 if isQt6: 

1093 width = self.fm.boundingRect(str(max(1000, self.highest_line))).width() 

1094 else: 

1095 width = self.fm.width(str(max(1000, self.highest_line))) + self.w_adjust 

1096 if self.width() != width: 

1097 self.setFixedWidth(width) 

1098 QtWidgets.QWidget.update(self, *args) 

1099 #@+node:ekr.20150403094706.6: *3* NumberBar.paintEvent 

1100 def paintEvent(self, event): 

1101 """ 

1102 Enhance QFrame.paintEvent. 

1103 Paint all visible text blocks in the editor's document. 

1104 """ 

1105 e = self.edit 

1106 d = self.d 

1107 layout = d.documentLayout() 

1108 # Compute constants. 

1109 current_block = d.findBlock(e.textCursor().position()) 

1110 scroll_y = e.verticalScrollBar().value() 

1111 page_bottom = scroll_y + e.viewport().height() 

1112 # Paint each visible block. 

1113 painter = QtGui.QPainter(self) 

1114 block = d.begin() 

1115 n = i = 0 

1116 c = self.c 

1117 translation = c.user_dict.get('line_number_translation', []) 

1118 self.offsets = [] 

1119 while block.isValid(): 

1120 i = translation[n] if n < len(translation) else n + 1 

1121 n += 1 

1122 top_left = layout.blockBoundingRect(block).topLeft() 

1123 if top_left.y() > page_bottom: 

1124 break # Outside the visible area. 

1125 bold = block == current_block 

1126 self.paintBlock(bold, i, painter, top_left, scroll_y) 

1127 block = block.next() 

1128 self.highest_line = i 

1129 painter.end() 

1130 QtWidgets.QWidget.paintEvent(self, event) 

1131 # Propagate the event. 

1132 #@+node:ekr.20150403094706.7: *3* NumberBar.paintBlock 

1133 def paintBlock(self, bold, n, painter, top_left, scroll_y): 

1134 """Paint n, right justified in the line number field.""" 

1135 c = self.c 

1136 if bold: 

1137 self.setBold(painter, True) 

1138 s = str(n) 

1139 pad = max(4, len(str(self.highest_line))) - len(s) 

1140 s = ' ' * pad + s 

1141 # x = self.width() - self.fm.width(s) - self.w_adjust 

1142 x = 0 

1143 y = round(top_left.y()) - scroll_y + self.fm.ascent() + self.y_adjust 

1144 self.offsets.append((n, y),) 

1145 painter.drawText(x, y, s) 

1146 if bold: 

1147 self.setBold(painter, False) 

1148 xdb = getattr(g.app, 'xdb', None) 

1149 if not xdb: 

1150 return 

1151 if not xdb.has_breakpoints(): 

1152 return 

1153 path = g.fullPath(c, c.p) 

1154 if xdb.has_breakpoint(path, n): 

1155 target_r = QtCore.QRect( 

1156 self.fm.width(s) + 16, 

1157 top_left.y() + self.y_adjust - 2, 

1158 16.0, 16.0) 

1159 if self.image: 

1160 source_r = QtCore.QRect(0.0, 0.0, 16.0, 16.0) 

1161 painter.drawImage(target_r, self.image, source_r) 

1162 else: 

1163 painter.drawEllipse(target_r) 

1164 #@+node:ekr.20150403094706.8: *3* NumberBar.setBold 

1165 def setBold(self, painter, flag): 

1166 """Set or clear bold facing in the painter, depending on flag.""" 

1167 font = painter.font() 

1168 font.setBold(flag) 

1169 painter.setFont(font) 

1170 #@-others 

1171#@+node:ekr.20140901141402.18700: ** class PlainTextWrapper(QTextMixin) 

1172class PlainTextWrapper(QTextMixin): 

1173 """A Qt class for use by the find code.""" 

1174 

1175 def __init__(self, widget): 

1176 """Ctor for the PlainTextWrapper class.""" 

1177 super().__init__() 

1178 self.widget = widget 

1179#@+node:ekr.20110605121601.18116: ** class QHeadlineWrapper (QLineEditWrapper) 

1180class QHeadlineWrapper(QLineEditWrapper): 

1181 """ 

1182 A wrapper class for QLineEdit widgets in QTreeWidget's. 

1183 This class just redefines the check method. 

1184 """ 

1185 #@+others 

1186 #@+node:ekr.20110605121601.18117: *3* qhw.Birth 

1187 def __init__(self, c, item, name, widget): 

1188 """The ctor for the QHeadlineWrapper class.""" 

1189 assert isinstance(widget, QtWidgets.QLineEdit), widget 

1190 super().__init__(widget, name, c) 

1191 # Set ivars. 

1192 self.c = c 

1193 self.item = item 

1194 self.name = name 

1195 self.permanent = False # Warn the minibuffer that we can go away. 

1196 self.widget = widget 

1197 # Set the signal. 

1198 g.app.gui.setFilter(c, self.widget, self, tag=name) 

1199 

1200 def __repr__(self): 

1201 return f"QHeadlineWrapper: {id(self)}" 

1202 #@+node:ekr.20110605121601.18119: *3* qhw.check 

1203 def check(self): 

1204 """Return True if the tree item exists and it's edit widget exists.""" 

1205 tree = self.c.frame.tree 

1206 try: 

1207 e = tree.treeWidget.itemWidget(self.item, 0) 

1208 except RuntimeError: 

1209 return False 

1210 valid = tree.isValidItem(self.item) 

1211 result = valid and e == self.widget 

1212 return result 

1213 #@-others 

1214#@+node:ekr.20110605121601.18131: ** class QMinibufferWrapper (QLineEditWrapper) 

1215class QMinibufferWrapper(QLineEditWrapper): 

1216 

1217 def __init__(self, c): 

1218 """Ctor for QMinibufferWrapper class.""" 

1219 self.c = c 

1220 w = c.frame.top.lineEdit # QLineEdit 

1221 super().__init__(widget=w, name='minibuffer', c=c) 

1222 assert self.widget 

1223 g.app.gui.setFilter(c, w, self, tag='minibuffer') 

1224 # Monkey-patch the event handlers 

1225 #@+<< define mouseReleaseEvent >> 

1226 #@+node:ekr.20110605121601.18132: *3* << define mouseReleaseEvent >> (QMinibufferWrapper) 

1227 def mouseReleaseEvent(event, self=self): 

1228 """Override QLineEdit.mouseReleaseEvent. 

1229 

1230 Simulate alt-x if we are not in an input state. 

1231 """ 

1232 assert isinstance(self, QMinibufferWrapper), self 

1233 assert isinstance(self.widget, QtWidgets.QLineEdit), self.widget 

1234 c, k = self.c, self.c.k 

1235 if not k.state.kind: 

1236 # c.widgetWantsFocusNow(w) # Doesn't work. 

1237 event2 = g.app.gui.create_key_event(c, w=c.frame.body.wrapper) 

1238 k.fullCommand(event2) 

1239 # c.outerUpdate() # Doesn't work. 

1240 

1241 #@-<< define mouseReleaseEvent >> 

1242 

1243 w.mouseReleaseEvent = mouseReleaseEvent 

1244 

1245 def setStyleClass(self, style_class): 

1246 self.widget.setProperty('style_class', style_class) 

1247 # 

1248 # to get the appearance to change because of a property 

1249 # change, unlike a focus or hover change, we need to 

1250 # re-apply the stylesheet. But re-applying at the top level 

1251 # is too CPU hungry, so apply just to this widget instead. 

1252 # It may lag a bit when the style's edited, but the new top 

1253 # level sheet will get pushed down quite frequently. 

1254 self.widget.setStyleSheet(self.c.frame.top.styleSheet()) 

1255 

1256 def setSelectionRange(self, i, j, insert=None, s=None): 

1257 QLineEditWrapper.setSelectionRange(self, i, j, insert, s) 

1258 insert = j if insert is None else insert 

1259 if self.widget: 

1260 self.widget._sel_and_insert = (i, j, insert) 

1261#@+node:ekr.20110605121601.18103: ** class QScintillaWrapper(QTextMixin) 

1262class QScintillaWrapper(QTextMixin): 

1263 """ 

1264 A wrapper for QsciScintilla supporting the high-level interface. 

1265 

1266 This widget will likely always be less capable the QTextEditWrapper. 

1267 To do: 

1268 - Fix all Scintilla unit-test failures. 

1269 - Add support for all scintilla lexers. 

1270 """ 

1271 #@+others 

1272 #@+node:ekr.20110605121601.18105: *3* qsciw.ctor 

1273 def __init__(self, widget, c, name=None): 

1274 """Ctor for the QScintillaWrapper class.""" 

1275 super().__init__(c) 

1276 self.baseClassName = 'QScintillaWrapper' 

1277 self.c = c 

1278 self.name = name 

1279 self.useScintilla = True 

1280 self.widget = widget 

1281 # Complete the init. 

1282 self.set_config() 

1283 # Set the signal. 

1284 g.app.gui.setFilter(c, widget, self, tag=name) 

1285 #@+node:ekr.20110605121601.18106: *3* qsciw.set_config 

1286 def set_config(self): 

1287 """Set QScintillaWrapper configuration options.""" 

1288 c, w = self.c, self.widget 

1289 n = c.config.getInt('qt-scintilla-zoom-in') 

1290 if n not in (None, 1, 0): 

1291 w.zoomIn(n) 

1292 w.setUtf8(True) # Important. 

1293 if 1: 

1294 w.setBraceMatching(2) # Sloppy 

1295 else: 

1296 w.setBraceMatching(0) # wrapper.flashCharacter creates big problems. 

1297 if 0: 

1298 w.setMarginWidth(1, 40) 

1299 w.setMarginLineNumbers(1, True) 

1300 w.setIndentationWidth(4) 

1301 w.setIndentationsUseTabs(False) 

1302 w.setAutoIndent(True) 

1303 #@+node:ekr.20110605121601.18107: *3* qsciw.WidgetAPI 

1304 #@+node:ekr.20140901062324.18593: *4* qsciw.delete 

1305 def delete(self, i, j=None): 

1306 """Delete s[i:j]""" 

1307 w = self.widget 

1308 i = self.toPythonIndex(i) 

1309 if j is None: 

1310 j = i + 1 

1311 j = self.toPythonIndex(j) 

1312 self.setSelectionRange(i, j) 

1313 try: 

1314 self.changingText = True # Disable onTextChanged 

1315 w.replaceSelectedText('') 

1316 finally: 

1317 self.changingText = False 

1318 #@+node:ekr.20140901062324.18594: *4* qsciw.flashCharacter (disabled) 

1319 def flashCharacter(self, i, bg='white', fg='red', flashes=2, delay=50): 

1320 """Flash the character at position i.""" 

1321 if 0: # This causes a lot of problems: Better to use Scintilla matching. 

1322 # This causes problems during unit tests: 

1323 # The selection point isn't restored in time. 

1324 if g.unitTesting: 

1325 return 

1326 #@+others 

1327 #@+node:ekr.20140902084950.18635: *5* after 

1328 def after(func, delay=delay): 

1329 """Run func after the given delay.""" 

1330 QtCore.QTimer.singleShot(delay, func) 

1331 #@+node:ekr.20140902084950.18636: *5* addFlashCallback 

1332 def addFlashCallback(self=self): 

1333 i = self.flashIndex 

1334 w = self.widget 

1335 self.setSelectionRange(i, i + 1) 

1336 if self.flashBg: 

1337 w.setSelectionBackgroundColor(QtGui.QColor(self.flashBg)) 

1338 if self.flashFg: 

1339 w.setSelectionForegroundColor(QtGui.QColor(self.flashFg)) 

1340 self.flashCount -= 1 

1341 after(removeFlashCallback) 

1342 #@+node:ekr.20140902084950.18637: *5* removeFlashCallback 

1343 def removeFlashCallback(self=self): 

1344 """Remove the extra selections.""" 

1345 self.setInsertPoint(self.flashIndex) 

1346 w = self.widget 

1347 if self.flashCount > 0: 

1348 after(addFlashCallback) 

1349 else: 

1350 w.resetSelectionBackgroundColor() 

1351 self.setInsertPoint(self.flashIndex1) 

1352 w.setFocus() 

1353 #@-others 

1354 # Numbered color names don't work in Ubuntu 8.10, so... 

1355 if bg and bg[-1].isdigit() and bg[0] != '#': 

1356 bg = bg[:-1] 

1357 if fg and fg[-1].isdigit() and fg[0] != '#': 

1358 fg = fg[:-1] 

1359 # w = self.widget # A QsciScintilla widget. 

1360 self.flashCount = flashes 

1361 self.flashIndex1 = self.getInsertPoint() 

1362 self.flashIndex = self.toPythonIndex(i) 

1363 self.flashBg = None if bg.lower() == 'same' else bg 

1364 self.flashFg = None if fg.lower() == 'same' else fg 

1365 addFlashCallback() 

1366 #@+node:ekr.20140901062324.18595: *4* qsciw.get 

1367 def get(self, i, j=None): 

1368 # Fix the following two bugs by using vanilla code: 

1369 # https://bugs.launchpad.net/leo-editor/+bug/979142 

1370 # https://bugs.launchpad.net/leo-editor/+bug/971166 

1371 s = self.getAllText() 

1372 i = self.toPythonIndex(i) 

1373 j = self.toPythonIndex(j) 

1374 return s[i:j] 

1375 #@+node:ekr.20110605121601.18108: *4* qsciw.getAllText 

1376 def getAllText(self): 

1377 """Get all text from a QsciScintilla widget.""" 

1378 w = self.widget 

1379 return w.text() 

1380 #@+node:ekr.20110605121601.18109: *4* qsciw.getInsertPoint 

1381 def getInsertPoint(self): 

1382 """Get the insertion point from a QsciScintilla widget.""" 

1383 w = self.widget 

1384 i = int(w.SendScintilla(w.SCI_GETCURRENTPOS)) 

1385 return i 

1386 #@+node:ekr.20110605121601.18110: *4* qsciw.getSelectionRange 

1387 def getSelectionRange(self, sort=True): 

1388 """Get the selection range from a QsciScintilla widget.""" 

1389 w = self.widget 

1390 i = int(w.SendScintilla(w.SCI_GETCURRENTPOS)) 

1391 j = int(w.SendScintilla(w.SCI_GETANCHOR)) 

1392 if sort and i > j: 

1393 i, j = j, i 

1394 return i, j 

1395 #@+node:ekr.20140901062324.18599: *4* qsciw.getX/YScrollPosition (to do) 

1396 def getXScrollPosition(self): 

1397 # w = self.widget 

1398 return 0 # Not ready yet. 

1399 

1400 def getYScrollPosition(self): 

1401 # w = self.widget 

1402 return 0 # Not ready yet. 

1403 #@+node:ekr.20110605121601.18111: *4* qsciw.hasSelection 

1404 def hasSelection(self): 

1405 """Return True if a QsciScintilla widget has a selection range.""" 

1406 return self.widget.hasSelectedText() 

1407 #@+node:ekr.20140901062324.18601: *4* qsciw.insert 

1408 def insert(self, i, s): 

1409 """Insert s at position i.""" 

1410 w = self.widget 

1411 i = self.toPythonIndex(i) 

1412 w.SendScintilla(w.SCI_SETSEL, i, i) 

1413 w.SendScintilla(w.SCI_ADDTEXT, len(s), g.toEncodedString(s)) 

1414 i += len(s) 

1415 w.SendScintilla(w.SCI_SETSEL, i, i) 

1416 return i 

1417 #@+node:ekr.20140901062324.18603: *4* qsciw.linesPerPage 

1418 def linesPerPage(self): 

1419 """Return the number of lines presently visible.""" 

1420 # Not used in Leo's core. Not tested. 

1421 w = self.widget 

1422 return int(w.SendScintilla(w.SCI_LINESONSCREEN)) 

1423 #@+node:ekr.20140901062324.18604: *4* qsciw.scrollDelegate (maybe) 

1424 if 0: # Not yet. 

1425 

1426 def scrollDelegate(self, kind): 

1427 """ 

1428 Scroll a QTextEdit up or down one page. 

1429 direction is in ('down-line','down-page','up-line','up-page') 

1430 """ 

1431 c = self.c 

1432 w = self.widget 

1433 vScroll = w.verticalScrollBar() 

1434 h = w.size().height() 

1435 lineSpacing = w.fontMetrics().lineSpacing() 

1436 n = h / lineSpacing 

1437 n = max(2, n - 3) 

1438 if kind == 'down-half-page': 

1439 delta = n / 2 

1440 elif kind == 'down-line': 

1441 delta = 1 

1442 elif kind == 'down-page': 

1443 delta = n 

1444 elif kind == 'up-half-page': 

1445 delta = -n / 2 

1446 elif kind == 'up-line': 

1447 delta = -1 

1448 elif kind == 'up-page': 

1449 delta = -n 

1450 else: 

1451 delta = 0 

1452 g.trace('bad kind:', kind) 

1453 val = vScroll.value() 

1454 vScroll.setValue(val + (delta * lineSpacing)) 

1455 c.bodyWantsFocus() 

1456 #@+node:ekr.20110605121601.18112: *4* qsciw.see 

1457 def see(self, i): 

1458 """Ensure insert point i is visible in a QsciScintilla widget.""" 

1459 # Ok for now. Using SCI_SETYCARETPOLICY might be better. 

1460 w = self.widget 

1461 s = self.getAllText() 

1462 i = self.toPythonIndex(i) 

1463 row, col = g.convertPythonIndexToRowCol(s, i) 

1464 w.ensureLineVisible(row) 

1465 #@+node:ekr.20110605121601.18113: *4* qsciw.setAllText 

1466 def setAllText(self, s): 

1467 """Set the text of a QScintilla widget.""" 

1468 w = self.widget 

1469 assert isinstance(w, Qsci.QsciScintilla), w 

1470 w.setText(s) 

1471 # w.update() 

1472 #@+node:ekr.20110605121601.18114: *4* qsciw.setInsertPoint 

1473 def setInsertPoint(self, i, s=None): 

1474 """Set the insertion point in a QsciScintilla widget.""" 

1475 w = self.widget 

1476 i = self.toPythonIndex(i) 

1477 # w.SendScintilla(w.SCI_SETCURRENTPOS,i) 

1478 # w.SendScintilla(w.SCI_SETANCHOR,i) 

1479 w.SendScintilla(w.SCI_SETSEL, i, i) 

1480 #@+node:ekr.20110605121601.18115: *4* qsciw.setSelectionRange 

1481 def setSelectionRange(self, i, j, insert=None, s=None): 

1482 """Set the selection range in a QsciScintilla widget.""" 

1483 w = self.widget 

1484 i = self.toPythonIndex(i) 

1485 j = self.toPythonIndex(j) 

1486 insert = j if insert is None else self.toPythonIndex(insert) 

1487 if insert >= i: 

1488 w.SendScintilla(w.SCI_SETSEL, i, j) 

1489 else: 

1490 w.SendScintilla(w.SCI_SETSEL, j, i) 

1491 #@+node:ekr.20140901062324.18609: *4* qsciw.setX/YScrollPosition (to do) 

1492 def setXScrollPosition(self, pos): 

1493 """Set the position of the horizontal scrollbar.""" 

1494 

1495 def setYScrollPosition(self, pos): 

1496 """Set the position of the vertical scrollbar.""" 

1497 #@-others 

1498#@+node:ekr.20110605121601.18071: ** class QTextEditWrapper(QTextMixin) 

1499class QTextEditWrapper(QTextMixin): 

1500 """A wrapper for a QTextEdit/QTextBrowser supporting the high-level interface.""" 

1501 #@+others 

1502 #@+node:ekr.20110605121601.18073: *3* qtew.ctor & helpers 

1503 def __init__(self, widget, name, c=None): 

1504 """Ctor for QTextEditWrapper class. widget is a QTextEdit/QTextBrowser.""" 

1505 super().__init__(c) 

1506 # Make sure all ivars are set. 

1507 self.baseClassName = 'QTextEditWrapper' 

1508 self.c = c 

1509 self.name = name 

1510 self.widget = widget 

1511 self.useScintilla = False 

1512 # Complete the init. 

1513 if c and widget: 

1514 self.widget.setUndoRedoEnabled(False) 

1515 self.set_config() 

1516 self.set_signals() 

1517 

1518 #@+node:ekr.20110605121601.18076: *4* qtew.set_config 

1519 def set_config(self): 

1520 """Set configuration options for QTextEdit.""" 

1521 w = self.widget 

1522 w.setWordWrapMode(WrapMode.NoWrap) 

1523 # tab stop in pixels - no config for this (yet) 

1524 if isQt6: 

1525 w.setTabStopDistance(24) 

1526 else: 

1527 w.setTabStopWidth(24) 

1528 #@+node:ekr.20140901062324.18566: *4* qtew.set_signals (should be distributed?) 

1529 def set_signals(self): 

1530 """Set up signals.""" 

1531 c, name = self.c, self.name 

1532 if name in ('body', 'rendering-pane-wrapper') or name.startswith('head'): 

1533 # Hook up qt events. 

1534 g.app.gui.setFilter(c, self.widget, self, tag=name) 

1535 if name == 'body': 

1536 w = self.widget 

1537 w.textChanged.connect(self.onTextChanged) 

1538 w.cursorPositionChanged.connect(self.onCursorPositionChanged) 

1539 if name in ('body', 'log'): 

1540 # Monkey patch the event handler. 

1541 #@+others 

1542 #@+node:ekr.20140901062324.18565: *5* mouseReleaseEvent (monkey-patch) QTextEditWrapper 

1543 def mouseReleaseEvent(event, self=self): 

1544 """ 

1545 Monkey patch for self.widget (QTextEditWrapper) mouseReleaseEvent. 

1546 """ 

1547 assert isinstance(self, QTextEditWrapper), self 

1548 assert isinstance(self.widget, QtWidgets.QTextEdit), self.widget 

1549 QtWidgets.QTextEdit.mouseReleaseEvent(self.widget, event) 

1550 # Call the base class. 

1551 c = self.c 

1552 setattr(event, 'c', c) 

1553 # Open the url on a control-click. 

1554 if KeyboardModifier.ControlModifier & event.modifiers(): 

1555 g.openUrlOnClick(event) 

1556 else: 

1557 if name == 'body': 

1558 c.p.v.insertSpot = c.frame.body.wrapper.getInsertPoint() 

1559 g.doHook("bodyclick2", c=c, p=c.p, v=c.p) 

1560 # Do *not* change the focus! This would rip focus away from tab panes. 

1561 c.k.keyboardQuit(setFocus=False) 

1562 #@-others 

1563 self.widget.mouseReleaseEvent = mouseReleaseEvent 

1564 #@+node:ekr.20200312052821.1: *3* qtew.repr 

1565 def __repr__(self): 

1566 # Add a leading space to align with StringTextWrapper. 

1567 return f" <QTextEditWrapper: {id(self)} {self.name}>" 

1568 

1569 __str__ = __repr__ 

1570 #@+node:ekr.20110605121601.18078: *3* qtew.High-level interface 

1571 # These are all widget-dependent 

1572 #@+node:ekr.20110605121601.18079: *4* qtew.delete (avoid call to setAllText) 

1573 def delete(self, i, j=None): 

1574 """QTextEditWrapper.""" 

1575 w = self.widget 

1576 i = self.toPythonIndex(i) 

1577 if j is None: 

1578 j = i + 1 

1579 j = self.toPythonIndex(j) 

1580 if i > j: 

1581 i, j = j, i 

1582 sb = w.verticalScrollBar() 

1583 pos = sb.sliderPosition() 

1584 cursor = w.textCursor() 

1585 try: 

1586 self.changingText = True # Disable onTextChanged 

1587 old_i, old_j = self.getSelectionRange() 

1588 if i == old_i and j == old_j: 

1589 # Work around an apparent bug in cursor.movePosition. 

1590 cursor.removeSelectedText() 

1591 elif i == j: 

1592 pass 

1593 else: 

1594 cursor.setPosition(i) 

1595 moveCount = abs(j - i) 

1596 cursor.movePosition(MoveOperation.Right, MoveMode.KeepAnchor, moveCount) 

1597 w.setTextCursor(cursor) # Bug fix: 2010/01/27 

1598 cursor.removeSelectedText() 

1599 finally: 

1600 self.changingText = False 

1601 sb.setSliderPosition(pos) 

1602 #@+node:ekr.20110605121601.18080: *4* qtew.flashCharacter 

1603 def flashCharacter(self, i, bg='white', fg='red', flashes=3, delay=75): 

1604 """QTextEditWrapper.""" 

1605 # numbered color names don't work in Ubuntu 8.10, so... 

1606 if bg[-1].isdigit() and bg[0] != '#': 

1607 bg = bg[:-1] 

1608 if fg[-1].isdigit() and fg[0] != '#': 

1609 fg = fg[:-1] 

1610 # This might causes problems during unit tests. 

1611 # The selection point isn't restored in time. 

1612 if g.unitTesting: 

1613 return 

1614 w = self.widget # A QTextEdit. 

1615 # Remember highlighted line: 

1616 last_selections = w.extraSelections() 

1617 

1618 def after(func): 

1619 QtCore.QTimer.singleShot(delay, func) 

1620 

1621 def addFlashCallback(self=self, w=w): 

1622 i = self.flashIndex 

1623 cursor = w.textCursor() # Must be the widget's cursor. 

1624 cursor.setPosition(i) 

1625 cursor.movePosition(MoveOperation.Right, MoveMode.KeepAnchor, 1) 

1626 extra = w.ExtraSelection() 

1627 extra.cursor = cursor 

1628 if self.flashBg: 

1629 extra.format.setBackground(QtGui.QColor(self.flashBg)) 

1630 if self.flashFg: 

1631 extra.format.setForeground(QtGui.QColor(self.flashFg)) 

1632 self.extraSelList = last_selections[:] 

1633 self.extraSelList.append(extra) # must be last 

1634 w.setExtraSelections(self.extraSelList) 

1635 self.flashCount -= 1 

1636 after(removeFlashCallback) 

1637 

1638 def removeFlashCallback(self=self, w=w): 

1639 w.setExtraSelections(last_selections) 

1640 if self.flashCount > 0: 

1641 after(addFlashCallback) 

1642 else: 

1643 w.setFocus() 

1644 

1645 self.flashCount = flashes 

1646 self.flashIndex = i 

1647 self.flashBg = None if bg.lower() == 'same' else bg 

1648 self.flashFg = None if fg.lower() == 'same' else fg 

1649 addFlashCallback() 

1650 

1651 #@+node:ekr.20110605121601.18081: *4* qtew.getAllText 

1652 def getAllText(self): 

1653 """QTextEditWrapper.""" 

1654 w = self.widget 

1655 return w.toPlainText() 

1656 #@+node:ekr.20110605121601.18082: *4* qtew.getInsertPoint 

1657 def getInsertPoint(self): 

1658 """QTextEditWrapper.""" 

1659 return self.widget.textCursor().position() 

1660 #@+node:ekr.20110605121601.18083: *4* qtew.getSelectionRange 

1661 def getSelectionRange(self, sort=True): 

1662 """QTextEditWrapper.""" 

1663 w = self.widget 

1664 tc = w.textCursor() 

1665 i, j = tc.selectionStart(), tc.selectionEnd() 

1666 return i, j 

1667 #@+node:ekr.20110605121601.18084: *4* qtew.getX/YScrollPosition 

1668 # **Important**: There is a Qt bug here: the scrollbar position 

1669 # is valid only if cursor is visible. Otherwise the *reported* 

1670 # scrollbar position will be such that the cursor *is* visible. 

1671 

1672 def getXScrollPosition(self): 

1673 """QTextEditWrapper: Get the horizontal scrollbar position.""" 

1674 w = self.widget 

1675 sb = w.horizontalScrollBar() 

1676 pos = sb.sliderPosition() 

1677 return pos 

1678 

1679 def getYScrollPosition(self): 

1680 """QTextEditWrapper: Get the vertical scrollbar position.""" 

1681 w = self.widget 

1682 sb = w.verticalScrollBar() 

1683 pos = sb.sliderPosition() 

1684 return pos 

1685 #@+node:ekr.20110605121601.18085: *4* qtew.hasSelection 

1686 def hasSelection(self): 

1687 """QTextEditWrapper.""" 

1688 return self.widget.textCursor().hasSelection() 

1689 #@+node:ekr.20110605121601.18089: *4* qtew.insert (avoid call to setAllText) 

1690 def insert(self, i, s): 

1691 """QTextEditWrapper.""" 

1692 w = self.widget 

1693 i = self.toPythonIndex(i) 

1694 cursor = w.textCursor() 

1695 try: 

1696 self.changingText = True # Disable onTextChanged. 

1697 cursor.setPosition(i) 

1698 cursor.insertText(s) 

1699 w.setTextCursor(cursor) # Bug fix: 2010/01/27 

1700 finally: 

1701 self.changingText = False 

1702 #@+node:ekr.20110605121601.18077: *4* qtew.leoMoveCursorHelper & helper 

1703 def leoMoveCursorHelper(self, kind, extend=False, linesPerPage=15): 

1704 """QTextEditWrapper.""" 

1705 w = self.widget 

1706 d = { 

1707 'begin-line': MoveOperation.StartOfLine, # Was start-line 

1708 'down': MoveOperation.Down, 

1709 'end': MoveOperation.End, 

1710 'end-line': MoveOperation.EndOfLine, # Not used. 

1711 'exchange': True, # Dummy. 

1712 'home': MoveOperation.Start, 

1713 'left': MoveOperation.Left, 

1714 'page-down': MoveOperation.Down, 

1715 'page-up': MoveOperation.Up, 

1716 'right': MoveOperation.Right, 

1717 'up': MoveOperation.Up, 

1718 } 

1719 kind = kind.lower() 

1720 op = d.get(kind) 

1721 mode = MoveMode.KeepAnchor if extend else MoveMode.MoveAnchor 

1722 if not op: 

1723 g.trace(f"can not happen: bad kind: {kind}") 

1724 return 

1725 if kind in ('page-down', 'page-up'): 

1726 self.pageUpDown(op, mode) 

1727 elif kind == 'exchange': # exchange-point-and-mark 

1728 cursor = w.textCursor() 

1729 anchor = cursor.anchor() 

1730 pos = cursor.position() 

1731 cursor.setPosition(pos, MoveOperation.MoveAnchor) 

1732 cursor.setPosition(anchor, MoveOperation.KeepAnchor) 

1733 w.setTextCursor(cursor) 

1734 else: 

1735 if not extend: 

1736 # Fix an annoyance. Make sure to clear the selection. 

1737 cursor = w.textCursor() 

1738 cursor.clearSelection() 

1739 w.setTextCursor(cursor) 

1740 w.moveCursor(op, mode) 

1741 self.seeInsertPoint() 

1742 self.rememberSelectionAndScroll() 

1743 # #218. 

1744 cursor = w.textCursor() 

1745 sel = cursor.selection().toPlainText() 

1746 if sel and hasattr(g.app.gui, 'setClipboardSelection'): 

1747 g.app.gui.setClipboardSelection(sel) 

1748 self.c.frame.updateStatusLine() 

1749 #@+node:btheado.20120129145543.8180: *5* qtew.pageUpDown 

1750 def pageUpDown(self, op, moveMode): 

1751 """ 

1752 The QTextEdit PageUp/PageDown functionality seems to be "baked-in" 

1753 and not externally accessible. Since Leo has its own keyhandling 

1754 functionality, this code emulates the QTextEdit paging. This is a 

1755 straight port of the C++ code found in the pageUpDown method of 

1756 gui/widgets/qtextedit.cpp. 

1757 """ 

1758 control = self.widget 

1759 cursor = control.textCursor() 

1760 moved = False 

1761 lastY = control.cursorRect(cursor).top() 

1762 distance = 0 

1763 # move using movePosition to keep the cursor's x 

1764 while True: 

1765 y = control.cursorRect(cursor).top() 

1766 distance += abs(y - lastY) 

1767 lastY = y 

1768 moved = cursor.movePosition(op, moveMode) 

1769 if (not moved or distance >= control.height()): 

1770 break 

1771 sb = control.verticalScrollBar() 

1772 if moved: 

1773 if op == MoveOperation.Up: 

1774 cursor.movePosition(MoveOperation.Down, moveMode) 

1775 sb.triggerAction(SliderAction.SliderPageStepSub) 

1776 else: 

1777 cursor.movePosition(MoveOperation.Up, moveMode) 

1778 sb.triggerAction(SliderAction.SliderPageStepAdd) 

1779 control.setTextCursor(cursor) 

1780 #@+node:ekr.20110605121601.18087: *4* qtew.linesPerPage 

1781 def linesPerPage(self): 

1782 """QTextEditWrapper.""" 

1783 # Not used in Leo's core. 

1784 w = self.widget 

1785 h = w.size().height() 

1786 lineSpacing = w.fontMetrics().lineSpacing() 

1787 n = h / lineSpacing 

1788 return n 

1789 #@+node:ekr.20110605121601.18088: *4* qtew.scrollDelegate 

1790 def scrollDelegate(self, kind): 

1791 """ 

1792 Scroll a QTextEdit up or down one page. 

1793 direction is in ('down-line','down-page','up-line','up-page') 

1794 """ 

1795 c = self.c 

1796 w = self.widget 

1797 vScroll = w.verticalScrollBar() 

1798 h = w.size().height() 

1799 lineSpacing = w.fontMetrics().lineSpacing() 

1800 n = h / lineSpacing 

1801 n = max(2, n - 3) 

1802 if kind == 'down-half-page': 

1803 delta = n / 2 

1804 elif kind == 'down-line': 

1805 delta = 1 

1806 elif kind == 'down-page': 

1807 delta = n 

1808 elif kind == 'up-half-page': 

1809 delta = -n / 2 

1810 elif kind == 'up-line': 

1811 delta = -1 

1812 elif kind == 'up-page': 

1813 delta = -n 

1814 else: 

1815 delta = 0 

1816 g.trace('bad kind:', kind) 

1817 val = vScroll.value() 

1818 vScroll.setValue(val + (delta * lineSpacing)) 

1819 c.bodyWantsFocus() 

1820 #@+node:ekr.20110605121601.18090: *4* qtew.see & seeInsertPoint 

1821 def see(self, see_i): 

1822 """Scroll so that position see_i is visible.""" 

1823 w = self.widget 

1824 tc = w.textCursor() 

1825 # Put see_i in range. 

1826 s = self.getAllText() 

1827 see_i = max(0, min(see_i, len(s))) 

1828 # Remember the old cursor 

1829 old_cursor = QtGui.QTextCursor(tc) 

1830 # Scroll so that see_i is visible. 

1831 tc.setPosition(see_i) 

1832 w.setTextCursor(tc) 

1833 w.ensureCursorVisible() 

1834 # Restore the old cursor 

1835 w.setTextCursor(old_cursor) 

1836 

1837 def seeInsertPoint(self): 

1838 """Make sure the insert point is visible.""" 

1839 self.widget.ensureCursorVisible() 

1840 #@+node:ekr.20110605121601.18092: *4* qtew.setAllText 

1841 def setAllText(self, s): 

1842 """Set the text of body pane.""" 

1843 w = self.widget 

1844 try: 

1845 self.changingText = True # Disable onTextChanged. 

1846 w.setReadOnly(False) 

1847 w.setPlainText(s) 

1848 finally: 

1849 self.changingText = False 

1850 #@+node:ekr.20110605121601.18095: *4* qtew.setInsertPoint 

1851 def setInsertPoint(self, i, s=None): 

1852 # Fix bug 981849: incorrect body content shown. 

1853 # Use the more careful code in setSelectionRange. 

1854 self.setSelectionRange(i=i, j=i, insert=i, s=s) 

1855 #@+node:ekr.20110605121601.18096: *4* qtew.setSelectionRange 

1856 def setSelectionRange(self, i, j, insert=None, s=None): 

1857 """Set the selection range and the insert point.""" 

1858 # 

1859 # Part 1 

1860 w = self.widget 

1861 i = self.toPythonIndex(i) 

1862 j = self.toPythonIndex(j) 

1863 if s is None: 

1864 s = self.getAllText() 

1865 n = len(s) 

1866 i = max(0, min(i, n)) 

1867 j = max(0, min(j, n)) 

1868 if insert is None: 

1869 ins = max(i, j) 

1870 else: 

1871 ins = self.toPythonIndex(insert) 

1872 ins = max(0, min(ins, n)) 

1873 # 

1874 # Part 2: 

1875 # 2010/02/02: Use only tc.setPosition here. 

1876 # Using tc.movePosition doesn't work. 

1877 tc = w.textCursor() 

1878 if i == j: 

1879 tc.setPosition(i) 

1880 elif ins == j: 

1881 # Put the insert point at j 

1882 tc.setPosition(i) 

1883 tc.setPosition(j, MoveMode.KeepAnchor) 

1884 elif ins == i: 

1885 # Put the insert point at i 

1886 tc.setPosition(j) 

1887 tc.setPosition(i, MoveMode.KeepAnchor) 

1888 else: 

1889 # 2014/08/21: It doesn't seem possible to put the insert point somewhere else! 

1890 tc.setPosition(j) 

1891 tc.setPosition(i, MoveMode.KeepAnchor) 

1892 w.setTextCursor(tc) 

1893 # #218. 

1894 if hasattr(g.app.gui, 'setClipboardSelection'): 

1895 if s[i:j]: 

1896 g.app.gui.setClipboardSelection(s[i:j]) 

1897 # 

1898 # Remember the values for v.restoreCursorAndScroll. 

1899 v = self.c.p.v # Always accurate. 

1900 v.insertSpot = ins 

1901 if i > j: 

1902 i, j = j, i 

1903 assert i <= j 

1904 v.selectionStart = i 

1905 v.selectionLength = j - i 

1906 v.scrollBarSpot = w.verticalScrollBar().value() 

1907 #@+node:ekr.20141103061944.40: *4* qtew.setXScrollPosition 

1908 def setXScrollPosition(self, pos): 

1909 """Set the position of the horizonatl scrollbar.""" 

1910 if pos is not None: 

1911 w = self.widget 

1912 sb = w.horizontalScrollBar() 

1913 sb.setSliderPosition(pos) 

1914 #@+node:ekr.20110605121601.18098: *4* qtew.setYScrollPosition 

1915 def setYScrollPosition(self, pos): 

1916 """Set the vertical scrollbar position.""" 

1917 if pos is not None: 

1918 w = self.widget 

1919 sb = w.verticalScrollBar() 

1920 sb.setSliderPosition(pos) 

1921 #@+node:ekr.20110605121601.18100: *4* qtew.toPythonIndex 

1922 def toPythonIndex(self, index, s=None): 

1923 """This is much faster than versions using g.toPythonIndex.""" 

1924 w = self 

1925 te = self.widget 

1926 if index is None: 

1927 return 0 

1928 if isinstance(index, int): 

1929 return index 

1930 if index == '1.0': 

1931 return 0 

1932 if index == 'end': 

1933 return w.getLastPosition() 

1934 doc = te.document() 

1935 data = index.split('.') 

1936 if len(data) == 2: 

1937 row, col = data 

1938 row, col = int(row), int(col) 

1939 bl = doc.findBlockByNumber(row - 1) 

1940 return bl.position() + col 

1941 g.trace(f"bad string index: {index}") 

1942 return 0 

1943 #@+node:ekr.20110605121601.18101: *4* qtew.toPythonIndexRowCol 

1944 def toPythonIndexRowCol(self, index): 

1945 w = self 

1946 if index == '1.0': 

1947 return 0, 0, 0 

1948 if index == 'end': 

1949 index = w.getLastPosition() 

1950 te = self.widget 

1951 doc = te.document() 

1952 i = w.toPythonIndex(index) 

1953 bl = doc.findBlock(i) 

1954 row = bl.blockNumber() 

1955 col = i - bl.position() 

1956 return i, row, col 

1957 #@-others 

1958#@-others 

1959 

1960#@@language python 

1961#@@tabwidth -4 

1962#@@pagewidth 70 

1963#@-leo