Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 self.supportsHighLevelInterface = True 

99 # A flag for k.masterKeyHandler and isTextWrapper. 

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_hl_color': hl_color 

519 } 

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

521 def __repr__(self): 

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

523 

524 __str__ = __repr__ 

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

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

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

528 #@+others 

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

530 def __init__(self, c): 

531 """ctor for LeoQListWidget class""" 

532 super().__init__() 

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

534 # Inject the ivars 

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

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

537 self.leo_c = c 

538 # A weird hack. 

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

540 self.itemClicked.connect(self.select_callback) 

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

542 def closeEvent(self, event): 

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

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

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

546 def end_completer(self): 

547 """End completion.""" 

548 c = self.leo_c 

549 c.in_qt_dialog = False 

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

551 c.k.keyboardQuit() 

552 c.bodyWantsFocusNow() 

553 try: 

554 self.deleteLater() 

555 except RuntimeError: 

556 # Avoid bug 1338773: Autocompleter error 

557 pass 

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

559 def get_selection(self): 

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

561 return self.currentItem().text() 

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

563 def keyPressEvent(self, event): 

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

565 c = self.leo_c 

566 w = c.frame.body.wrapper 

567 key = event.key() 

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

569 # A modifier key on it's own. 

570 pass 

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

572 QtWidgets.QListWidget.keyPressEvent(self, event) 

573 elif key == Key.Key_Tab: 

574 self.tab_callback() 

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

576 self.select_callback() 

577 else: 

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

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

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

581 def select_callback(self): 

582 """ 

583 Called when user selects an item in the QListWidget. 

584 """ 

585 c = self.leo_c 

586 p = c.p 

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

588 oldSel = w.getSelectionRange() 

589 oldText = w.getAllText() 

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

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

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

593 parts = prefix.split('.') 

594 if len(parts) > 1: 

595 tail = parts[-1] 

596 else: 

597 tail = prefix 

598 if tail != completion: 

599 j = w.getInsertPoint() 

600 i = j - len(tail) 

601 w.delete(i, j) 

602 w.insert(i, completion) 

603 j = i + len(completion) 

604 c.setChanged() 

605 w.setInsertPoint(j) 

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

607 newText=w.getAllText(), 

608 newInsert=w.getInsertPoint(), 

609 newSel=w.getSelectionRange(), 

610 oldSel=oldSel, 

611 ) 

612 self.end_completer() 

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

614 def tab_callback(self): 

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

616 c = self.leo_c 

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

618 if w is None: 

619 return 

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

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

622 parts = prefix.split('.') 

623 if len(parts) < 2: 

624 return 

625 i = j = w.getInsertPoint() 

626 s = w.getAllText() 

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

628 i -= 1 

629 i += 1 

630 if j > i: 

631 w.delete(i, j) 

632 w.setInsertPoint(i) 

633 c.k.autoCompleter.compute_completion_list() 

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

635 def set_position(self, c): 

636 """Set the position of the QListWidget.""" 

637 

638 def glob(obj, pt): 

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

640 return obj.mapToGlobal(pt) 

641 

642 w = self.leo_w 

643 vp = self.viewport() 

644 r = w.cursorRect() 

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

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

647 # As a workaround to the Qt setGeometry bug, 

648 # The window is destroyed instead of being hidden. 

649 if self.leo_geom_set: 

650 g.trace('Error: leo_geom_set') 

651 return 

652 # This code illustrates the Qt bug... 

653 # if self.leo_geom_set: 

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

655 # gg_topLeft = geom.topLeft() 

656 # else: 

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

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

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

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

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

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

663 x_offset, y_offset = 10, 60 

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

665 geom2_topLeft = QtCore.QPoint( 

666 geom.x() + delta_x + x_offset, 

667 geom.y() + delta_y + y_offset) 

668 geom2_size = QtCore.QSize(400, 100) 

669 geom2 = QtCore.QRect(geom2_topLeft, geom2_size) 

670 # These tests fail once offsets are added. 

671 if x_offset == 0 and y_offset == 0: 

672 if self.leo_geom_set: 

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

