Coverage for C:\Repos\leo-editor\leo\plugins\qt_text.py: 23%

1211 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 10:21 -0500

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

6#@+<< imports qt_text.py >> 

7#@+node:ekr.20220416085845.1: ** << imports qt_text.py >> 

8import time 

9assert time 

10from typing import Any, Callable, Dict, List, Tuple 

11from typing import TYPE_CHECKING 

12from leo.core import leoGlobals as g 

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

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

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

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

17 

18#@-<< imports qt_text.py >> 

19#@+<< type aliases qt_text.py >> 

20#@+node:ekr.20220416085945.1: ** << type aliases qt_text.py >> 

21if TYPE_CHECKING: # Always False at runtime. 

22 from leo.core.leoCommands import Commands as Cmdr 

23 # from leo.core.leoNodes import Position as Pos 

24else: 

25 Cmdr = Any 

26 

27Event = Any 

28Index = Any # For now, really Union[int, str], but that creates type-checking problems. 

29Widget = Any 

30Wrapper = Any 

31#@-<< type aliases qt_text.py >> 

32 

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

34QColor = QtGui.QColor 

35QFontMetrics = QtGui.QFontMetrics 

36 

37#@+others 

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

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

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

41def zoom_in(event: Event=None, delta: int=1) -> None: 

42 """increase body font size by one 

43 

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

45 """ 

46 zoom_helper(event, delta=1) 

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

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

49def zoom_out(event: Event=None) -> None: 

50 """decrease body font size by one 

51 

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

53 """ 

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

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

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

57def zoom_helper(event: Event, delta: int) -> None: 

58 """ 

59 Common helper for zoom commands. 

60 """ 

61 c = event.get('c') 

62 if not c: 

63 return 

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

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

66 g.trace('text zoom disabled') 

67 return 

68 wrapper = c.frame.body.wrapper 

69 # 

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

71 # Apply to body widget directly 

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

73 ssm = c.styleSheetManager 

74 sheet = ssm.expand_css_constants(c.active_stylesheet) 

75 wrapper.widget.setStyleSheet(sheet) 

76 # 

77 # #490: Honor language-specific settings. 

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

79 if not colorizer: 

80 return 

81 c.zoom_delta = delta 

82 colorizer.configure_fonts() 

83 wrapper.setAllText(wrapper.getAllText()) # Recolor everything. 

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

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

86hilite_doc = r''' 

87Changing The Current Line Highlighting Color 

88-------------------------------------------- 

89 

90The 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. 

91 

92The 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. 

93 

94Settings for Current Line Highlighting 

95--------------------------------------- 

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

97 

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

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

100''' 

101 

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

103def helpForLineHighlight(self: Any, event: Event=None) -> None: 

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

105 self.c.putHelpFor(hilite_doc) 

106 

107#@+node:tom.20220424002954.1: ** Show Right Margin Settings command 

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

109rmargin_doc = r''' 

110Right Margin Guidelines 

111------------------------- 

112 

113A vertical guideline may optionally shown at the right margin of the 

114body editor. The guideline will be shown at 

115 

1161. The column value of an @pagewidth directive in effect; or 

1172. The column value given by the setting ``@int rguide-col = <col>``; or 

1183. Column 80. 

119 

120The guideline will be shown if the setting ``@bool show-rmargin-guide`` 

121is ``True``. 

122 

123The color of the guideline is set based on the current text color. 

124''' 

125 

126@g.command('help-for-right-margin-guide') 

127def helpForRMarginGuides(self, event=None): 

128 """Displays settings used by right margin guide lines.""" 

129 self.c.putHelpFor(rmargin_doc) 

130 

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

132class QTextMixin: 

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

134 #@+others 

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

136 def __init__(self, c: Cmdr=None) -> None: 

137 """Ctor for QTextMixin class""" 

138 self.c = c 

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

140 self.enabled = True 

141 # A flag for k.masterKeyHandler and isTextWrapper. 

142 self.supportsHighLevelInterface = True 

143 self.tags: Dict[str, str] = {} 

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

145 self.useScintilla = False # This is used! 

146 self.virtualInsertPoint = None 

147 if c: 

148 self.injectIvars(c) 

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

150 def injectIvars(self, c: Cmdr) -> Widget: 

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

152 w = self 

153 w.leo_p = c.p.copy() if c.p else None 

154 w.leo_active = True 

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

156 w.leo_bodyBar = None 

157 w.leo_bodyXBar = None 

158 w.leo_chapter = None 

159 w.leo_frame = None 

160 w.leo_name = '1' 

161 w.leo_label = None 

162 return w 

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

164 def getName(self) -> str: 

165 return self.name # Essential. 

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

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

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

169 def onCursorPositionChanged(self, event: Event=None) -> None: 

170 

171 c = self.c 

172 name = c.widget_name(self) 

173 # Apparently, this does not cause problems 

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

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

176 return 

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

178 c.frame.statusLine.update() 

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

180 def onTextChanged(self) -> None: 

181 """ 

182 Update Leo after the body has been changed. 

183 

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

185 """ 

186 # Important: usually w.changingText is True. 

187 # This method very seldom does anything. 

188 w = self 

189 c, p = self.c, self.c.p 

190 tree = c.frame.tree 

191 if w.changingText: 

192 return 

193 if tree.tree_select_lockout: 

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

195 return 

196 if not p: 

197 return 

198 newInsert = w.getInsertPoint() 

199 newSel = w.getSelectionRange() 

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

201 # Get the previous values from the VNode. 

202 oldText = p.b 

203 if oldText == newText: 

204 # This can happen as the result of undo. 

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

206 return 

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

208 oldSel = (i, i + j) 

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

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

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

212 # These call only wrapper methods. 

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

214 def disable(self) -> None: 

215 self.enabled = False 

216 

217 def enable(self, enabled: bool=True) -> None: 

218 self.enabled = enabled 

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

220 def clipboard_append(self, s: str) -> None: 

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

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

223 

224 def clipboard_clear(self) -> None: 

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

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

227 def setFocus(self) -> None: 

228 """QTextMixin""" 

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

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

231 # Call the base class 

232 assert isinstance(self.widget, ( 

233 QtWidgets.QTextBrowser, 

234 QtWidgets.QLineEdit, 

235 QtWidgets.QTextEdit, 

236 Qsci and Qsci.QsciScintilla, 

237 )), self.widget 

238 QtWidgets.QTextBrowser.setFocus(self.widget) 

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

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

241 def appendText(self, s: str) -> None: 

242 """QTextMixin""" 

243 s2 = self.getAllText() 

244 self.setAllText(s2 + s) 

245 self.setInsertPoint(len(s2)) 

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

247 def delete(self, i: Index, j: Index=None) -> None: 

248 """QTextMixin""" 

249 i = self.toPythonIndex(i) 

250 if j is None: 

251 j = i + 1 

252 j = self.toPythonIndex(j) 

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

254 if i > j: 

255 i, j = j, i 

256 s = self.getAllText() 

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

258 # Bug fix: Significant in external tests. 

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

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

261 def deleteTextSelection(self) -> None: 

262 """QTextMixin""" 

263 i, j = self.getSelectionRange() 

264 self.delete(i, j) 

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

266 def get(self, i: int, j: int=None) -> str: 

267 """QTextMixin""" 

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

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

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

271 s = self.getAllText() 

272 i = self.toPythonIndex(i) 

273 j = self.toPythonIndex(j) 

274 return s[i:j] 

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

276 def getLastPosition(self, s: str=None) -> int: 

277 """QTextMixin""" 

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

279 

280 def getLength(self, s: str=None) -> int: 

281 """QTextMixin""" 

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

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

284 def getSelectedText(self) -> str: 

285 """QTextMixin""" 

286 i, j = self.getSelectionRange() 

287 if i == j: 

288 return '' 

289 s = self.getAllText() 

290 return s[i:j] 

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

292 def insert(self, i: Index, s: str) -> int: 

293 """QTextMixin""" 

294 s2 = self.getAllText() 

295 i = self.toPythonIndex(i) 

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

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

298 return i 

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

300 def seeInsertPoint(self) -> None: 

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

302 # getInsertPoint defined in client classes. 

303 self.see(self.getInsertPoint()) 

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

305 def selectAllText(self, s: str=None) -> None: 

306 """QTextMixin.""" 

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

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

309 def toPythonIndex(self, index: Index, s: str=None) -> int: 

310 """QTextMixin""" 

311 if s is None: 

312 s = self.getAllText() 

313 i = g.toPythonIndex(s, index) 

314 return i 

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

316 def toPythonIndexRowCol(self, index: int) -> Tuple[int, int, int]: 

317 """QTextMixin""" 

318 s = self.getAllText() 

319 i = self.toPythonIndex(index) 

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

321 return i, row, col 

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

323 def rememberSelectionAndScroll(self) -> None: 

324 

325 w = self 

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

327 v.insertSpot = w.getInsertPoint() 

328 i, j = w.getSelectionRange() 

329 if i > j: 

330 i, j = j, i 

331 assert i <= j 

332 v.selectionStart = i 

333 v.selectionLength = j - i 

334 v.scrollBarSpot = w.getYScrollPosition() 

335 #@-others 

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

337class QLineEditWrapper(QTextMixin): 

338 """ 

339 A class to wrap QLineEdit widgets. 

340 

341 The QHeadlineWrapper class is a subclass that merely 

342 redefines the do-nothing check method here. 

343 """ 

344 #@+others 

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

346 def __init__(self, widget: Widget, name: str, c: Cmdr=None) -> None: 

347 """Ctor for QLineEditWrapper class.""" 

348 super().__init__(c) 

349 self.widget = widget 

350 self.name = name 

351 self.baseClassName = 'QLineEditWrapper' 

352 

353 def __repr__(self) -> str: 

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

355 

356 __str__ = __repr__ 

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

358 def check(self) -> bool: 

359 """ 

360 QLineEditWrapper. 

361 """ 

362 return True 

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

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

365 def getAllText(self) -> str: 

366 """QHeadlineWrapper.""" 

367 if self.check(): 

368 w = self.widget 

369 return w.text() 

370 return '' 

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

372 def getInsertPoint(self) -> int: 

373 """QHeadlineWrapper.""" 

374 if self.check(): 

375 return self.widget.cursorPosition() 

376 return 0 

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

378 def getSelectionRange(self, sort: bool=True) -> Tuple[int, int]: 

379 """QHeadlineWrapper.""" 

380 w = self.widget 

381 if self.check(): 

382 if w.hasSelectedText(): 

383 i = w.selectionStart() 

384 s = w.selectedText() 

385 j = i + len(s) 

386 else: 

387 i = j = w.cursorPosition() 

388 return i, j 

389 return 0, 0 

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

391 def getYScrollPosition(self) -> int: 

392 return 0 # #1801. 

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

394 def hasSelection(self) -> bool: 

395 """QHeadlineWrapper.""" 

396 if self.check(): 

397 return self.widget.hasSelectedText() 

398 return False 

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

400 def see(self, i: int) -> None: 

401 """QHeadlineWrapper.""" 

402 

403 pass 

404 

405 def seeInsertPoint(self) -> None: 

406 """QHeadlineWrapper.""" 

407 pass 

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

409 def setAllText(self, s: str) -> None: 

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

411 if self.check(): 

412 w = self.widget 

413 w.setText(s) 

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

415 def setFocus(self) -> None: 

416 """QHeadlineWrapper.""" 

417 if self.check(): 

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

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

420 def setInsertPoint(self, i: int, s: str=None) -> None: 

421 """QHeadlineWrapper.""" 

422 if not self.check(): 

423 return 

424 w = self.widget 

425 if s is None: 

426 s = w.text() 

427 i = self.toPythonIndex(i) 

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

429 w.setCursorPosition(i) 

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

431 def setSelectionRange(self, 

432 i: Index, j: Index, insert: Index=None, s: str=None, 

433 ) -> None: 

434 """QHeadlineWrapper.""" 

435 if not self.check(): 

436 return 

437 w = self.widget 

438 if i > j: 

439 i, j = j, i 

440 if s is None: 

441 s = w.text() 

442 n = len(s) 

443 i = self.toPythonIndex(i) 

444 j = self.toPythonIndex(j) 

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

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

447 if insert is None: 

448 insert = j 

449 else: 

450 insert = self.toPythonIndex(insert) 

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

452 if i == j: 

453 w.setCursorPosition(i) 

454 else: 

455 length = j - i 

456 # Set selection is a QLineEditMethod 

457 if insert < j: 

458 w.setSelection(j, -length) 

459 else: 

460 w.setSelection(i, length) 

461 # setSelectionRangeHelper = setSelectionRange 

462 #@-others 

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

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

465 """ 

466 A QFrame supporting gutter line numbers. 

467 

468 This class *has* a QTextEdit. 

469 """ 

470 #@+others 

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

472 def __init__(self, c: Cmdr, e: Any, *args: Any) -> None: 

473 """Ctor for LineTextWidget.""" 

474 super().__init__(*args) 

475 self.c = c 

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

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

478 self.setFrameStyle(Raised) 

479 self.edit = e # A QTextEdit 

480 e.setFrameStyle(NoFrame) 

481 # e.setAcceptRichText(False) 

482 self.number_bar = NumberBar(c, e) 

483 hbox = QtWidgets.QHBoxLayout(self) 

484 hbox.setSpacing(0) 

485 hbox.setContentsMargins(0, 0, 0, 0) 

486 hbox.addWidget(self.number_bar) 

487 hbox.addWidget(e) 

488 e.installEventFilter(self) 

489 e.viewport().installEventFilter(self) 

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

491 def eventFilter(self, obj: Any, event: Event) -> Any: 

492 """ 

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

494 This is easier than connecting all necessary signals. 

495 """ 

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

497 self.number_bar.update() 

498 return False 

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

500 #@-others 

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

502if QtWidgets: 

503 

504 

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

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

507 #@+others 

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

509 def __init__(self, parent: Widget, c: Cmdr, wrapper: Wrapper) -> None: 

510 """ctor for LeoQTextBrowser class.""" 

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

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

513 self.leo_c = c 

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

515 self.leo_wrapper = wrapper 