674 g.trace( 

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

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

677 else: 

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

679 g.trace( 

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

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

682 self.setGeometry(geom2) 

683 self.leo_geom_set = True 

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

685 def show_completions(self, aList): 

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

687 self.clear() 

688 self.addItems(aList) 

689 self.setCurrentRow(0) 

690 self.activateWindow() 

691 self.setFocus() 

692 #@-others 

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

694 def init_completer(self, options): 

695 """Connect a QCompleter.""" 

696 c = self.leo_c 

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

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

699 qc.set_position(c) 

700 # Show the initial completions. 

701 c.in_qt_dialog = True 

702 qc.show() 

703 qc.activateWindow() 

704 c.widgetWantsFocusNow(qc) 

705 qc.show_completions(options) 

706 return qc 

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

708 def end_completer(self): 

709 if hasattr(self, 'leo_qc'): 

710 self.leo_qc.end_completer() 

711 delattr(self, 'leo_qc') 

712 

713 def show_completions(self, aList): 

714 if hasattr(self, 'leo_qc'): 

715 self.leo_qc.show_completions(aList) 

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

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

718 #@@language python 

719 @staticmethod 

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

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

722 

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

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

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

726 css for our widget. 

727 

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

729 """ 

730 # Get first block with name matching "clas' 

731 block = css_string.split(clas, 1) 

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

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

734 

735 # Split into styles separated by ";" 

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

737 

738 # Split into fields separated by ":" 

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

740 

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

742 color = bg = '' 

743 for style, val in fields: 

744 style = style.strip() 

745 if style == 'color': 

746 color = val.strip() 

747 elif style == 'background': 

748 bg = val.strip() 

749 if color and bg: 

750 break 

751 return color, bg 

752 

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

754 #@@language python 

755 @staticmethod 

756 def assign_bg(fg): 

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

758 reasonable values. Can happen with incorrectly 

759 constructed themes, or no-theme color schemes. 

760 

761 Intended to be called when bg color is missing. 

762 

763 RETURNS 

764 a QColor object for the background color 

765 """ 

766 if not fg: 

767 fg = 'black' # QTextEdit default 

768 bg = 'white' # QTextEdit default 

769 if fg == 'black': 

770 bg = 'white' # QTextEdit default 

771 else: 

772 fg_color = QColor(fg) 

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

774 if v < 128: # dark foreground 

775 bg = 'white' 

776 else: 

777 bg = 'black' 

778 return QColor(bg) 

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

780 #@@language python 

781 @staticmethod 

782 def calc_hl(bg_color): 

783 """Return the line highlight color. 

784 

785 ARGUMENT 

786 bg_color -- a QColor object for the background color 

787 

788 RETURNS 

789 a QColor object for the highlight color 

790 """ 

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

792 

793 if v < 40: 

794 v = 60 

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

796 elif v > 240: 

797 v = 220 

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

799 elif v < 128: 

800 bg_color = bg_color.lighter(130) 

801 else: 

802 bg_color = bg_color.darker(130) 

803 

804 return bg_color 

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

806 #@@language python 

807 def highlightCurrentLine(self): 

808 """Highlight cursor line.""" 

809 c = self.leo_c 

810 params = self.hiliter_params 

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

812 

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

814 editor.setExtraSelections([]) 

815 return 

816 

817 curs = editor.textCursor() 

818 blocknum = curs.blockNumber() 

819 

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

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

822 # return 

823 

824 if blocknum == 0: # invalid position 

825 blocknum = 1 

826 params['lastblock'] = blocknum 

827 

828 hl_color = params['last_hl_color'] 

829 

830 #@+<< Recalculate Color >> 

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

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

833 or '' 

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

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

836 .replace('none', '')) 

837 

838 last_color_setting = params['last_color_setting'] 

839 config_setting_changed = config_setting != last_color_setting 

840 

841 if config_setting: 

842 if config_setting_changed: 

843 hl_color = QColor(config_setting) 

844 params['last_hl_color'] = hl_color 

845 params['last_color_setting'] = config_setting 

846 else: 

847 hl_color = params['last_hl_color'] 

848 else: 

849 ssm = c.styleSheetManager 

850 sheet = ssm.expand_css_constants(c.active_stylesheet) 

851 h = hash(sheet) 

852 params['last_color_setting'] = '' 

853 if params['last_style_hash'] != h or config_setting_changed: 

854 fg, bg = self.parse_css(sheet, 'QTextEdit') 

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

856 hl_color = self.calc_hl(bg_color) 

857 # g.trace('fg', fg, 'bg', bg, 'hl_color', hl_color.name()) 

858 params['last_hl_color'] = hl_color 

859 params['last_style_hash'] = h 

860 #@-<< Recalculate Color >> 

861 #@+<< Apply Highlight >> 

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

863 # Based on code from 

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

865 

866 selection = editor.ExtraSelection() 

867 selection.format.setBackground(hl_color) 

868 selection.format.setProperty(FullWidthSelection, True) 

869 selection.cursor = curs 

870 selection.cursor.clearSelection() 

871 

872 editor.setExtraSelections([selection]) 

873 #@-<< Apply Highlight >> 

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

875 # Add entry to Help menu 

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

877 

878 if g.app.config: 

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

880 if 'Help' in item[0]: 

881 for entry in item[1]: 

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

883 menu_items = entry[1] 

884 menu_items.append(new_entry) 

885 menu_items.sort() 

886 break 

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

888 def getXScrollPosition(self): 

889 """Get the horizontal scrollbar position.""" 

890 w = self 

891 sb = w.horizontalScrollBar() 

892 pos = sb.sliderPosition() 

893 return pos 

894 

895 def setXScrollPosition(self, pos): 

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

897 if pos is not None: 

898 w = self 

899 sb = w.horizontalScrollBar() 

900 sb.setSliderPosition(pos) 

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

902 def getYScrollPosition(self): 

903 """Get the vertical scrollbar position.""" 

904 w = self 

905 sb = w.verticalScrollBar() 

906 pos = sb.sliderPosition() 

907 return pos 

908 

909 def setYScrollPosition(self, pos): 

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

911 w = self 

912 if pos is None: 

913 pos = 0 

914 sb = w.verticalScrollBar() 

915 sb.setSliderPosition(pos) 

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

917 def leo_dumpButton(self, event, tag): 

918 button = event.button() 

919 table = ( 

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

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

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

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

924 ) 

925 for val, s in table: 

926 if button == val: 

927 kind = s 

928 break 

929 else: 

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

931 return kind 

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

933 def onContextMenu(self, point): 

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

935 # #1286. 

936 c, w = self.leo_c, self 

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

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

939 def onSliderChanged(self, arg): 

940 """Handle a Qt onSliderChanged event.""" 

941 c = self.leo_c 

942 p = c.p 

943 # Careful: changing nodes changes the scrollbars. 

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

945 if c.frame.tree.tree_select_lockout: 

946 return 

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

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

949 return 

950 if p: 

951 p.v.scrollBarSpot = arg 

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

953 leo_cursor_width = 0 

954 

955 leo_vim_mode = None 

956 

957 def paintEvent(self, event): 

958 """ 

959 LeoQTextBrowser.paintEvent. 

960 

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

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

963 """ 

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

965 # 

966 # First, call the base class paintEvent. 

967 QtWidgets.QTextBrowser.paintEvent(self, event) 

968 

969 def set_cursor_width(width): 

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

971 if self.leo_cursor_width != width: 

972 self.leo_cursor_width = width 

973 w.setCursorWidth(width) 

974 

975 # 

976 # Are we in vim mode? 

977 if self.leo_vim_mode is None: 

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

979 # 

980 # Are we in command mode? 

981 if self.leo_vim_mode: 

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

983 else: 

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

985 # 

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

987 if ( 

988 not in_command 

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

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

991 ): 

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

993 return 

994 # 

995 # Set the width of the cursor. 

996 font = w.currentFont() 

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

998 set_cursor_width(cursor_width) 

999 # 

1000 # Draw a box around the cursor. 

1001 qp = QtGui.QPainter() 

1002 qp.begin(self.viewport()) 

1003 qp.drawRect(w.cursorRect()) 

1004 qp.end() 

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

1006 def wheelEvent(self, event): 

1007 """Handle a wheel event.""" 

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

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

1010 try: # Qt5 or later. 

1011 point = event.angleDelta() 

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

1013 except AttributeError: 

1014 delta = event.delta() # Qt4. 

1015 if delta < 0: 

1016 zoom_out(d) 

1017 else: 

1018 zoom_in(d) 

1019 event.accept() 

1020 return 

1021 QtWidgets.QTextBrowser.wheelEvent(self, event) 

1022 #@-others 

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

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

1025 #@+others 

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

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

1028 """Ctor for NumberBar class.""" 

1029 super().__init__(*args) 

1030 self.c = c 

1031 self.edit = e 

1032 # A QTextEdit. 

1033 self.d = e.document() 

1034 # A QTextDocument. 

1035 self.fm = self.fontMetrics() 

1036 # A QFontMetrics 

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

1038 g.os_path_finalize_join(g.app.loadDir, 

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

1040 self.highest_line = 0 

1041 # The highest line that is currently visibile. 

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

1043 self.offsets = [] 

1044 self.setObjectName('gutter') 

1045 self.reloadSettings() 

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

1047 def reloadSettings(self): 

1048 c = self.c 

1049 c.registerReloadSettings(self) 

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

1051 # Extra width for column. 

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

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

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

1055 def mousePressEvent(self, event): 

1056 

1057 c = self.c 

1058 

1059 def find_line(y): 

1060 n, last_y = 0, 0 

1061 for n, y2 in self.offsets: 

1062 if last_y <= y < y2: 

1063 return n 

1064 last_y = y2 

1065 return n if self.offsets else 0 

1066 

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

1068 if not xdb: 

1069 return 

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

1071 if not path: 

1072 return 

1073 n = find_line(event.y()) 

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

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

1076 return 

1077 if xdb.has_breakpoint(path, n): 

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

1079 else: 

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

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

1082 def update(self, *args): 

1083 """ 

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

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

1086 """ 

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

1088 # Always allocate room for 2 columns 

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

1090 if isQt6: 

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

1092 else: 

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

1094 if self.width() != width: 

1095 self.setFixedWidth(width) 

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

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

1098 def paintEvent(self, event): 

1099 """ 

1100 Enhance QFrame.paintEvent. 

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

1102 """ 

1103 e = self.edit 

1104 d = self.d 

1105 layout = d.documentLayout() 

1106 # Compute constants. 

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

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

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

1110 # Paint each visible block. 

1111 painter = QtGui.QPainter(self) 

1112 block = d.begin() 

1113 n = i = 0 

1114 c = self.c 

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

1116 self.offsets = [] 

1117 while block.isValid(): 

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

1119 n += 1 

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

1121 if top_left.y() > page_bottom: 

1122 break # Outside the visible area. 

1123 bold = block == current_block 

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

1125 block = block.next() 

1126 self.highest_line = i 

1127 painter.end() 

1128 QtWidgets.QWidget.paintEvent(self, event) 

1129 # Propagate the event. 

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

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

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

1133 c = self.c 

1134 if bold: 

1135 self.setBold(painter, True) 

1136 s = str(n) 

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

1138 s = ' ' * pad + s 

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

1140 x = 0 

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

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

1143 painter.drawText(x, y, s) 

1144 if bold: 

1145 self.setBold(painter, False) 

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

1147 if not xdb: 

1148 return 

1149 if not xdb.has_breakpoints(): 

1150 return 

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

1152 if xdb.has_breakpoint(path, n): 

1153 target_r = QtCore.QRect( 

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

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

1156 16.0, 16.0) 

1157 if self.image: 

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

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

1160 else: 

1161 painter.drawEllipse(target_r) 

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

1163 def setBold(self, painter, flag): 

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

1165 font = painter.font() 

1166 font.setBold(flag) 

1167 painter.setFont(font) 

1168 #@-others 

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

1170class PlainTextWrapper(QTextMixin): 

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

1172 

1173 def __init__(self, widget): 

1174 """Ctor for the PlainTextWrapper class.""" 

1175 super().__init__() 

1176 self.widget = widget 

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

1178class QHeadlineWrapper(QLineEditWrapper): 

1179 """ 

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

1181 This class just redefines the check method. 

1182 """ 

1183 #@+others 

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

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

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

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

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

1189 # Set ivars. 

1190 self.c = c 

1191 self.item = item 

1192 self.name = name 

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

1194 self.widget = widget 

1195 # Set the signal. 

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

1197 

1198 def __repr__(self): 

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

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

1201 def check(self): 

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

1203 tree = self.c.frame.tree 

1204 try: 

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

1206 except RuntimeError: 

1207 return False 

1208 valid = tree.isValidItem(self.item) 

1209 result = valid and e == self.widget 

1210 return result 

1211 #@-others 

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

1213class QMinibufferWrapper(QLineEditWrapper): 

1214 

1215 def __init__(self, c): 

1216 """Ctor for QMinibufferWrapper class.""" 

1217 self.c = c 

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

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

1220 assert self.widget 

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

1222 # Monkey-patch the event handlers 

1223 #@+<< define mouseReleaseEvent >> 

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

1225 def mouseReleaseEvent(event, self=self): 

1226 """Override QLineEdit.mouseReleaseEvent. 

1227 

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

1229 """ 

1230 assert isinstance(self, QMinibufferWrapper), self 

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

1232 c, k = self.c, self.c.k 

1233 if not k.state.kind: 

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

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

1236 k.fullCommand(event2) 

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

1238 

1239 #@-<< define mouseReleaseEvent >> 

1240 

1241 w.mouseReleaseEvent = mouseReleaseEvent 

1242 

1243 def setStyleClass(self, style_class): 

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

1245 # 

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

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

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

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

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

1251 # level sheet will get pushed down quite frequently. 

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

1253 

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

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

1256 insert = j if insert is None else insert 

1257 if self.widget: 

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

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

1260class QScintillaWrapper(QTextMixin): 

1261 """ 

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

1263 

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

1265 To do: 

1266 - Fix all Scintilla unit-test failures. 

1267 - Add support for all scintilla lexers. 

1268 """ 

1269 #@+others 

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

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

1272 """Ctor for the QScintillaWrapper class.""" 

1273 super().__init__(c) 

1274 self.baseClassName = 'QScintillaWrapper' 

1275 self.c = c 

1276 self.name = name 

1277 self.useScintilla = True 

1278 self.widget = widget 

1279 # Complete the init. 

1280 self.set_config() 

1281 # Set the signal. 

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

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

1284 def set_config(self): 

1285 """Set QScintillaWrapper configuration options.""" 

1286 c, w = self.c, self.widget 

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

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

1289 w.zoomIn(n) 

1290 w.setUtf8(True) # Important. 

1291 if 1: 

1292 w.setBraceMatching(2) # Sloppy 

1293 else: 

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

1295 if 0: 

1296 w.setMarginWidth(1, 40) 

1297 w.setMarginLineNumbers(1, True) 

1298 w.setIndentationWidth(4) 

1299 w.setIndentationsUseTabs(False) 

1300 w.setAutoIndent(True) 

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

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

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

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

1305 w = self.widget 

1306 i = self.toPythonIndex(i) 

1307 if j is None: 

1308 j = i + 1 

1309 j = self.toPythonIndex(j) 

1310 self.setSelectionRange(i, j) 

1311 try: 

1312 self.changingText = True # Disable onTextChanged 

1313 w.replaceSelectedText('') 

1314 finally: 

1315 self.changingText = False 

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

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

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

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

1320 # This causes problems during unit tests: 

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

1322 if g.unitTesting: 

1323 return 

1324 #@+others 

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

1326 def after(func, delay=delay): 

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

1328 QtCore.QTimer.singleShot(delay, func) 

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

1330 def addFlashCallback(self=self): 

1331 i = self.flashIndex 

1332 w = self.widget 

1333 self.setSelectionRange(i, i + 1) 

1334 if self.flashBg: 

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

1336 if self.flashFg: 

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

1338 self.flashCount -= 1 

1339 after(removeFlashCallback) 

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

1341 def removeFlashCallback(self=self): 

1342 """Remove the extra selections.""" 

1343 self.setInsertPoint(self.flashIndex) 

1344 w = self.widget 

1345 if self.flashCount > 0: 

1346 after(addFlashCallback) 

1347 else: 

1348 w.resetSelectionBackgroundColor() 

1349 self.setInsertPoint(self.flashIndex1) 

1350 w.setFocus() 

1351 #@-others 

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

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

1354 bg = bg[:-1] 

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

1356 fg = fg[:-1] 

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

1358 self.flashCount = flashes 

1359 self.flashIndex1 = self.getInsertPoint() 

1360 self.flashIndex = self.toPythonIndex(i) 

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

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

1363 addFlashCallback() 

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

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

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

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

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

1369 s = self.getAllText() 

1370 i = self.toPythonIndex(i) 

1371 j = self.toPythonIndex(j) 

1372 return s[i:j] 

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

1374 def getAllText(self): 

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

1376 w = self.widget 

1377 return w.text() 

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

1379 def getInsertPoint(self): 

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

1381 w = self.widget 

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

1383 return i 

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

1385 def getSelectionRange(self, sort=True): 

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

1387 w = self.widget 

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

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

1390 if sort and i > j: 

1391 i, j = j, i 

1392 return i, j 

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

1394 def getXScrollPosition(self): 

1395 # w = self.widget 

1396 return 0 # Not ready yet. 

1397 

1398 def getYScrollPosition(self): 

1399 # w = self.widget 

1400 return 0 # Not ready yet. 

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

1402 def hasSelection(self): 

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

1404 return self.widget.hasSelectedText() 

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

1406 def insert(self, i, s): 

1407 """Insert s at position i.""" 

1408 w = self.widget 

1409 i = self.toPythonIndex(i) 

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

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

1412 i += len(s) 

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

1414 return i 

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

1416 def linesPerPage(self): 

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

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

1419 w = self.widget 

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

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

1422 if 0: # Not yet. 

1423 

1424 def scrollDelegate(self, kind): 

1425 """ 

1426 Scroll a QTextEdit up or down one page. 

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

1428 """ 

1429 c = self.c 

1430 w = self.widget 

1431 vScroll = w.verticalScrollBar() 

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

1433 lineSpacing = w.fontMetrics().lineSpacing() 

1434 n = h / lineSpacing 

1435 n = max(2, n - 3) 

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

1437 delta = n / 2 

1438 elif kind == 'down-line': 

1439 delta = 1 

1440 elif kind == 'down-page': 

1441 delta = n 

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

1443 delta = -n / 2 

1444 elif kind == 'up-line': 

1445 delta = -1 

1446 elif kind == 'up-page': 

1447 delta = -n 

1448 else: 

1449 delta = 0 

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

1451 val = vScroll.value() 

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

1453 c.bodyWantsFocus() 

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

1455 def see(self, i): 

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

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

1458 w = self.widget 

1459 s = self.getAllText() 

1460 i = self.toPythonIndex(i) 

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

1462 w.ensureLineVisible(row) 

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

1464 def setAllText(self, s): 

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

1466 w = self.widget 

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

1468 w.setText(s) 

1469 # w.update() 

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

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

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

1473 w = self.widget 

1474 i = self.toPythonIndex(i) 

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

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

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

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

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

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

1481 w = self.widget 

1482 i = self.toPythonIndex(i) 

1483 j = self.toPythonIndex(j) 

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

1485 if insert >= i: 

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

1487 else: 

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

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

1490 def setXScrollPosition(self, pos): 

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

1492 

1493 def setYScrollPosition(self, pos): 

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

1495 #@-others 

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

1497class QTextEditWrapper(QTextMixin): 

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

1499 #@+others 

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

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

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

1503 super().__init__(c) 

1504 # Make sure all ivars are set. 

1505 self.baseClassName = 'QTextEditWrapper' 

1506 self.c = c 

1507 self.name = name 

1508 self.widget = widget 

1509 self.useScintilla = False 

1510 # Complete the init. 

1511 if c and widget: 

1512 self.widget.setUndoRedoEnabled(False) 

1513 self.set_config() 

1514 self.set_signals() 

1515 

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

1517 def set_config(self): 

1518 """Set configuration options for QTextEdit.""" 

1519 w = self.widget 

1520 w.setWordWrapMode(WrapMode.NoWrap) 

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

1522 if isQt6: 

1523 w.setTabStopDistance(24) 

1524 else: 

1525 w.setTabStopWidth(24) 

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

1527 def set_signals(self): 

1528 """Set up signals.""" 

1529 c, name = self.c, self.name 

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

1531 # Hook up qt events. 

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

1533 if name == 'body': 

1534 w = self.widget 

1535 w.textChanged.connect(self.onTextChanged) 

1536 w.cursorPositionChanged.connect(self.onCursorPositionChanged) 

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

1538 # Monkey patch the event handler. 

1539 #@+others 

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

1541 def mouseReleaseEvent(event, self=self): 

1542 """ 

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

1544 """ 

1545 assert isinstance(self, QTextEditWrapper), self 

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

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

1548 # Call the base class. 

1549 c = self.c 

1550 setattr(event, 'c', c) 

1551 # Open the url on a control-click. 

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

1553 g.openUrlOnClick(event) 

1554 else: 

1555 if name == 'body': 

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

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

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

1559 c.k.keyboardQuit(setFocus=False) 

1560 #@-others 

1561 self.widget.mouseReleaseEvent = mouseReleaseEvent 

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

1563 def __repr__(self): 

1564 # Add a leading space to align with StringTextWrapper. 

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

1566 

1567 __str__ = __repr__ 

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

1569 # These are all widget-dependent 

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

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

1572 """QTextEditWrapper.""" 

1573 w = self.widget 

1574 i = self.toPythonIndex(i) 

1575 if j is None: 

1576 j = i + 1 

1577 j = self.toPythonIndex(j) 

1578 if i > j: 

1579 i, j = j, i 

1580 sb = w.verticalScrollBar() 

1581 pos = sb.sliderPosition() 

1582 cursor = w.textCursor() 

1583 try: 

1584 self.changingText = True # Disable onTextChanged 

1585 old_i, old_j = self.getSelectionRange() 

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

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

1588 cursor.removeSelectedText() 

1589 elif i == j: 

1590 pass 

1591 else: 

1592 cursor.setPosition(i) 

1593 moveCount = abs(j - i) 

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

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

1596 cursor.removeSelectedText() 

1597 finally: 

1598 self.changingText = False 

1599 sb.setSliderPosition(pos) 

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

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

1602 """QTextEditWrapper.""" 

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

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

1605 bg = bg[:-1] 

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

1607 fg = fg[:-1] 

1608 # This might causes problems during unit tests. 

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

1610 if g.unitTesting: 

1611 return 

1612 w = self.widget # A QTextEdit. 

1613 # Remember highlighted line: 

1614 last_selections = w.extraSelections() 

1615 

1616 def after(func): 

1617 QtCore.QTimer.singleShot(delay, func) 

1618 

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

1620 i = self.flashIndex 

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

1622 cursor.setPosition(i) 

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

1624 extra = w.ExtraSelection() 

1625 extra.cursor = cursor 

1626 if self.flashBg: 

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

1628 if self.flashFg: 

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

1630 self.extraSelList = last_selections[:] 

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

1632 w.setExtraSelections(self.extraSelList) 

1633 self.flashCount -= 1 

1634 after(removeFlashCallback) 

1635 

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

1637 w.setExtraSelections(last_selections) 

1638 if self.flashCount > 0: 

1639 after(addFlashCallback) 

1640 else: 

1641 w.setFocus() 

1642 

1643 self.flashCount = flashes 

1644 self.flashIndex = i 

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

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

1647 addFlashCallback() 

1648 

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

1650 def getAllText(self): 

1651 """QTextEditWrapper.""" 

1652 w = self.widget 

1653 return w.toPlainText() 

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

1655 def getInsertPoint(self): 

1656 """QTextEditWrapper.""" 

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

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

1659 def getSelectionRange(self, sort=True): 

1660 """QTextEditWrapper.""" 

1661 w = self.widget 

1662 tc = w.textCursor() 

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

1664 return i, j 

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

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

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

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

1669 

1670 def getXScrollPosition(self): 

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

1672 w = self.widget 

1673 sb = w.horizontalScrollBar() 

1674 pos = sb.sliderPosition() 

1675 return pos 

1676 

1677 def getYScrollPosition(self): 

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

1679 w = self.widget 

1680 sb = w.verticalScrollBar() 

1681 pos = sb.sliderPosition() 

1682 return pos 

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

1684 def hasSelection(self): 

1685 """QTextEditWrapper.""" 

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

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

1688 def insert(self, i, s): 

1689 """QTextEditWrapper.""" 

1690 w = self.widget 

1691 i = self.toPythonIndex(i) 

1692 cursor = w.textCursor() 

1693 try: 

1694 self.changingText = True # Disable onTextChanged. 

1695 cursor.setPosition(i) 

1696 cursor.insertText(s) 

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

1698 finally: 

1699 self.changingText = False 

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

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

1702 """QTextEditWrapper.""" 

1703 w = self.widget 

1704 d = { 

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

1706 'down': MoveOperation.Down, 

1707 'end': MoveOperation.End, 

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

1709 'exchange': True, # Dummy. 

1710 'home': MoveOperation.Start, 

1711 'left': MoveOperation.Left, 

1712 'page-down': MoveOperation.Down, 

1713 'page-up': MoveOperation.Up, 

1714 'right': MoveOperation.Right, 

1715 'up': MoveOperation.Up, 

1716 } 

1717 kind = kind.lower() 

1718 op = d.get(kind) 

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

1720 if not op: 

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

1722 return 

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

1724 self.pageUpDown(op, mode) 

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

1726 cursor = w.textCursor() 

1727 anchor = cursor.anchor() 

1728 pos = cursor.position() 

1729 cursor.setPosition(pos, MoveOperation.MoveAnchor) 

1730 cursor.setPosition(anchor, MoveOperation.KeepAnchor) 

1731 w.setTextCursor(cursor) 

1732 else: 

1733 if not extend: 

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

1735 cursor = w.textCursor() 

1736 cursor.clearSelection() 

1737 w.setTextCursor(cursor) 

1738 w.moveCursor(op, mode) 

1739 self.seeInsertPoint() 

1740 self.rememberSelectionAndScroll() 

1741 # #218. 

1742 cursor = w.textCursor() 

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

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

1745 g.app.gui.setClipboardSelection(sel) 

1746 self.c.frame.updateStatusLine() 

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

1748 def pageUpDown(self, op, moveMode): 

1749 """ 

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

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

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

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

1754 gui/widgets/qtextedit.cpp. 

1755 """ 

1756 control = self.widget 

1757 cursor = control.textCursor() 

1758 moved = False 

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

1760 distance = 0 

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

1762 while True: 

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

1764 distance += abs(y - lastY) 

1765 lastY = y 

1766 moved = cursor.movePosition(op, moveMode) 

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

1768 break 

1769 sb = control.verticalScrollBar() 

1770 if moved: 

1771 if op == MoveOperation.Up: 

1772 cursor.movePosition(MoveOperation.Down, moveMode) 

1773 sb.triggerAction(SliderAction.SliderPageStepSub) 

1774 else: 

1775 cursor.movePosition(MoveOperation.Up, moveMode) 

1776 sb.triggerAction(SliderAction.SliderPageStepAdd) 

1777 control.setTextCursor(cursor) 

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

1779 def linesPerPage(self): 

1780 """QTextEditWrapper.""" 

1781 # Not used in Leo's core. 

1782 w = self.widget 

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

1784 lineSpacing = w.fontMetrics().lineSpacing() 

1785 n = h / lineSpacing 

1786 return n 

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

1788 def scrollDelegate(self, kind): 

1789 """ 

1790 Scroll a QTextEdit up or down one page. 

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

1792 """ 

1793 c = self.c 

1794 w = self.widget 

1795 vScroll = w.verticalScrollBar() 

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

1797 lineSpacing = w.fontMetrics().lineSpacing() 

1798 n = h / lineSpacing 

1799 n = max(2, n - 3) 

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

1801 delta = n / 2 

1802 elif kind == 'down-line': 

1803 delta = 1 

1804 elif kind == 'down-page': 

1805 delta = n 

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

1807 delta = -n / 2 

1808 elif kind == 'up-line': 

1809 delta = -1 

1810 elif kind == 'up-page': 

1811 delta = -n 

1812 else: 

1813 delta = 0 

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

1815 val = vScroll.value() 

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

1817 c.bodyWantsFocus() 

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

1819 def see(self, see_i): 

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

1821 w = self.widget 

1822 tc = w.textCursor() 

1823 # Put see_i in range. 

1824 s = self.getAllText() 

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

1826 # Remember the old cursor 

1827 old_cursor = QtGui.QTextCursor(tc) 

1828 # Scroll so that see_i is visible. 

1829 tc.setPosition(see_i) 

1830 w.setTextCursor(tc) 

1831 w.ensureCursorVisible() 

1832 # Restore the old cursor 

1833 w.setTextCursor(old_cursor) 

1834 

1835 def seeInsertPoint(self): 

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

1837 self.widget.ensureCursorVisible() 

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

1839 def setAllText(self, s): 

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

1841 w = self.widget 

1842 try: 

1843 self.changingText = True # Disable onTextChanged. 

1844 w.setReadOnly(False) 

1845 w.setPlainText(s) 

1846 finally: 

1847 self.changingText = False 

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

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

1850 # Fix bug 981849: incorrect body content shown. 

1851 # Use the more careful code in setSelectionRange. 

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

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

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

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

1856 # 

1857 # Part 1 

1858 w = self.widget 

1859 i = self.toPythonIndex(i) 

1860 j = self.toPythonIndex(j) 

1861 if s is None: 

1862 s = self.getAllText() 

1863 n = len(s) 

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

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

1866 if insert is None: 

1867 ins = max(i, j) 

1868 else: 

1869 ins = self.toPythonIndex(insert) 

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

1871 # 

1872 # Part 2: 

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

1874 # Using tc.movePosition doesn't work. 

1875 tc = w.textCursor() 

1876 if i == j: 

1877 tc.setPosition(i) 

1878 elif ins == j: 

1879 # Put the insert point at j 

1880 tc.setPosition(i) 

1881 tc.setPosition(j, MoveMode.KeepAnchor) 

1882 elif ins == i: 

1883 # Put the insert point at i 

1884 tc.setPosition(j) 

1885 tc.setPosition(i, MoveMode.KeepAnchor) 

1886 else: 

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

1888 tc.setPosition(j) 

1889 tc.setPosition(i, MoveMode.KeepAnchor) 

1890 w.setTextCursor(tc) 

1891 # #218. 

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

1893 if s[i:j]: 

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

1895 # 

1896 # Remember the values for v.restoreCursorAndScroll. 

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

1898 v.insertSpot = ins 

1899 if i > j: 

1900 i, j = j, i 

1901 assert i <= j 

1902 v.selectionStart = i 

1903 v.selectionLength = j - i 

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

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

1906 def setXScrollPosition(self, pos): 

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

1908 if pos is not None: 

1909 w = self.widget 

1910 sb = w.horizontalScrollBar() 

1911 sb.setSliderPosition(pos) 

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

1913 def setYScrollPosition(self, pos): 

1914 """Set the vertical scrollbar position.""" 

1915 if pos is not None: 

1916 w = self.widget 

1917 sb = w.verticalScrollBar() 

1918 sb.setSliderPosition(pos) 

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

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

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

1922 w = self 

1923 te = self.widget 

1924 if index is None: 

1925 return 0 

1926 if isinstance(index, int): 

1927 return index 

1928 if index == '1.0': 

1929 return 0 

1930 if index == 'end': 

1931 return w.getLastPosition() 

1932 doc = te.document() 

1933 data = index.split('.') 

1934 if len(data) == 2: 

1935 row, col = data 

1936 row, col = int(row), int(col) 

1937 bl = doc.findBlockByNumber(row - 1) 

1938 return bl.position() + col 

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

1940 return 0 

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

1942 def toPythonIndexRowCol(self, index): 

1943 w = self 

1944 if index == '1.0': 

1945 return 0, 0, 0 

1946 if index == 'end': 

1947 index = w.getLastPosition() 

1948 te = self.widget 

1949 doc = te.document() 

1950 i = w.toPythonIndex(index) 

1951 bl = doc.findBlock(i) 

1952 row = bl.blockNumber() 

1953 col = i - bl.position() 

1954 return i, row, col 

1955 #@-others 

1956#@-others 

1957 

1958#@@language python 

1959#@@tabwidth -4 

1960#@@pagewidth 70 

1961#@-leo