516 self.htmlFlag = True 

517 super().__init__(parent) 

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

519 

520 # Connect event handlers... 

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

522 # #1286 

523 self.textChanged.connect(self.onTextChanged) 

524 self.cursorPositionChanged.connect(self.highlightCurrentLine) 

525 self.textChanged.connect(self.highlightCurrentLine) 

526 self.setContextMenuPolicy(ContextMenuPolicy.CustomContextMenu) 

527 self.customContextMenuRequested.connect(self.onContextMenu) 

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

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

530 vsb.valueChanged.connect(self.onSliderChanged) 

531 # For QCompleter 

532 self.leo_q_completer = None 

533 self.leo_options = None 

534 self.leo_model = None 

535 

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

537 hl_color = QColor(hl_color_setting) 

538 self.hiliter_params = { 

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

540 'last_color_setting': hl_color_setting, 

541 'last_fg': '', 'last_bg': '', 

542 'last_hl_color': hl_color 

543 } 

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

545 def __repr__(self) -> str: 

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

547 

548 __str__ = __repr__ 

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

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

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

552 #@+others 

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

554 def __init__(self, c: Cmdr) -> None: 

555 """ctor for LeoQListWidget class""" 

556 super().__init__() 

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

558 # Inject the ivars 

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

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

561 self.leo_c = c 

562 # A weird hack. 

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

564 self.itemClicked.connect(self.select_callback) 

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

566 def closeEvent(self, event: Event) -> None: 

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

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

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

570 def end_completer(self) -> None: 

571 """End completion.""" 

572 c = self.leo_c 

573 c.in_qt_dialog = False 

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

575 c.k.keyboardQuit() 

576 c.bodyWantsFocusNow() 

577 try: 

578 self.deleteLater() 

579 except RuntimeError: 

580 # Avoid bug 1338773: Autocompleter error 

581 pass 

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

583 def get_selection(self) -> str: 

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

585 return self.currentItem().text() 

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

587 def keyPressEvent(self, event: Event) -> None: 

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

589 c = self.leo_c 

590 w = c.frame.body.wrapper 

591 key = event.key() 

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

593 # A modifier key on it's own. 

594 pass 

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

596 QtWidgets.QListWidget.keyPressEvent(self, event) 

597 elif key == Key.Key_Tab: 

598 self.tab_callback() 

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

600 self.select_callback() 

601 else: 

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

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

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

605 def select_callback(self) -> None: 

606 """ 

607 Called when user selects an item in the QListWidget. 

608 """ 

609 c = self.leo_c 

610 p = c.p 

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

612 oldSel = w.getSelectionRange() 

613 oldText = w.getAllText() 

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

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

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

617 parts = prefix.split('.') 

618 if len(parts) > 1: 

619 tail = parts[-1] 

620 else: 

621 tail = prefix 

622 if tail != completion: 

623 j = w.getInsertPoint() 

624 i = j - len(tail) 

625 w.delete(i, j) 

626 w.insert(i, completion) 

627 j = i + len(completion) 

628 c.setChanged() 

629 w.setInsertPoint(j) 

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

631 newText=w.getAllText(), 

632 newInsert=w.getInsertPoint(), 

633 newSel=w.getSelectionRange(), 

634 oldSel=oldSel, 

635 ) 

636 self.end_completer() 

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

638 def tab_callback(self) -> None: 

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

640 c = self.leo_c 

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

642 if w is None: 

643 return 

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

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

646 parts = prefix.split('.') 

647 if len(parts) < 2: 

648 return 

649 i = j = w.getInsertPoint() 

650 s = w.getAllText() 

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

652 i -= 1 

653 i += 1 

654 if j > i: 

655 w.delete(i, j) 

656 w.setInsertPoint(i) 

657 c.k.autoCompleter.compute_completion_list() 

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

659 def set_position(self, c: Cmdr) -> None: 

660 """Set the position of the QListWidget.""" 

661 

662 def glob(obj: Any, pt: str) -> Any: 

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

664 return obj.mapToGlobal(pt) 

665 

666 w = self.leo_w 

667 vp = self.viewport() 

668 r = w.cursorRect() 

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

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

671 # As a workaround to the Qt setGeometry bug, 

672 # The window is destroyed instead of being hidden. 

673 if self.leo_geom_set: 

674 g.trace('Error: leo_geom_set') 

675 return 

676 # This code illustrates the Qt bug... 

677 # if self.leo_geom_set: 

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

679 # gg_topLeft = geom.topLeft() 

680 # else: 

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

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

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

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

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

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

687 x_offset, y_offset = 10, 60 

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

689 geom2_topLeft = QtCore.QPoint( 

690 geom.x() + delta_x + x_offset, 

691 geom.y() + delta_y + y_offset) 

692 geom2_size = QtCore.QSize(400, 100) 

693 geom2 = QtCore.QRect(geom2_topLeft, geom2_size) 

694 # These tests fail once offsets are added. 

695 if x_offset == 0 and y_offset == 0: 

696 if self.leo_geom_set: 

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

698 g.trace( 

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

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

701 else: 

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

703 g.trace( 

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

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

706 self.setGeometry(geom2) 

707 self.leo_geom_set = True 

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

709 def show_completions(self, aList: List[str]) -> None: 

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

711 self.clear() 

712 self.addItems(aList) 

713 self.setCurrentRow(0) 

714 self.activateWindow() 

715 self.setFocus() 

716 #@-others 

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

718 def init_completer(self, options: List[str]) -> Widget: 

719 """Connect a QCompleter.""" 

720 c = self.leo_c 

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

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

723 qc.set_position(c) 

724 # Show the initial completions. 

725 c.in_qt_dialog = True 

726 qc.show() 

727 qc.activateWindow() 

728 c.widgetWantsFocusNow(qc) 

729 qc.show_completions(options) 

730 return qc 

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

732 def end_completer(self) -> None: 

733 if hasattr(self, 'leo_qc'): 

734 self.leo_qc.end_completer() 

735 delattr(self, 'leo_qc') 

736 

737 def show_completions(self, aList: List) -> None: 

738 if hasattr(self, 'leo_qc'): 

739 self.leo_qc.show_completions(aList) 

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

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

742 #@@language python 

743 @staticmethod 

744 def parse_css(css_string: str, clas: str='') -> Tuple[str, str]: 

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

746 

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

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

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

750 css for our widget. 

751 

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

753 """ 

754 # Get first block with name matching "clas' 

755 block = css_string.split(clas, 1) 

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

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

758 

759 # Split into styles separated by ";" 

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

761 

762 # Split into fields separated by ":" 

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

764 

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

766 color = bg = '' 

767 for style, val in fields: 

768 style = style.strip() 

769 if style == 'color': 

770 color = val.strip() 

771 elif style == 'background': 

772 bg = val.strip() 

773 if color and bg: 

774 break 

775 return color, bg 

776 

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

778 #@@language python 

779 @staticmethod 

780 def assign_bg(fg: str) -> Any: 

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

782 reasonable values. Can happen with incorrectly 

783 constructed themes, or no-theme color schemes. 

784 

785 Intended to be called when bg color is missing. 

786 

787 RETURNS 

788 a QColor object for the background color 

789 """ 

790 if not fg: 

791 fg = 'black' # QTextEdit default 

792 bg = 'white' # QTextEdit default 

793 if fg == 'black': 

794 bg = 'white' # QTextEdit default 

795 else: 

796 fg_color = QColor(fg) 

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

798 if v < 128: # dark foreground 

799 bg = 'white' 

800 else: 

801 bg = 'black' 

802 return QColor(bg) 

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

804 #@@language python 

805 @staticmethod 

806 def calc_hl(palette: QtGui.QPalette) -> QColor: # type:ignore 

807 """Return the line highlight color. 

808 

809 ARGUMENT 

810 palette -- a QPalette object for the body 

811 

812 RETURNS 

813 a QColor object for the highlight color 

814 """ 

815 fg = palette.text().color() 

816 bg = palette.window().color() 

817 hsv_fg = fg.getHsv() 

818 hsv_bg = bg.getHsv() 

819 v_fg = hsv_fg[2] 

820 v_bg = hsv_bg[2] 

821 is_dark_on_light = v_fg < v_bg 

822 if is_dark_on_light: 

823 hl = bg.darker(110) 

824 else: 

825 if v_bg < 20: 

826 hl = QColor(bg) 

827 hl.setHsv(360, 0, 30, hsv_bg[3]) 

828 else: 

829 hl = bg.lighter(140) 

830 return hl 

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

832 #@@language python 

833 def highlightCurrentLine(self) -> None: 

834 """Highlight cursor line.""" 

835 c = self.leo_c 

836 params = self.hiliter_params 

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

838 

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

840 editor.setExtraSelections([]) 

841 return 

842 

843 curs = editor.textCursor() 

844 blocknum = curs.blockNumber() 

845 

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

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

848 # return 

849 

850 if blocknum == 0: # invalid position 

851 blocknum = 1 

852 params['lastblock'] = blocknum 

853 

854 hl_color = params['last_hl_color'] 

855 

856 #@+<< Recalculate Color >> 

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

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

859 or '' 

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

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

862 .replace('none', '')) 

863 

864 last_color_setting = params['last_color_setting'] 

865 config_setting_changed = config_setting != last_color_setting 

866 

867 if config_setting: 

868 if config_setting_changed: 

869 hl_color = QColor(config_setting) 

870 params['last_hl_color'] = hl_color 

871 params['last_color_setting'] = config_setting 

872 else: 

873 hl_color = params['last_hl_color'] 

874 else: 

875 # Get current colors from the body editor widget 

876 wrapper = c.frame.body.wrapper 

877 w = wrapper.widget 

878 palette = w.viewport().palette() 

879 

880 fg_hex = palette.text().color().rgb() 

881 bg_hex = palette.window().color().rgb() 

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

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

884 

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

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

887 hl_color = self.calc_hl(palette) 

888 params['last_hl_color'] = hl_color 

889 params['last_fg'] = fg 

890 params['last_bg'] = bg 

891 #@-<< Recalculate Color >> 

892 #@+<< Apply Highlight >> 

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

894 # Based on code from 

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

896 

897 selection = editor.ExtraSelection() 

898 selection.format.setBackground(hl_color) 

899 selection.format.setProperty(FullWidthSelection, True) 

900 selection.cursor = curs 

901 selection.cursor.clearSelection() 

902 

903 editor.setExtraSelections([selection]) 

904 #@-<< Apply Highlight >> 

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

906 def getXScrollPosition(self) -> int: 

907 """Get the horizontal scrollbar position.""" 

908 w = self 

909 sb = w.horizontalScrollBar() 

910 pos = sb.sliderPosition() 

911 return pos 

912 

913 def setXScrollPosition(self, pos: int) -> None: 

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

915 if pos is not None: 

916 w = self 

917 sb = w.horizontalScrollBar() 

918 sb.setSliderPosition(pos) 

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

920 def getYScrollPosition(self) -> int: 

921 """Get the vertical scrollbar position.""" 

922 w = self 

923 sb = w.verticalScrollBar() 

924 pos = sb.sliderPosition() 

925 return pos 

926 

927 def setYScrollPosition(self, pos: int) -> None: 

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

929 w = self 

930 if pos is None: 

931 pos = 0 

932 sb = w.verticalScrollBar() 

933 sb.setSliderPosition(pos) 

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

935 def leo_dumpButton(self, event: Event, tag: str) -> str: 

936 button = event.button() 

937 table = ( 

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

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

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

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

942 ) 

943 for val, s in table: 

944 if button == val: 

945 kind = s 

946 break 

947 else: 

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

949 return kind 

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

951 def onContextMenu(self, point: Any) -> None: 

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

953 # #1286. 

954 c, w = self.leo_c, self 

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

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

957 def onSliderChanged(self, arg: int) -> None: 

958 """Handle a Qt onSliderChanged event.""" 

959 c = self.leo_c 

960 p = c.p 

961 # Careful: changing nodes changes the scrollbars. 

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

963 if c.frame.tree.tree_select_lockout: 

964 return 

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

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

967 return 

968 if p: 

969 p.v.scrollBarSpot = arg 

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

971 leo_cursor_width = 0 

972 

973 leo_vim_mode = None 

974 

975 def paintEvent(self, event: Event) -> None: 

976 """ 

977 LeoQTextBrowser.paintEvent. 

978 

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

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

981 New in Leo 6.6.2: Draw right margin guideline. 

982 """ 

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

984 # 

985 # First, call the base class paintEvent. 

986 QtWidgets.QTextBrowser.paintEvent(self, event) 

987 

988 def set_cursor_width(width: int) -> None: 

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

990 if self.leo_cursor_width != width: 

991 self.leo_cursor_width = width 

992 w.setCursorWidth(width) 

993 

994 if w == c.frame.body.widget and \ 

995 c.config.getBool('show-rmargin-guide'): 

996 #@+<< paint margin guides >> 

997 #@+node:tom.20220423204906.1: *4* << paint margin guides >> 

998 # based on https://stackoverflow.com/questions/30371613/draw-vertical-lines-on-qtextedit-in-pyqt 

999 # Honor @pagewidth directive if any 

1000 dict_list = g.get_directives_dict_list(c.p) 

1001 rcol = (g.scanAtPagewidthDirectives(dict_list) 

1002 or c.config.getInt('rguide-col') or 80) 

1003 

1004 vp = w.viewport() 

1005 palette = vp.palette() 

1006 font = w.document().defaultFont() 

1007 fm = QFontMetrics(font) 

1008 rmargin = fm.horizontalAdvance('9' * rcol) + 2 

1009 if vp.width() >= rmargin: 

1010 painter = QtGui.QPainter(vp) 

1011 pen = QtGui.QPen(SolidLine) 

1012 

1013 # guideline color 

1014 fg = palette.text().color() 

1015 bg = palette.window().color() 

1016 hsv_fg = fg.getHsv()[2] 

1017 hsv_bg = bg.getHsv()[2] 

1018 is_dark_on_light = hsv_fg < hsv_bg 

1019 if is_dark_on_light: 

1020 fg = fg.lighter() 

1021 else: 

1022 fg = fg.darker() 

1023 pen.setColor(fg) 

1024 

1025 pen.setWidth(1) 

1026 painter.setPen(pen) 

1027 painter.drawLine(rmargin, 0, rmargin, vp.height()) 

1028 #@-<< paint margin guides >> 

1029 

1030 # 

1031 # Are we in vim mode? 

1032 if self.leo_vim_mode is None: 

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

1034 # 

1035 # Are we in command mode? 

1036 if self.leo_vim_mode: 

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

1038 else: 

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

1040 # 

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

1042 if ( 

1043 not in_command 

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

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

1046 ): 

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

1048 return 

1049 # 

1050 # Set the width of the cursor. 

1051 font = w.currentFont() 

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

1053 set_cursor_width(cursor_width) 

1054 # 

1055 # Draw a box around the cursor. 

1056 qp = QtGui.QPainter() 

1057 qp.begin(self.viewport()) 

1058 qp.drawRect(w.cursorRect()) 

1059 qp.end() 

1060 

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

1062 def wheelEvent(self, event: Event) -> None: 

1063 """Handle a wheel event.""" 

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

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

1066 try: # Qt5 or later. 

1067 point = event.angleDelta() 

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

1069 except AttributeError: 

1070 delta = event.delta() # Qt4. 

1071 if delta < 0: 

1072 zoom_out(d) 

1073 else: 

1074 zoom_in(d) 

1075 event.accept() 

1076 return 

1077 QtWidgets.QTextBrowser.wheelEvent(self, event) 

1078 #@-others 

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

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

1081 #@+others 

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

1083 def __init__(self, c: Cmdr, e: Any, *args: Any) -> None: 

1084 """Ctor for NumberBar class.""" 

1085 super().__init__(*args) 

1086 self.c = c 

1087 self.edit = e # A QTextEdit. 

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

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

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

1091 g.os_path_finalize_join(g.app.loadDir, 

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

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

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

1095 self.offsets: List[Tuple[int, Any]] = [] 

1096 self.setObjectName('gutter') 

1097 self.reloadSettings() 

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

1099 def reloadSettings(self) -> None: 

1100 c = self.c 

1101 c.registerReloadSettings(self) 

1102 # Extra width for column. 

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

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

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

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

1107 def mousePressEvent(self, event: Event) -> None: 

1108 

1109 c = self.c 

1110 

1111 def find_line(y: int) -> int: 

1112 n, last_y = 0, 0 

1113 for n, y2 in self.offsets: 

1114 if last_y <= y < y2: 

1115 return n 

1116 last_y = y2 

1117 return n if self.offsets else 0 

1118 

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

1120 if not xdb: 

1121 return 

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

1123 if not path: 

1124 return 

1125 n = find_line(event.y()) 

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

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

1128 return 

1129 if xdb.has_breakpoint(path, n): 

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

1131 else: 

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

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

1134 def update(self, *args: Any) -> None: 

1135 """ 

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

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

1138 """ 

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

1140 # Always allocate room for 2 columns 

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

1142 if isQt6: 

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

1144 else: 

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

1146 if self.width() != width: 

1147 self.setFixedWidth(width) 

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

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

1150 def paintEvent(self, event: Event) -> None: 

1151 """ 

1152 Enhance QFrame.paintEvent. 

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

1154 """ 

1155 e = self.edit 

1156 d = self.d 

1157 layout = d.documentLayout() 

1158 # Compute constants. 

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

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

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

1162 # Paint each visible block. 

1163 painter = QtGui.QPainter(self) 

1164 block = d.begin() 

1165 n = i = 0 

1166 c = self.c 

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

1168 self.offsets = [] 

1169 while block.isValid(): 

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

1171 n += 1 

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

1173 if top_left.y() > page_bottom: 

1174 break # Outside the visible area. 

1175 bold = block == current_block 

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

1177 block = block.next() 

1178 self.highest_line = i 

1179 painter.end() 

1180 QtWidgets.QWidget.paintEvent(self, event) # Propagate the event. 

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

1182 def paintBlock(self, 

1183 bold: bool, n: int, painter: Any, top_left: int, scroll_y: int, 

1184 ) -> None: 

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

1186 c = self.c 

1187 if bold: 

1188 self.setBold(painter, True) 

1189 s = str(n) 

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

1191 s = ' ' * pad + s 

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

1193 x = 0 

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

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

1196 painter.drawText(x, y, s) 

1197 if bold: 

1198 self.setBold(painter, False) 

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

1200 if not xdb: 

1201 return 

1202 if not xdb.has_breakpoints(): 

1203 return 

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

1205 if xdb.has_breakpoint(path, n): 

1206 target_r = QtCore.QRect( 

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

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

1209 16.0, 16.0) 

1210 if self.image: 

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

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

1213 else: 

1214 painter.drawEllipse(target_r) 

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

1216 def setBold(self, painter: Any, flag: bool) -> None: 

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

1218 font = painter.font() 

1219 font.setBold(flag) 

1220 painter.setFont(font) 

1221 #@-others 

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

1223class PlainTextWrapper(QTextMixin): 

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

1225 

1226 def __init__(self, widget: Widget) -> None: 

1227 """Ctor for the PlainTextWrapper class.""" 

1228 super().__init__() 

1229 self.widget = widget 

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

1231class QHeadlineWrapper(QLineEditWrapper): 

1232 """ 

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

1234 This class just redefines the check method. 

1235 """ 

1236 #@+others 

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

1238 def __init__(self, c: Cmdr, item: str, name: str, widget: Widget) -> None: 

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

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

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

1242 # Set ivars. 

1243 self.c = c 

1244 self.item = item 

1245 self.name = name 

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

1247 self.widget = widget 

1248 # Set the signal. 

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

1250 

1251 def __repr__(self) -> str: 

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

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

1254 def check(self) -> bool: 

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

1256 tree = self.c.frame.tree 

1257 try: 

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

1259 except RuntimeError: 

1260 return False 

1261 valid = tree.isValidItem(self.item) 

1262 result = valid and e == self.widget 

1263 return result 

1264 #@-others 

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

1266class QMinibufferWrapper(QLineEditWrapper): 

1267 

1268 def __init__(self, c: Cmdr) -> None: 

1269 """Ctor for QMinibufferWrapper class.""" 

1270 self.c = c 

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

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

1273 assert self.widget 

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

1275 # Monkey-patch the event handlers 

1276 #@+<< define mouseReleaseEvent >> 

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

1278 def mouseReleaseEvent(event: Event, self: Any=self) -> None: 

1279 """Override QLineEdit.mouseReleaseEvent. 

1280 

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

1282 """ 

1283 assert isinstance(self, QMinibufferWrapper), self 

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

1285 c, k = self.c, self.c.k 

1286 if not k.state.kind: 

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

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

1289 k.fullCommand(event2) 

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

1291 

1292 #@-<< define mouseReleaseEvent >> 

1293 

1294 w.mouseReleaseEvent = mouseReleaseEvent 

1295 

1296 def setStyleClass(self, style_class: Any) -> None: 

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

1298 # 

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

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

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

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

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

1304 # level sheet will get pushed down quite frequently. 

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

1306 

1307 def setSelectionRange(self, i: Index, j: Index, insert: Index=None, s: str=None) -> None: 

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

1309 insert = j if insert is None else insert 

1310 if self.widget: 

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

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

1313class QScintillaWrapper(QTextMixin): 

1314 """ 

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

1316 

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

1318 To do: 

1319 - Fix all Scintilla unit-test failures. 

1320 - Add support for all scintilla lexers. 

1321 """ 

1322 #@+others 

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

1324 def __init__(self, widget: Widget, c: Cmdr, name: str=None) -> None: 

1325 """Ctor for the QScintillaWrapper class.""" 

1326 super().__init__(c) 

1327 self.baseClassName = 'QScintillaWrapper' 

1328 self.c = c 

1329 self.name = name 

1330 self.useScintilla = True 

1331 self.widget = widget 

1332 # Complete the init. 

1333 self.set_config() 

1334 # Set the signal. 

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

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

1337 def set_config(self) -> None: 

1338 """Set QScintillaWrapper configuration options.""" 

1339 c, w = self.c, self.widget 

1340 n = c.config.getInt('qt-scintilla-zoom-in') # type:ignore 

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

1342 w.zoomIn(n) 

1343 w.setUtf8(True) # Important. 

1344 if 1: 

1345 w.setBraceMatching(2) # Sloppy 

1346 else: 

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

1348 if 0: 

1349 w.setMarginWidth(1, 40) 

1350 w.setMarginLineNumbers(1, True) 

1351 w.setIndentationWidth(4) 

1352 w.setIndentationsUseTabs(False) 

1353 w.setAutoIndent(True) 

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

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

1356 def delete(self, i: Index, j: Index=None) -> None: 

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

1358 w = self.widget 

1359 i = self.toPythonIndex(i) 

1360 if j is None: 

1361 j = i + 1 

1362 j = self.toPythonIndex(j) 

1363 self.setSelectionRange(i, j) 

1364 try: 

1365 self.changingText = True # Disable onTextChanged 

1366 w.replaceSelectedText('') 

1367 finally: 

1368 self.changingText = False 

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

1370 def flashCharacter(self, 

1371 i: int, bg: str='white', fg: str='red', flashes: int=2, delay: int=50, 

1372 ) -> None: 

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

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

1375 # This causes problems during unit tests: 

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

1377 if g.unitTesting: 

1378 return 

1379 #@+others 

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

1381 def after(func: Callable, delay: int=delay) -> None: 

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

1383 QtCore.QTimer.singleShot(delay, func) 

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

1385 def addFlashCallback(self=self) -> None: 

1386 i = self.flashIndex 

1387 w = self.widget 

1388 self.setSelectionRange(i, i + 1) 

1389 if self.flashBg: 

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

1391 if self.flashFg: 

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

1393 self.flashCount -= 1 

1394 after(removeFlashCallback) 

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

1396 def removeFlashCallback(self=self) -> None: 

1397 """Remove the extra selections.""" 

1398 self.setInsertPoint(self.flashIndex) 

1399 w = self.widget 

1400 if self.flashCount > 0: 

1401 after(addFlashCallback) 

1402 else: 

1403 w.resetSelectionBackgroundColor() 

1404 self.setInsertPoint(self.flashIndex1) 

1405 w.setFocus() 

1406 #@-others 

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

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

1409 bg = bg[:-1] 

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

1411 fg = fg[:-1] 

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

1413 self.flashCount = flashes 

1414 self.flashIndex1 = self.getInsertPoint() 

1415 self.flashIndex = self.toPythonIndex(i) 

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

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

1418 addFlashCallback() 

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

1420 def get(self, i: int, j: int=None) -> str: 

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

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

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

1424 s = self.getAllText() 

1425 i = self.toPythonIndex(i) 

1426 j = self.toPythonIndex(j) 

1427 return s[i:j] 

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

1429 def getAllText(self) -> str: 

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

1431 w = self.widget 

1432 return w.text() 

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

1434 def getInsertPoint(self) -> int: 

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

1436 w = self.widget 

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

1438 return i 

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

1440 def getSelectionRange(self, sort: bool=True) -> Tuple[int, int]: 

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

1442 w = self.widget 

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

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

1445 if sort and i > j: 

1446 i, j = j, i 

1447 return i, j 

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

1449 def getXScrollPosition(self) -> int: 

1450 # w = self.widget 

1451 return 0 # Not ready yet. 

1452 

1453 def getYScrollPosition(self) -> int: 

1454 # w = self.widget 

1455 return 0 # Not ready yet. 

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

1457 def hasSelection(self) -> bool: 

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

1459 return self.widget.hasSelectedText() 

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

1461 def insert(self, i: Index, s: str) -> int: 

1462 """Insert s at position i.""" 

1463 w = self.widget 

1464 i = self.toPythonIndex(i) 

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

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

1467 i += len(s) 

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

1469 return i 

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

1471 def linesPerPage(self) -> int: 

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

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

1474 w = self.widget 

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

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

1477 if 0: # Not yet. 

1478 

1479 def scrollDelegate(self, kind: str) -> None: 

1480 """ 

1481 Scroll a QTextEdit up or down one page. 

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

1483 """ 

1484 c = self.c 

1485 w = self.widget 

1486 vScroll = w.verticalScrollBar() 

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

1488 lineSpacing = w.fontMetrics().lineSpacing() 

1489 n = h / lineSpacing 

1490 n = max(2, n - 3) 

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

1492 delta = n / 2 

1493 elif kind == 'down-line': 

1494 delta = 1 

1495 elif kind == 'down-page': 

1496 delta = n 

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

1498 delta = -n / 2 

1499 elif kind == 'up-line': 

1500 delta = -1 

1501 elif kind == 'up-page': 

1502 delta = -n 

1503 else: 

1504 delta = 0 

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

1506 val = vScroll.value() 

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

1508 c.bodyWantsFocus() 

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

1510 def see(self, i: int) -> None: 

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

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

1513 w = self.widget 

1514 s = self.getAllText() 

1515 i = self.toPythonIndex(i) 

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

1517 w.ensureLineVisible(row) 

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

1519 def setAllText(self, s: str) -> None: 

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

1521 w = self.widget 

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

1523 w.setText(s) 

1524 # w.update() 

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

1526 def setInsertPoint(self, i: int, s: str=None) -> None: 

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

1528 w = self.widget 

1529 i = self.toPythonIndex(i) 

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

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

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

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

1534 def setSelectionRange(self, i: Index, j: int, insert: Index=None, s: str=None) -> None: 

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

1536 w = self.widget 

1537 i = self.toPythonIndex(i) 

1538 j = self.toPythonIndex(j) 

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

1540 if insert >= i: 

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

1542 else: 

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

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

1545 def setXScrollPosition(self, pos: int) -> None: 

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

1547 

1548 def setYScrollPosition(self, pos: int) -> None: 

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

1550 #@-others 

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

1552class QTextEditWrapper(QTextMixin): 

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

1554 #@+others 

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

1556 def __init__(self, widget: Widget, name: str, c: Cmdr=None) -> None: 

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

1558 super().__init__(c) 

1559 # Make sure all ivars are set. 

1560 self.baseClassName = 'QTextEditWrapper' 

1561 self.c = c 

1562 self.name = name 

1563 self.widget = widget 

1564 self.useScintilla = False 

1565 # Complete the init. 

1566 if c and widget: 

1567 self.widget.setUndoRedoEnabled(False) 

1568 self.set_config() 

1569 self.set_signals() 

1570 

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

1572 def set_config(self) -> None: 

1573 """Set configuration options for QTextEdit.""" 

1574 w = self.widget 

1575 w.setWordWrapMode(WrapMode.NoWrap) 

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

1577 if isQt6: 

1578 w.setTabStopDistance(24) 

1579 else: 

1580 w.setTabStopWidth(24) 

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

1582 def set_signals(self) -> None: 

1583 """Set up signals.""" 

1584 c, name = self.c, self.name 

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

1586 # Hook up qt events. 

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

1588 if name == 'body': 

1589 w = self.widget 

1590 w.textChanged.connect(self.onTextChanged) 

1591 w.cursorPositionChanged.connect(self.onCursorPositionChanged) 

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

1593 # Monkey patch the event handler. 

1594 #@+others 

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

1596 def mouseReleaseEvent(event: Event, self: Any=self) -> None: 

1597 """ 

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

1599 """ 

1600 assert isinstance(self, QTextEditWrapper), self 

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

1602 # Call the base class. 

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

1604 c = self.c 

1605 setattr(event, 'c', c) 

1606 # Open the url on a control-click. 

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

1608 g.openUrlOnClick(event) 

1609 else: 

1610 if name == 'body': 

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

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

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

1614 c.k.keyboardQuit(setFocus=False) 

1615 #@-others 

1616 self.widget.mouseReleaseEvent = mouseReleaseEvent 

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

1618 def __repr__(self) -> str: 

1619 # Add a leading space to align with StringTextWrapper. 

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

1621 

1622 __str__ = __repr__ 

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

1624 # These are all widget-dependent 

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

1626 def delete(self, i: Index, j: Index=None) -> None: 

1627 """QTextEditWrapper.""" 

1628 w = self.widget 

1629 i = self.toPythonIndex(i) 

1630 if j is None: 

1631 j = i + 1 

1632 j = self.toPythonIndex(j) 

1633 if i > j: 

1634 i, j = j, i 

1635 sb = w.verticalScrollBar() 

1636 pos = sb.sliderPosition() 

1637 cursor = w.textCursor() 

1638 try: 

1639 self.changingText = True # Disable onTextChanged 

1640 old_i, old_j = self.getSelectionRange() 

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

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

1643 cursor.removeSelectedText() 

1644 elif i == j: 

1645 pass 

1646 else: 

1647 cursor.setPosition(i) 

1648 moveCount = abs(j - i) 

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

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

1651 cursor.removeSelectedText() 

1652 finally: 

1653 self.changingText = False 

1654 sb.setSliderPosition(pos) 

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

1656 def flashCharacter(self, 

1657 i: int, bg: str='white', fg: str='red', flashes: int=3, delay: int=75, 

1658 ) -> None: 

1659 """QTextEditWrapper.""" 

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

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

1662 bg = bg[:-1] 

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

1664 fg = fg[:-1] 

1665 # This might causes problems during unit tests. 

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

1667 if g.unitTesting: 

1668 return 

1669 w = self.widget # A QTextEdit. 

1670 # Remember highlighted line: 

1671 last_selections = w.extraSelections() 

1672 

1673 def after(func: Callable) -> None: 

1674 QtCore.QTimer.singleShot(delay, func) 

1675 

1676 def addFlashCallback(self: Any=self, w: str=w) -> None: 

1677 i = self.flashIndex 

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

1679 cursor.setPosition(i) 

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

1681 extra = w.ExtraSelection() 

1682 extra.cursor = cursor 

1683 if self.flashBg: 

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

1685 if self.flashFg: 

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

1687 self.extraSelList = last_selections[:] 

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

1689 w.setExtraSelections(self.extraSelList) 

1690 self.flashCount -= 1 

1691 after(removeFlashCallback) 

1692 

1693 def removeFlashCallback(self: Any=self, w: Widget=w) -> None: 

1694 w.setExtraSelections(last_selections) 

1695 if self.flashCount > 0: 

1696 after(addFlashCallback) 

1697 else: 

1698 w.setFocus() 

1699 

1700 self.flashCount = flashes 

1701 self.flashIndex = i 

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

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

1704 addFlashCallback() 

1705 

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

1707 def getAllText(self) -> str: 

1708 """QTextEditWrapper.""" 

1709 w = self.widget 

1710 return w.toPlainText() 

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

1712 def getInsertPoint(self) -> int: 

1713 """QTextEditWrapper.""" 

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

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

1716 def getSelectionRange(self, sort: bool=True) -> Tuple[int, int]: 

1717 """QTextEditWrapper.""" 

1718 w = self.widget 

1719 tc = w.textCursor() 

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

1721 return i, j 

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

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

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

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

1726 

1727 def getXScrollPosition(self) -> int: 

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

1729 w = self.widget 

1730 sb = w.horizontalScrollBar() 

1731 pos = sb.sliderPosition() 

1732 return pos 

1733 

1734 def getYScrollPosition(self) -> int: 

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

1736 w = self.widget 

1737 sb = w.verticalScrollBar() 

1738 pos = sb.sliderPosition() 

1739 return pos 

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

1741 def hasSelection(self) -> bool: 

1742 """QTextEditWrapper.""" 

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

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

1745 def insert(self, i: Index, s: str) -> None: 

1746 """QTextEditWrapper.""" 

1747 w = self.widget 

1748 i = self.toPythonIndex(i) 

1749 cursor = w.textCursor() 

1750 try: 

1751 self.changingText = True # Disable onTextChanged. 

1752 cursor.setPosition(i) 

1753 cursor.insertText(s) 

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

1755 finally: 

1756 self.changingText = False 

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

1758 def leoMoveCursorHelper(self, kind: str, extend: bool=False, linesPerPage: int=15) -> None: 

1759 """QTextEditWrapper.""" 

1760 w = self.widget 

1761 d = { 

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

1763 'down': MoveOperation.Down, 

1764 'end': MoveOperation.End, 

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

1766 'exchange': True, # Dummy. 

1767 'home': MoveOperation.Start, 

1768 'left': MoveOperation.Left, 

1769 'page-down': MoveOperation.Down, 

1770 'page-up': MoveOperation.Up, 

1771 'right': MoveOperation.Right, 

1772 'up': MoveOperation.Up, 

1773 } 

1774 kind = kind.lower() 

1775 op = d.get(kind) 

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

1777 if not op: 

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

1779 return 

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

1781 self.pageUpDown(op, mode) 

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

1783 cursor = w.textCursor() 

1784 anchor = cursor.anchor() 

1785 pos = cursor.position() 

1786 cursor.setPosition(pos, MoveOperation.MoveAnchor) 

1787 cursor.setPosition(anchor, MoveOperation.KeepAnchor) 

1788 w.setTextCursor(cursor) 

1789 else: 

1790 if not extend: 

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

1792 cursor = w.textCursor() 

1793 cursor.clearSelection() 

1794 w.setTextCursor(cursor) 

1795 w.moveCursor(op, mode) 

1796 self.seeInsertPoint() 

1797 self.rememberSelectionAndScroll() 

1798 # #218. 

1799 cursor = w.textCursor() 

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

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

1802 g.app.gui.setClipboardSelection(sel) 

1803 self.c.frame.updateStatusLine() 

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

1805 def pageUpDown(self, op: Any, moveMode: Any) -> None: 

1806 """ 

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

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

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

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

1811 gui/widgets/qtextedit.cpp. 

1812 """ 

1813 control = self.widget 

1814 cursor = control.textCursor() 

1815 moved = False 

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

1817 distance = 0 

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

1819 while True: 

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

1821 distance += abs(y - lastY) 

1822 lastY = y 

1823 moved = cursor.movePosition(op, moveMode) 

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

1825 break 

1826 sb = control.verticalScrollBar() 

1827 if moved: 

1828 if op == MoveOperation.Up: 

1829 cursor.movePosition(MoveOperation.Down, moveMode) 

1830 sb.triggerAction(SliderAction.SliderPageStepSub) 

1831 else: 

1832 cursor.movePosition(MoveOperation.Up, moveMode) 

1833 sb.triggerAction(SliderAction.SliderPageStepAdd) 

1834 control.setTextCursor(cursor) 

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

1836 def linesPerPage(self) -> float: 

1837 """QTextEditWrapper.""" 

1838 # Not used in Leo's core. 

1839 w = self.widget 

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

1841 lineSpacing = w.fontMetrics().lineSpacing() 

1842 n = h / lineSpacing 

1843 return n 

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

1845 def scrollDelegate(self, kind: str) -> None: 

1846 """ 

1847 Scroll a QTextEdit up or down one page. 

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

1849 """ 

1850 c = self.c 

1851 w = self.widget 

1852 vScroll = w.verticalScrollBar() 

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

1854 lineSpacing = w.fontMetrics().lineSpacing() 

1855 n = h / lineSpacing 

1856 n = max(2, n - 3) 

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

1858 delta = n / 2 

1859 elif kind == 'down-line': 

1860 delta = 1 

1861 elif kind == 'down-page': 

1862 delta = n 

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

1864 delta = -n / 2 

1865 elif kind == 'up-line': 

1866 delta = -1 

1867 elif kind == 'up-page': 

1868 delta = -n 

1869 else: 

1870 delta = 0 

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

1872 val = vScroll.value() 

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

1874 c.bodyWantsFocus() 

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

1876 def see(self, see_i: int) -> None: 

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

1878 w = self.widget 

1879 tc = w.textCursor() 

1880 # Put see_i in range. 

1881 s = self.getAllText() 

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

1883 # Remember the old cursor 

1884 old_cursor = QtGui.QTextCursor(tc) 

1885 # Scroll so that see_i is visible. 

1886 tc.setPosition(see_i) 

1887 w.setTextCursor(tc) 

1888 w.ensureCursorVisible() 

1889 # Restore the old cursor 

1890 w.setTextCursor(old_cursor) 

1891 

1892 def seeInsertPoint(self) -> None: 

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

1894 self.widget.ensureCursorVisible() 

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

1896 def setAllText(self, s: str) -> None: 

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

1898 w = self.widget 

1899 try: 

1900 self.changingText = True # Disable onTextChanged. 

1901 w.setReadOnly(False) 

1902 w.setPlainText(s) 

1903 finally: 

1904 self.changingText = False 

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

1906 def setInsertPoint(self, i: Index, s: str=None) -> None: 

1907 # Fix bug 981849: incorrect body content shown. 

1908 # Use the more careful code in setSelectionRange. 

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

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

1911 def setSelectionRange(self, i: Index, j: int, insert: Index=None, s: str=None) -> None: 

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

1913 # 

1914 # Part 1 

1915 w = self.widget 

1916 i = self.toPythonIndex(i) 

1917 j = self.toPythonIndex(j) 

1918 if s is None: 

1919 s = self.getAllText() 

1920 n = len(s) 

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

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

1923 if insert is None: 

1924 ins = max(i, j) 

1925 else: 

1926 ins = self.toPythonIndex(insert) 

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

1928 # 

1929 # Part 2: 

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

1931 # Using tc.movePosition doesn't work. 

1932 tc = w.textCursor() 

1933 if i == j: 

1934 tc.setPosition(i) 

1935 elif ins == j: 

1936 # Put the insert point at j 

1937 tc.setPosition(i) 

1938 tc.setPosition(j, MoveMode.KeepAnchor) 

1939 elif ins == i: 

1940 # Put the insert point at i 

1941 tc.setPosition(j) 

1942 tc.setPosition(i, MoveMode.KeepAnchor) 

1943 else: 

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

1945 tc.setPosition(j) 

1946 tc.setPosition(i, MoveMode.KeepAnchor) 

1947 w.setTextCursor(tc) 

1948 # #218. 

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

1950 if s[i:j]: 

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

1952 # 

1953 # Remember the values for v.restoreCursorAndScroll. 

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

1955 v.insertSpot = ins 

1956 if i > j: 

1957 i, j = j, i 

1958 assert i <= j 

1959 v.selectionStart = i 

1960 v.selectionLength = j - i 

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

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

1963 def setXScrollPosition(self, pos: Index) -> None: 

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

1965 if pos is not None: 

1966 w = self.widget 

1967 sb = w.horizontalScrollBar() 

1968 sb.setSliderPosition(pos) 

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

1970 def setYScrollPosition(self, pos: Index) -> None: 

1971 """Set the vertical scrollbar position.""" 

1972 if pos is not None: 

1973 w = self.widget 

1974 sb = w.verticalScrollBar() 

1975 sb.setSliderPosition(pos) 

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

1977 def toPythonIndex(self, index: Index, s: str=None) -> int: 

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

1979 w = self 

1980 te = self.widget 

1981 if index is None: 

1982 return 0 

1983 if isinstance(index, int): 

1984 return index 

1985 if index == '1.0': 

1986 return 0 

1987 if index == 'end': 

1988 return w.getLastPosition() 

1989 doc = te.document() 

1990 data = index.split('.') 

1991 if len(data) == 2: 

1992 row1, col1 = data 

1993 row, col = int(row1), int(col1) 

1994 bl = doc.findBlockByNumber(row - 1) 

1995 return bl.position() + col 

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

1997 return 0 

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

1999 def toPythonIndexRowCol(self, index: int) -> Tuple[int, int, int]: 

2000 w = self 

2001 if index == '1.0': 

2002 return 0, 0, 0 

2003 if index == 'end': 

2004 index = w.getLastPosition() 

2005 te = self.widget 

2006 doc = te.document() 

2007 i = w.toPythonIndex(index) 

2008 bl = doc.findBlock(i) 

2009 row = bl.blockNumber() 

2010 col = i - bl.position() 

2011 return i, row, col 

2012 #@-others 

2013#@-others 

2014 

2015#@@language python 

2016#@@tabwidth -4 

2017#@@pagewidth 70 

2018#@-leo