Coverage for C:\Repos\leo-editor\leo\plugins\qt_tree.py: 15%

957 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.20140907131341.18707: * @file ../plugins/qt_tree.py 

4#@@first 

5"""Leo's Qt tree class.""" 

6#@+<< imports: qt_tree.py >> 

7#@+node:ekr.20140907131341.18709: ** << imports: qt_tree.py >> 

8import re 

9import time 

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

11from typing import TYPE_CHECKING 

12from leo.core.leoQt import isQt6, QtCore, QtGui, QtWidgets 

13from leo.core.leoQt import EndEditHint, Format, ItemFlag, KeyboardModifier 

14from leo.core import leoGlobals as g 

15from leo.core import leoFrame 

16from leo.core import leoNodes 

17from leo.core import leoPlugins # Uses leoPlugins.TryNext. 

18from leo.plugins import qt_text 

19#@-<< imports: qt_tree.py >> 

20#@+<< type aliases: qt_tree.py >> 

21#@+node:ekr.20220417193741.1: ** << type aliases: qt_tree.py >> 

22if TYPE_CHECKING: # Always False at runtime. 

23 from leo.core.leoCommands import Commands as Cmdr 

24 from leo.core.leoNodes import Position as Pos 

25 from leo.core.leoNodes import VNode 

26else: 

27 Cmdr = Any 

28 Pos = Any 

29 VNode = Any 

30Editor = Any 

31Event = Any 

32Icon = Any 

33Item = Any 

34Selection = Tuple[int, int, int] 

35Widget = Any 

36Wrapper = Any 

37#@-<< type aliases: qt_tree.py >> 

38#@+others 

39#@+node:ekr.20160514120051.1: ** class LeoQtTree 

40class LeoQtTree(leoFrame.LeoTree): 

41 """Leo Qt tree class""" 

42 #@+others 

43 #@+node:ekr.20110605121601.18404: *3* qtree.Birth 

44 #@+node:ekr.20110605121601.18405: *4* qtree.__init__ 

45 def __init__(self, c: Cmdr, frame: Wrapper) -> None: 

46 """Ctor for the LeoQtTree class.""" 

47 super().__init__(frame) 

48 self.c = c 

49 # Widget independent status ivars... 

50 self.prev_v = None 

51 self.redrawCount = 0 # Count for debugging. 

52 self.revertHeadline = None # Previous headline text for abortEditLabel. 

53 self.busy = False 

54 # Debugging... 

55 self.traceCallersFlag = False # Enable traceCallers method. 

56 # Associating items with position and vnodes... 

57 self.items: List[Item] = [] 

58 self.item2positionDict: Dict[str, Pos] = {} # Keys are gnxs. 

59 self.item2vnodeDict: Dict[str, VNode] = {} # Keys are gnxs. 

60 self.nodeIconsDict: Dict[str, List[Icon]] = {} # keys are gnxs, values are declutter generated icons 

61 self.position2itemDict: Dict[str, Item] = {} # Keys are gnxs. 

62 self.vnode2itemsDict: Dict[VNode, List[Item]] = {} # values are lists of items. 

63 self.editWidgetsDict: Dict[Editor, Wrapper] = {} # keys are native edit widgets, values are wrappers. 

64 self.reloadSettings() 

65 # Components. 

66 self.canvas: Wrapper = self # An official ivar used by Leo's core. 

67 self.headlineWrapper: Wrapper = qt_text.QHeadlineWrapper # This is a class. 

68 # w is a LeoQTreeWidget, a subclass of QTreeWidget. 

69 self.treeWidget: Widget = frame.top.treeWidget # An internal ivar. 

70 w = self.treeWidget 

71 # 

72 # "declutter", node appearance tweaking 

73 self.declutter_patterns: List[Any] = None # list of pairs of patterns for decluttering 

74 self.declutter_data: Dict[Any, Any] = {} 

75 self.loaded_images: Dict[str, Icon] = {} 

76 if 0: # Drag and drop 

77 w.setDragEnabled(True) 

78 w.viewport().setAcceptDrops(True) 

79 w.showDropIndicator = True 

80 w.setAcceptDrops(True) 

81 w.setDragDropMode(w.InternalMove) 

82 if 1: # Does not work 

83 

84 def dropMimeData(self, data: str, action: str, row: str, col: str, parent: str) -> None: 

85 g.trace() 

86 

87 # w.dropMimeData = dropMimeData 

88 

89 def mimeData(self, indexes: str) -> None: 

90 g.trace() 

91 

92 # Early inits... 

93 

94 try: 

95 w.headerItem().setHidden(True) 

96 except Exception: 

97 pass 

98 n = c.config.getInt('icon-height') or 16 

99 w.setIconSize(QtCore.QSize(160, n)) 

100 #@+node:ekr.20110605121601.17866: *4* qtree.get_name 

101 def getName(self) -> str: 

102 """Return the name of this widget: must start with "canvas".""" 

103 return 'canvas(tree)' 

104 #@+node:ekr.20110605121601.18406: *4* qtree.initAfterLoad 

105 def initAfterLoad(self) -> None: 

106 """Do late-state inits.""" 

107 # Called by Leo's core. 

108 c = self.c 

109 # w = c.frame.top 

110 tw = self.treeWidget 

111 tw.itemDoubleClicked.connect(self.onItemDoubleClicked) 

112 tw.itemClicked.connect(self.onItemClicked) 

113 tw.itemSelectionChanged.connect(self.onTreeSelect) 

114 tw.itemCollapsed.connect(self.onItemCollapsed) 

115 tw.itemExpanded.connect(self.onItemExpanded) 

116 tw.customContextMenuRequested.connect(self.onContextMenu) 

117 # tw.onItemChanged.connect(self.onItemChanged) 

118 g.app.gui.setFilter(c, tw, self, tag='tree') 

119 # 2010/01/24: Do not set this here. 

120 # The read logic sets c.changed to indicate nodes have changed. 

121 # c.clearChanged() 

122 #@+node:ekr.20110605121601.17871: *4* qtree.reloadSettings 

123 def reloadSettings(self) -> None: 

124 """LeoQtTree.""" 

125 c = self.c 

126 self.auto_edit = c.config.getBool('single-click-auto-edits-headline', False) 

127 self.enable_drag_messages = c.config.getBool("enable-drag-messages") 

128 self.select_all_text_when_editing_headlines = c.config.getBool( 

129 'select_all_text_when_editing_headlines') 

130 self.stayInTree = c.config.getBool('stayInTreeAfterSelect') 

131 self.use_chapters = c.config.getBool('use-chapters') 

132 self.use_declutter = c.config.getBool('tree-declutter', default=False) 

133 #@+node:ekr.20110605121601.17940: *4* qtree.wrapQLineEdit 

134 def wrapQLineEdit(self, w: Wrapper) -> Wrapper: 

135 """A wretched kludge for MacOs k.masterMenuHandler.""" 

136 c = self.c 

137 if isinstance(w, QtWidgets.QLineEdit): 

138 wrapper = self.edit_widget(c.p) 

139 else: 

140 wrapper = w 

141 return wrapper 

142 #@+node:ekr.20110605121601.17868: *3* qtree.Debugging & tracing 

143 def error(self, s: str) -> None: 

144 if not g.unitTesting: 

145 g.trace('LeoQtTree Error: ', s, g.callers()) 

146 

147 def traceItem(self, item: Item) -> str: 

148 if item: 

149 # A QTreeWidgetItem. 

150 return f"item {id(item)}: {self.getItemText(item)}" 

151 return '<no item>' 

152 

153 def traceCallers(self) -> str: 

154 if self.traceCallersFlag: 

155 return g.callers(5, excludeCaller=True) 

156 return '' 

157 #@+node:ekr.20110605121601.17872: *3* qtree.Drawing 

158 #@+node:ekr.20110605121601.18408: *4* qtree.clear 

159 def clear(self) -> None: 

160 """Clear all widgets in the tree.""" 

161 w = self.treeWidget 

162 w.clear() 

163 #@+node:ekr.20180810052056.1: *4* qtree.drawVisible & helpers (not used) 

164 def drawVisible(self, p: Pos) -> None: 

165 """ 

166 Add only the visible nodes to the outline. 

167 

168 Not used, as this causes scrolling issues. 

169 """ 

170 t1 = time.process_time() 

171 c = self.c 

172 parents: List[Any] = [] 

173 # Clear the widget. 

174 w = self.treeWidget 

175 w.clear() 

176 # Clear the dicts. 

177 self.initData() 

178 if c.hoistStack: 

179 first_p = c.hoistStack[-1].p 

180 target_p = first_p.nodeAfterTree().visBack(c) 

181 else: 

182 first_p = c.rootPosition() 

183 target_p = None 

184 n = 0 

185 for p in self.yieldVisible(first_p, target_p): 

186 n += 1 

187 level = p.level() 

188 parent_item = w if level == 0 else parents[level - 1] 

189 item = QtWidgets.QTreeWidgetItem(parent_item) 

190 item.setFlags(item.flags() | ItemFlag.ItemIsEditable) 

191 item.setChildIndicatorPolicy( 

192 item.ShowIndicator if p.hasChildren() 

193 else item.DontShowIndicator) 

194 item.setExpanded(bool(p.hasChildren() and p.isExpanded())) 

195 self.items.append(item) 

196 # Update parents. 

197 parents = [] if level == 0 else parents[:level] 

198 parents.append(item) 

199 # Update the dicts. 

200 itemHash = self.itemHash(item) 

201 self.item2positionDict[itemHash] = p.copy() 

202 self.item2vnodeDict[itemHash] = p.v 

203 self.position2itemDict[p.key()] = item 

204 d = self.vnode2itemsDict 

205 v = p.v 

206 aList = d.get(v, []) 

207 aList.append(item) 

208 d[v] = aList 

209 # Enter the headline. 

210 item.setText(0, p.h) 

211 if self.use_declutter: 

212 item._real_text = p.h 

213 # Draw the icon. 

214 v.iconVal = v.computeIcon() 

215 icon = self.getCompositeIconImage(p, v.iconVal) 

216 if icon: 

217 self.setItemIcon(item, icon) 

218 # Set current item. 

219 if p == c.p: 

220 w.setCurrentItem(item) 

221 # Useful, for now. 

222 t2 = time.process_time() 

223 if t2 - t1 > 0.1: 

224 g.trace(f"{n} nodes, {t2 - t1:5.2f} sec") 

225 #@+node:ekr.20180810052056.2: *5* qtree.yieldVisible (not used) 

226 def yieldVisible(self, first_p: Pos, target_p: Pos=None) -> Generator: 

227 """ 

228 A generator yielding positions from first_p to target_p. 

229 """ 

230 c = self.c 

231 p = first_p.copy() 

232 yield p 

233 while p: 

234 if p == target_p: 

235 return 

236 v = p.v 

237 if (v.children and ( 

238 # Use slower test for clones: 

239 len(v.parents) > 1 and p in v.expandedPositions or 

240 # Use a quick test for non-clones: 

241 len(v.parents) <= 1 and (v.statusBits & v.expandedBit) != 0 

242 )): 

243 # p.moveToFirstChild() 

244 p.stack.append((v, p._childIndex),) 

245 p.v = v.children[0] 

246 p._childIndex = 0 

247 yield p 

248 continue 

249 # if p.hasNext(): 

250 parent_v = p.stack[-1][0] if p.stack else c.hiddenRootNode 

251 if p._childIndex + 1 < len(parent_v.children): 

252 # p.moveToNext() 

253 p._childIndex += 1 

254 p.v = parent_v.children[p._childIndex] 

255 yield p 

256 continue 

257 # 

258 # A fast version of p.moveToThreadNext(). 

259 # We look for a parent with a following sibling. 

260 while p.stack: 

261 # p.moveToParent() 

262 p.v, p._childIndex = p.stack.pop() 

263 # if p.hasNext(): 

264 parent_v = p.stack[-1][0] if p.stack else c.hiddenRootNode 

265 if p._childIndex + 1 < len(parent_v.children): 

266 # p.moveToNext() 

267 p._childIndex += 1 

268 p.v = parent_v.children[p._childIndex] 

269 break # Found: moveToThreadNext() 

270 else: 

271 break # Not found. 

272 # Found moveToThreadNext() 

273 yield p 

274 continue 

275 if target_p: 

276 g.trace('NOT FOUND:', target_p.h) 

277 #@+node:ekr.20180810052056.3: *5* qtree.slowYieldVisible 

278 def slowYieldVisible(self, first_p: Pos, target_p: Pos=None) -> Generator: 

279 """ 

280 A generator yielding positions from first_p to target_p. 

281 """ 

282 c = self.c 

283 p = first_p.copy() 

284 while p: 

285 yield p 

286 if p == target_p: 

287 return 

288 p.moveToVisNext(c) 

289 if target_p: 

290 g.trace('NOT FOUND:', target_p.h) 

291 #@+node:ekr.20110605121601.17873: *4* qtree.full_redraw & helpers 

292 def full_redraw(self, p: Pos=None) -> Pos: 

293 """ 

294 Redraw all visible nodes of the tree. 

295 Preserve the vertical scrolling unless scroll is True. 

296 """ 

297 c = self.c 

298 if g.app.disable_redraw: 

299 return None 

300 if self.busy: 

301 return None 

302 # Cancel the delayed redraw request. 

303 c.requestLaterRedraw = False 

304 if not p: 

305 p = c.currentPosition() 

306 elif c.hoistStack and p.h.startswith('@chapter') and p.hasChildren(): 

307 # Make sure the current position is visible. 

308 # Part of fix of bug 875323: Hoist an @chapter node leaves a non-visible node selected. 

309 p = p.firstChild() 

310 c.frame.tree.select(p) 

311 c.setCurrentPosition(p) 

312 else: 

313 c.setCurrentPosition(p) 

314 assert not self.busy, g.callers() 

315 self.redrawCount += 1 

316 self.initData() 

317 try: 

318 self.busy = True 

319 self.drawTopTree(p) 

320 finally: 

321 self.busy = False 

322 self.setItemForCurrentPosition() 

323 return p # Return the position, which may have changed. 

324 

325 # Compatibility 

326 

327 # mypy complains that there is a mismatch with the base redraw method. 

328 redraw = full_redraw # type:ignore 

329 redraw_now = full_redraw #type:ignore 

330 #@+node:vitalije.20200329160945.1: *5* tree declutter code 

331 #@+node:tbrown.20150807090639.1: *6* qtree.declutter_node & helpers 

332 def declutter_node(self, c: Cmdr, p: Pos, item: Item) -> Icon: 

333 """declutter_node - change the appearance of a node 

334 

335 :param commander c: commander containing node 

336 :param position p: position of node 

337 :param QWidgetItem item: tree node widget item 

338 

339 returns composite icon for this node 

340 """ 

341 dd = self.declutter_data 

342 iconVal = p.v.computeIcon() 

343 iconName = f'box{iconVal:02d}.png' 

344 loaded_images = self.loaded_images 

345 #@+others 

346 #@+node:vitalije.20200329153544.1: *7* sorted_icons 

347 def sorted_icons(p: Pos) -> List[str]: 

348 """ 

349 Returns a list of icon filenames for this node. 

350 The list is sorted to owner the 'where' key of image dicts. 

351 """ 

352 icons = c.editCommands.getIconList(p) 

353 a = [x['file'] for x in icons if x['where'] == 'beforeIcon'] 

354 a.append(iconName) 

355 a.extend(x['file'] for x in icons if x['where'] == 'beforeHeadline') 

356 return a 

357 #@+node:ekr.20171122064635.1: *7* declutter_replace 

358 def declutter_replace(arg: str, cmd: Callable) -> Tuple[Callable, str]: 

359 """ 

360 Executes cmd if cmd is any replace command and returns 

361 pair (commander, s), where 'commander' corresponds 

362 to the executed replacement operation, 's' is the substituted string. 

363 If cmd is not a replacement command returns (None, None) 

364 """ 

365 # pylint: disable=undefined-loop-variable 

366 

367 replacement, s = None, None 

368 

369 if cmd == 'REPLACE': 

370 s = pattern.sub(arg, text) 

371 elif cmd == 'REPLACE-HEAD': 

372 s = text[: m.start()].rstrip() 

373 elif cmd == 'REPLACE-TAIL': 

374 s = text[m.end() :].lstrip() 

375 elif cmd == 'REPLACE-REST': 

376 s = (text[:m.start] + text[m.end() :]).strip() 

377 

378 # 's' is string when 'cmd' is recognised 

379 # and is None otherwise 

380 if isinstance(s, str): 

381 # Save the operation 

382 replacement = lambda item, s: item.setText(0, s) 

383 # ... and apply it 

384 replacement(item, s) 

385 

386 return replacement, s 

387 #@+node:ekr.20171122055719.1: *7* declutter_style 

388 def declutter_style(arg: str, cmd: Callable) -> Tuple[Callable, str]: 

389 """ 

390 Handles style options and returns pair '(commander, param)', 

391 where 'commander' is the applied style-modifying operation, 

392 param - the saved argument of that operation. 

393 Returns (None, param) if 'cmd' is not a style option. 

394 """ 

395 # pylint: disable=function-redefined 

396 param = c.styleSheetManager.expand_css_constants(arg).split()[0] 

397 modifier: Callable = None 

398 if cmd == 'ICON': 

399 def modifier(item: Item, param: str) -> None: 

400 # Does not fit well this function. And we cannot 

401 # wrap list 'new_icons' in a saved argument as 

402 # the list is recreated before each call. 

403 new_icons.append(param) 

404 elif cmd == 'BG': 

405 def modifier(item: Item, param: str) -> None: 

406 item.setBackground(0, QtGui.QBrush(QtGui.QColor(param))) 

407 elif cmd == 'FG': 

408 def modifier(item: Item, param: str) -> None: 

409 item.setForeground(0, QtGui.QBrush(QtGui.QColor(param))) 

410 elif cmd == 'FONT': 

411 def modifier(item: Item, param: str) -> None: 

412 item.setFont(0, QtGui.QFont(param)) 

413 elif cmd == 'ITALIC': 

414 def modifier(item: Item, param: str) -> None: 

415 font = item.font(0) 

416 font.setItalic(bool(int(param))) 

417 item.setFont(0, font) 

418 elif cmd == 'WEIGHT': 

419 def modifier(item: Item, param: str) -> None: 

420 arg = getattr(QtGui.QFont, param, 75) 

421 font = item.font(0) 

422 font.setWeight(arg) 

423 item.setFont(0, font) 

424 elif cmd == 'PX': 

425 def modifier(item: Item, param: str) -> None: 

426 font = item.font(0) 

427 font.setPixelSize(int(param)) 

428 item.setFont(0, font) 

429 elif cmd == 'PT': 

430 def modifier(item: Item, param: str) -> None: 

431 font = item.font(0) 

432 font.setPointSize(int(param)) 

433 item.setFont(0, font) 

434 # Apply the style update 

435 if modifier: 

436 modifier(item, param) 

437 return modifier, param 

438 #@+node:vitalije.20200327163522.1: *7* apply_declutter_rules 

439 def apply_declutter_rules(cmds: List[Tuple[Callable, str]]) -> List[Any]: 

440 """ 

441 Applies all commands for the matched rule. Returns the list 

442 of the applied operations paired with their single parameter. 

443 """ 

444 modifiers = [] 

445 for cmd, arg in cmds: 

446 modifier, param = declutter_replace(arg, cmd) 

447 if not modifier: 

448 modifier, param = declutter_style(arg, cmd) 

449 if modifier: 

450 modifiers.append((modifier, param)) 

451 return modifiers 

452 #@+node:vitalije.20200329162015.1: *7* preload_images 

453 def preload_images() -> None: 

454 for f in new_icons: 

455 if f not in loaded_images: 

456 loaded_images[f] = g.app.gui.getImageImage(f) 

457 #@-others 

458 if (p.h, iconVal) in dd: 

459 # Apply saved adjustments to the text and to the _style_ 

460 # of the node 

461 new_icons, modifiers_and_args = dd[(p.h, iconVal)] 

462 for modifier, arg in modifiers_and_args: 

463 modifier(item, arg) 

464 

465 new_icons = sorted_icons(p) + new_icons 

466 else: 

467 text = p.h 

468 new_icons = [] 

469 modifiers_and_args = [] 

470 for pattern, cmds in self.get_declutter_patterns(): 

471 m = pattern.match(text) or pattern.search(text) 

472 if m: 

473 modifiers_and_args.extend(apply_declutter_rules(cmds)) 

474 

475 # Save the lists of the icons and the adjusting operations 

476 # for future reuse. 

477 dd[(p.h, iconVal)] = new_icons, modifiers_and_args 

478 new_icons = sorted_icons(p) + new_icons 

479 preload_images() 

480 self.nodeIconsDict[p.gnx] = new_icons 

481 h = ':'.join(new_icons) 

482 icon = g.app.gui.iconimages.get(h) 

483 if not icon: 

484 preload_images() 

485 images = [loaded_images.get(x) for x in new_icons] 

486 icon = self.make_composite_icon(images) 

487 g.app.gui.iconimages[h] = icon 

488 return icon 

489 #@+node:vitalije.20200327162532.1: *6* qtree.get_declutter_patterns 

490 def get_declutter_patterns(self) -> List[Any]: 

491 "Initializes self.declutter_patterns from configuration and returns it" 

492 if self.declutter_patterns is not None: 

493 return self.declutter_patterns 

494 c = self.c 

495 patterns: List[Any] = [] 

496 warned = False 

497 lines = c.config.getData("tree-declutter-patterns") 

498 for line in lines: 

499 try: 

500 cmd, arg = line.split(None, 1) 

501 except ValueError: 

502 # Allow empty arg, and guard against user errors. 

503 cmd = line.strip() 

504 arg = '' 

505 if cmd.startswith('#'): 

506 pass 

507 elif cmd == 'RULE': 

508 patterns.append((re.compile(arg), [])) 

509 else: 

510 if patterns: 

511 patterns[-1][1].append((cmd, arg)) 

512 elif not warned: 

513 warned = True 

514 g.log('Declutter patterns must start with RULE*', 

515 color='error') 

516 self.declutter_patterns = patterns 

517 return patterns 

518 #@+node:ekr.20110605121601.17874: *5* qtree.drawChildren 

519 def drawChildren(self, p: Pos, parent_item: Item) -> None: 

520 """Draw the children of p if they should be expanded.""" 

521 if not p: 

522 g.trace('can not happen: no p') 

523 return 

524 if p.hasChildren(): 

525 if p.isExpanded(): 

526 self.expandItem(parent_item) 

527 child = p.firstChild() 

528 while child: 

529 self.drawTree(child, parent_item) 

530 child.moveToNext() 

531 else: 

532 # Draw the hidden children. 

533 child = p.firstChild() 

534 while child: 

535 self.drawNode(child, parent_item) 

536 child.moveToNext() 

537 self.contractItem(parent_item) 

538 else: 

539 self.contractItem(parent_item) 

540 #@+node:ekr.20110605121601.17875: *5* qtree.drawNode 

541 def drawNode(self, p: Pos, parent_item: Item) -> Item: 

542 """Draw the node p.""" 

543 c = self.c 

544 v = p.v 

545 # Allocate the QTreeWidgetItem. 

546 item = self.createTreeItem(p, parent_item) 

547 # Update the data structures. 

548 itemHash = self.itemHash(item) 

549 self.position2itemDict[p.key()] = item 

550 self.item2positionDict[itemHash] = p.copy() # was item 

551 self.item2vnodeDict[itemHash] = v # was item 

552 d = self.vnode2itemsDict 

553 aList = d.get(v, []) 

554 if item not in aList: 

555 aList.append(item) 

556 d[v] = aList 

557 # Set the headline and maybe the icon. 

558 self.setItemText(item, p.h) 

559 # #1310: Add a tool tip. 

560 item.setToolTip(0, p.h) 

561 if self.use_declutter: 

562 icon = self.declutter_node(c, p, item) 

563 if icon: 

564 item.setIcon(0, icon) 

565 return item 

566 # Draw the icon. 

567 v.iconVal = v.computeIcon() 

568 # **Slow**, but allows per-vnode icons. 

569 icon = self.getCompositeIconImage(p, v.iconVal) 

570 if icon: 

571 item.setIcon(0, icon) 

572 return item 

573 #@+node:ekr.20110605121601.17876: *5* qtree.drawTopTree 

574 def drawTopTree(self, p: Pos) -> None: 

575 """Draw the tree rooted at p.""" 

576 trace = 'drawing' in g.app.debug and not g.unitTesting 

577 if trace: 

578 t1 = time.process_time() 

579 c = self.c 

580 self.clear() 

581 # Draw all top-level nodes and their visible descendants. 

582 if c.hoistStack: 

583 bunch = c.hoistStack[-1] 

584 p = bunch.p 

585 h = p.h 

586 if len(c.hoistStack) == 1 and h.startswith('@chapter') and p.hasChildren(): 

587 p = p.firstChild() 

588 while p: 

589 self.drawTree(p) 

590 p.moveToNext() 

591 else: 

592 self.drawTree(p) 

593 else: 

594 p = c.rootPosition() 

595 while p: 

596 self.drawTree(p) 

597 p.moveToNext() 

598 if trace: 

599 t2 = time.process_time() 

600 g.trace(f"{t2 - t1:5.2f} sec.", g.callers(5)) 

601 #@+node:ekr.20110605121601.17877: *5* qtree.drawTree 

602 def drawTree(self, p: Pos, parent_item: Item=None) -> None: 

603 if g.app.gui.isNullGui: 

604 return 

605 # Draw the (visible) parent node. 

606 item = self.drawNode(p, parent_item) 

607 # Draw all the visible children. 

608 self.drawChildren(p, parent_item=item) 

609 #@+node:ekr.20110605121601.17878: *5* qtree.initData 

610 def initData(self) -> None: 

611 self.item2positionDict = {} 

612 self.item2vnodeDict = {} 

613 self.position2itemDict = {} 

614 self.vnode2itemsDict = {} 

615 self.editWidgetsDict = {} 

616 #@+node:ekr.20110605121601.17880: *4* qtree.redraw_after_contract 

617 def redraw_after_contract(self, p: Pos) -> None: 

618 

619 if self.busy: 

620 return 

621 self.update_expansion(p) 

622 #@+node:ekr.20110605121601.17881: *4* qtree.redraw_after_expand 

623 def redraw_after_expand(self, p: Pos) -> None: 

624 

625 if 0: # Does not work. Newly visible nodes do not show children correctly. 

626 c = self.c 

627 c.selectPosition(p) 

628 self.update_expansion(p) 

629 else: 

630 self.full_redraw(p) # Don't try to shortcut this! 

631 #@+node:ekr.20110605121601.17882: *4* qtree.redraw_after_head_changed 

632 def redraw_after_head_changed(self) -> None: 

633 """Redraw all Qt outline items cloned to c.p.""" 

634 if self.busy: 

635 return 

636 p = self.c.p 

637 if p: 

638 h = p.h # 2010/02/09: Fix bug 518823. 

639 for item in self.vnode2items(p.v): 

640 if self.isValidItem(item): 

641 self.setItemText(item, h) 

642 # Bug fix: 2009/10/06 

643 self.redraw_after_icons_changed() 

644 #@+node:ekr.20110605121601.17883: *4* qtree.redraw_after_icons_changed 

645 def redraw_after_icons_changed(self) -> None: 

646 

647 if self.busy: 

648 return 

649 self.redrawCount += 1 # To keep a unit test happy. 

650 c = self.c 

651 try: 

652 self.busy = True # Suppress call to setHeadString in onItemChanged! 

653 self.getCurrentItem() 

654 for p in c.rootPosition().self_and_siblings(copy=False): 

655 # Updates icons in p and all visible descendants of p. 

656 self.updateVisibleIcons(p) 

657 finally: 

658 self.busy = False 

659 #@+node:ekr.20110605121601.17884: *4* qtree.redraw_after_select 

660 def redraw_after_select(self, p: Pos=None) -> None: 

661 """Redraw the entire tree when an invisible node is selected.""" 

662 if self.busy: 

663 return 

664 self.full_redraw(p) 

665 # c.redraw_after_select calls tree.select indirectly. 

666 # Do not call it again here. 

667 #@+node:ekr.20140907201613.18986: *4* qtree.repaint (not used) 

668 def repaint(self) -> None: 

669 """Repaint the widget.""" 

670 w = self.treeWidget 

671 w.repaint() 

672 w.resizeColumnToContents(0) # 2009/12/22 

673 #@+node:ekr.20180817043619.1: *4* qtree.update_expansion 

674 def update_expansion(self, p: Pos) -> None: 

675 """Update expansion bits for p, including all clones.""" 

676 c = self.c 

677 w = self.treeWidget 

678 expand = c.shouldBeExpanded(p) 

679 if 'drawing' in g.app.debug: 

680 g.trace('expand' if expand else 'contract') 

681 item = self.position2itemDict.get(p.key()) 

682 if p: 

683 try: 

684 # These generate events, which would trigger a full redraw. 

685 self.busy = True 

686 if expand: 

687 w.expandItem(item) 

688 else: 

689 w.collapseItem(item) 

690 finally: 

691 self.busy = False 

692 w.repaint() 

693 else: 

694 g.trace('NO P') 

695 c.redraw() 

696 #@+node:ekr.20110605121601.17885: *3* qtree.Event handlers 

697 #@+node:ekr.20110605121601.17887: *4* qtree.Click Box 

698 #@+node:ekr.20110605121601.17888: *5* qtree.onClickBoxClick 

699 def onClickBoxClick(self, event: Event, p: Pos=None) -> None: 

700 if self.busy: 

701 return 

702 c = self.c 

703 g.doHook("boxclick1", c=c, p=p, event=event) 

704 g.doHook("boxclick2", c=c, p=p, event=event) 

705 c.outerUpdate() 

706 #@+node:ekr.20110605121601.17889: *5* qtree.onClickBoxRightClick 

707 def onClickBoxRightClick(self, event: Event, p: Pos=None) -> None: 

708 if self.busy: 

709 return 

710 c = self.c 

711 g.doHook("boxrclick1", c=c, p=p, event=event) 

712 g.doHook("boxrclick2", c=c, p=p, event=event) 

713 c.outerUpdate() 

714 #@+node:ekr.20110605121601.17890: *5* qtree.onPlusBoxRightClick 

715 def onPlusBoxRightClick(self, event: Event, p: Pos=None) -> None: 

716 if self.busy: 

717 return 

718 c = self.c 

719 g.doHook('rclick-popup', c=c, p=p, event=event, context_menu='plusbox') 

720 c.outerUpdate() 

721 #@+node:ekr.20110605121601.17891: *4* qtree.Icon Box 

722 # For Qt, there seems to be no way to trigger these events. 

723 #@+node:ekr.20110605121601.17892: *5* qtree.onIconBoxClick 

724 def onIconBoxClick(self, event: Event, p: Pos=None) -> None: 

725 if self.busy: 

726 return 

727 c = self.c 

728 g.doHook("iconclick1", c=c, p=p, event=event) 

729 g.doHook("iconclick2", c=c, p=p, event=event) 

730 c.outerUpdate() 

731 #@+node:ekr.20110605121601.17893: *5* qtree.onIconBoxRightClick 

732 def onIconBoxRightClick(self, event: Event, p: Pos=None) -> None: 

733 """Handle a right click in any outline widget.""" 

734 if self.busy: 

735 return 

736 c = self.c 

737 g.doHook("iconrclick1", c=c, p=p, event=event) 

738 g.doHook("iconrclick2", c=c, p=p, event=event) 

739 c.outerUpdate() 

740 #@+node:ekr.20110605121601.17894: *5* qtree.onIconBoxDoubleClick 

741 def onIconBoxDoubleClick(self, event: Event, p: Pos=None) -> None: 

742 if self.busy: 

743 return 

744 c = self.c 

745 if not p: 

746 p = c.p 

747 if not g.doHook("icondclick1", c=c, p=p, event=event): 

748 self.endEditLabel() 

749 self.OnIconDoubleClick(p) # Call the method in the base class. 

750 g.doHook("icondclick2", c=c, p=p, event=event) 

751 c.outerUpdate() 

752 #@+node:ekr.20110605121601.18437: *4* qtree.onContextMenu 

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

754 """LeoQtTree: Callback for customContextMenuRequested events.""" 

755 # #1286. 

756 c, w = self.c, self.treeWidget 

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

758 #@+node:ekr.20110605121601.17896: *4* qtree.onItemClicked 

759 def onItemClicked(self, item: Item, col: int) -> None: # Col not used. 

760 """Handle a click in a BaseNativeTree widget item.""" 

761 # This is called after an item is selected. 

762 if self.busy: 

763 return 

764 c = self.c 

765 try: 

766 self.busy = True 

767 p = self.item2position(item) 

768 if p: 

769 auto_edit = self.prev_v == p.v # #1049. 

770 self.prev_v = p.v 

771 event = None 

772 # 

773 # Careful. We may have switched gui during unit testing. 

774 if hasattr(g.app.gui, 'qtApp'): 

775 mods = g.app.gui.qtApp.keyboardModifiers() 

776 isCtrl = bool(mods & KeyboardModifier.ControlModifier) 

777 # We could also add support for QtConst.ShiftModifier, QtConst.AltModifier 

778 # & QtConst.MetaModifier. 

779 if isCtrl: 

780 if g.doHook("iconctrlclick1", c=c, p=p, event=event) is None: 

781 c.frame.tree.OnIconCtrlClick(p) # Call the base class method. 

782 g.doHook("iconctrlclick2", c=c, p=p, event=event) 

783 else: 

784 # 2014/02/21: generate headclick1/2 instead of iconclick1/2 

785 g.doHook("headclick1", c=c, p=p, event=event) 

786 g.doHook("headclick2", c=c, p=p, event=event) 

787 else: 

788 auto_edit = None 

789 g.trace('*** no p') 

790 # 2011/05/27: click here is like ctrl-g. 

791 c.k.keyboardQuit(setFocus=False) 

792 c.treeWantsFocus() # 2011/05/08: Focus must stay in the tree! 

793 c.outerUpdate() 

794 # 2011/06/01: A second *single* click on a selected node 

795 # enters editing state. 

796 if auto_edit and self.auto_edit: 

797 e, wrapper = self.createTreeEditorForItem(item) 

798 finally: 

799 self.busy = False 

800 #@+node:ekr.20110605121601.17895: *4* qtree.onItemCollapsed 

801 def onItemCollapsed(self, item: Item) -> None: 

802 

803 if self.busy: 

804 return 

805 c = self.c 

806 p = self.item2position(item) 

807 if not p: 

808 self.error('no p') 

809 return 

810 # Do **not** set lockouts here. 

811 # Only methods that actually generate events should set lockouts. 

812 if p.isExpanded(): 

813 p.contract() 

814 c.redraw_after_contract(p) 

815 self.select(p) 

816 c.outerUpdate() 

817 #@+node:ekr.20110605121601.17897: *4* qtree.onItemDoubleClicked 

818 def onItemDoubleClicked(self, item: Item, col: Any) -> None: # col not used. 

819 """Handle a double click in a BaseNativeTree widget item.""" 

820 if self.busy: # Required. 

821 return 

822 c = self.c 

823 try: 

824 self.busy = True 

825 e, wrapper = self.createTreeEditorForItem(item) 

826 if not e: 

827 g.trace('*** no e') 

828 p = self.item2position(item) 

829 # 2011/07/28: End the lockout here, not at the end. 

830 finally: 

831 self.busy = False 

832 if not p: 

833 self.error('no p') 

834 return 

835 # 2014/02/21: generate headddlick1/2 instead of icondclick1/2. 

836 if g.doHook("headdclick1", c=c, p=p, event=None) is None: 

837 c.frame.tree.OnIconDoubleClick(p) # Call the base class method. 

838 g.doHook("headclick2", c=c, p=p, event=None) 

839 c.outerUpdate() 

840 #@+node:ekr.20110605121601.17898: *4* qtree.onItemExpanded 

841 def onItemExpanded(self, item: Item) -> None: 

842 """Handle and tree-expansion event.""" 

843 if self.busy: # Required 

844 return 

845 c = self.c 

846 p = self.item2position(item) 

847 if not p: 

848 self.error('no p') 

849 return 

850 # Do **not** set lockouts here. 

851 # Only methods that actually generate events should set lockouts. 

852 if not p.isExpanded(): 

853 p.expand() 

854 c.redraw_after_expand(p) 

855 self.select(p) 

856 c.outerUpdate() 

857 #@+node:ekr.20110605121601.17899: *4* qtree.onTreeSelect 

858 def onTreeSelect(self) -> None: 

859 """Select the proper position when a tree node is selected.""" 

860 if self.busy: # Required 

861 return 

862 c = self.c 

863 item = self.getCurrentItem() 

864 p = self.item2position(item) 

865 if not p: 

866 self.error(f"no p for item: {item}") 

867 return 

868 # Do **not** set lockouts here. 

869 # Only methods that actually generate events should set lockouts. 

870 self.select(p) # This is a call to LeoTree.select(!!) 

871 c.outerUpdate() 

872 #@+node:ekr.20110605121601.17944: *3* qtree.Focus 

873 def getFocus(self) -> Any: 

874 return g.app.gui.get_focus(self.c) # Bug fix: 2009/6/30 

875 

876 findFocus = getFocus 

877 

878 def setFocus(self) -> None: 

879 g.app.gui.set_focus(self.c, self.treeWidget) 

880 #@+node:ekr.20110605121601.18409: *3* qtree.Icons 

881 #@+node:ekr.20110605121601.18410: *4* qtree.drawIcon 

882 def drawIcon(self, p: Pos) -> None: 

883 """Redraw the icon at p.""" 

884 self.updateIcon(p) 

885 # the following code is wrong. It constructs a new item 

886 # and assignes the icon to it. However this item is never 

887 # added to the treeWidget so it is soon garbage collected 

888 # w = self.treeWidget 

889 # itemOrTree = self.position2item(p) or w 

890 # item = QtWidgets.QTreeWidgetItem(itemOrTree) 

891 # icon = self.getIcon(p) 

892 # self.setItemIcon(item, icon) 

893 #@+node:ekr.20110605121601.18411: *4* qtree.getIcon & helper 

894 def getIcon(self, p: Pos) -> Icon: 

895 """Return the proper icon for position p.""" 

896 if self.use_declutter: 

897 item = self.position2item(p) 

898 return item and self.declutter_node(self.c, p, item) 

899 p.v.iconVal = iv = p.v.computeIcon() 

900 return self.getCompositeIconImage(p, iv) 

901 

902 

903 #@+node:vitalije.20200329153148.1: *5* qtree.icon_filenames_for_node 

904 def icon_filenames_for_node(self, p: Pos, val: int) -> List[str]: 

905 """Prepares and returns a list of icon filenames 

906 related to this node. 

907 """ 

908 nicon = f'box{val:02d}.png' 

909 fnames = self.nodeIconsDict.get(p.gnx) 

910 if not fnames: 

911 icons = self.c.editCommands.getIconList(p) 

912 fnames = [x['file'] for x in icons if x['where'] == 'beforeIcon'] 

913 fnames.append(nicon) 

914 fnames.extend(x['file'] for x in icons if x['where'] == 'beforeHeadline') 

915 self.nodeIconsDict[p.gnx] = fnames 

916 pat = re.compile(r'^box\d\d\.png$') 

917 loaded_images = self.loaded_images 

918 for i, f in enumerate(fnames): 

919 if pat.match(f): 

920 fnames[i] = nicon 

921 self.nodeIconsDict[p.gnx] = fnames 

922 f = nicon 

923 if f not in loaded_images: 

924 loaded_images[f] = g.app.gui.getImageImage(f) 

925 return fnames 

926 #@+node:vitalije.20200329153154.1: *5* qtree.make_composite_icon 

927 def make_composite_icon(self, images: List[Any]) -> Icon: 

928 hsep = self.c.config.getInt('tree-icon-separation') or 0 

929 images = [x for x in images if x] 

930 height = max([i.height() for i in images]) 

931 images = [i.scaledToHeight(height) for i in images] 

932 width = sum([i.width() for i in images]) + hsep * (len(images) - 1) 

933 pix = QtGui.QImage(width, height, Format.Format_ARGB32_Premultiplied) 

934 pix.fill(QtGui.QColor(0, 0, 0, 0).rgba()) # transparent fill, rgbA 

935 # .rgba() call required for Qt4.7, later versions work with straight color 

936 painter = QtGui.QPainter() 

937 if not painter.begin(pix): 

938 print("Failed to init. painter for icon") 

939 # don't return, the code still makes an icon for the cache 

940 # which stops this being called again and again 

941 x = 0 

942 for i in images: 

943 painter.drawPixmap(x, 0, i) 

944 x += i.width() + hsep 

945 painter.end() 

946 return QtGui.QIcon(QtGui.QPixmap.fromImage(pix)) 

947 #@+node:ekr.20110605121601.18412: *5* qtree.getCompositeIconImage 

948 def getCompositeIconImage(self, p: Pos, val: int) -> Icon: 

949 """Get the icon at position p.""" 

950 fnames = self.icon_filenames_for_node(p, val) 

951 h = ':'.join(fnames) 

952 icon = g.app.gui.iconimages.get(h) 

953 loaded_images = self.loaded_images 

954 images = list(map(loaded_images.get, fnames)) 

955 if not icon: 

956 icon = self.make_composite_icon(images) 

957 g.app.gui.iconimages[h] = icon 

958 return icon 

959 #@+node:ekr.20110605121601.17950: *4* qtree.setItemIcon 

960 def setItemIcon(self, item: Item, icon: str) -> None: 

961 

962 valid = item and self.isValidItem(item) 

963 if icon and valid: 

964 # Important: do not set lockouts here. 

965 # This will generate changed events, 

966 # but there is no itemChanged event handler. 

967 item.setIcon(0, icon) 

968 

969 #@+node:ekr.20110605121601.17951: *4* qtree.updateIcon & updateAllIcons 

970 def updateIcon(self, p: Pos) -> None: 

971 """Update p's icon.""" 

972 if not p: 

973 return 

974 val = p.v.computeIcon() 

975 if p.v.iconVal != val: 

976 self.nodeIconsDict.pop(p.gnx, None) 

977 self.getIcon(p) # sets p.v.iconVal 

978 

979 def updateAllIcons(self, p: Pos) -> None: 

980 if not p: 

981 return 

982 self.nodeIconsDict.pop(p.gnx, None) 

983 icon = self.getIcon(p) # sets p.v.iconVal 

984 # Update all cloned items. 

985 items = self.vnode2items(p.v) 

986 for item in items: 

987 self.setItemIcon(item, icon) 

988 #@+node:ekr.20110605121601.17952: *4* qtree.updateVisibleIcons 

989 def updateVisibleIcons(self, p: Pos) -> None: 

990 """Update the icon for p and the icons 

991 for all visible descendants of p.""" 

992 self.updateAllIcons(p) 

993 if p.hasChildren() and p.isExpanded(): 

994 for child in p.children(): 

995 self.updateVisibleIcons(child) 

996 #@+node:ekr.20110605121601.18414: *3* qtree.Items 

997 #@+node:ekr.20110605121601.17943: *4* qtree.item dict getters 

998 def itemHash(self, item: Item) -> str: 

999 return f"{repr(item)} at {str(id(item))}" 

1000 

1001 def item2position(self, item: Item) -> Pos: 

1002 itemHash = self.itemHash(item) 

1003 p = self.item2positionDict.get(itemHash) # was item 

1004 return p 

1005 

1006 def item2vnode(self, item: Item) -> VNode: 

1007 itemHash = self.itemHash(item) 

1008 return self.item2vnodeDict.get(itemHash) # was item 

1009 

1010 def position2item(self, p: Pos) -> Item: 

1011 item = self.position2itemDict.get(p.key()) 

1012 return item 

1013 

1014 def vnode2items(self, v: VNode) -> List[Item]: 

1015 return self.vnode2itemsDict.get(v, []) 

1016 

1017 def isValidItem(self, item: Item) -> bool: 

1018 itemHash = self.itemHash(item) 

1019 return itemHash in self.item2vnodeDict # was item. 

1020 #@+node:ekr.20110605121601.18415: *4* qtree.childIndexOfItem 

1021 def childIndexOfItem(self, item: Item) -> int: 

1022 parent = item and item.parent() 

1023 if parent: 

1024 n = parent.indexOfChild(item) 

1025 else: 

1026 w = self.treeWidget 

1027 n = w.indexOfTopLevelItem(item) 

1028 return n 

1029 #@+node:ekr.20110605121601.18416: *4* qtree.childItems 

1030 def childItems(self, parent_item: Item) -> List[Item]: 

1031 """ 

1032 Return the list of child items of the parent item, 

1033 or the top-level items if parent_item is None. 

1034 """ 

1035 if parent_item: 

1036 n = parent_item.childCount() 

1037 items = [parent_item.child(z) for z in range(n)] 

1038 else: 

1039 w = self.treeWidget 

1040 n = w.topLevelItemCount() 

1041 items = [w.topLevelItem(z) for z in range(n)] 

1042 return items 

1043 #@+node:ekr.20110605121601.18418: *4* qtree.connectEditorWidget & callback 

1044 def connectEditorWidget(self, e: Editor, item: Item) -> Wrapper: 

1045 """ 

1046 Connect QLineEdit e to QTreeItem item. 

1047 

1048 Also callback for when the editor ends. 

1049 

1050 New in Leo 6.4: The callback handles all updates w/o calling onHeadChanged. 

1051 """ 

1052 c, p, u = self.c, self.c.p, self.c.undoer 

1053 #@+others # define the callback. 

1054 #@+node:ekr.20201109043641.1: *5* function: editingFinished_callback 

1055 def editingFinished_callback() -> None: 

1056 """Called when Qt emits the editingFinished signal.""" 

1057 s = e.text() 

1058 i = s.find('\n') 

1059 # Truncate to one line. 

1060 if i > -1: 

1061 s = s[:i] 

1062 # #1310: update the tooltip. 

1063 if p.h != s: 

1064 # Update p.h and handle undo. 

1065 item.setToolTip(0, s) 

1066 undoData = u.beforeChangeHeadline(p) 

1067 p.v.setHeadString(s) # Set v.h *after* calling the undoer's before method. 

1068 if not c.changed: 

1069 c.setChanged() 

1070 # We must recolor the body because 

1071 # the headline may contain directives. 

1072 c.frame.body.recolor(p) 

1073 p.setDirty() 

1074 u.afterChangeHeadline(p, 'Edit Headline', undoData) 

1075 self.redraw_after_head_changed() 

1076 c.outerUpdate() 

1077 #@-others 

1078 if e: 

1079 # Hook up the widget. 

1080 wrapper = self.getWrapper(e, item) 

1081 e.editingFinished.connect(editingFinished_callback) 

1082 return wrapper # 2011/02/12 

1083 g.trace('can not happen: no e') 

1084 return None 

1085 #@+node:ekr.20110605121601.18419: *4* qtree.contractItem & expandItem 

1086 def contractItem(self, item: Item) -> None: 

1087 self.treeWidget.collapseItem(item) 

1088 

1089 def expandItem(self, item: Item) -> None: 

1090 self.treeWidget.expandItem(item) 

1091 #@+node:ekr.20110605121601.18420: *4* qtree.createTreeEditorForItem 

1092 def createTreeEditorForItem(self, item: Item) -> Tuple[Editor, Wrapper]: 

1093 

1094 c = self.c 

1095 w = self.treeWidget 

1096 w.setCurrentItem(item) # Must do this first. 

1097 if self.use_declutter: 

1098 item.setText(0, item._real_text) 

1099 w.editItem(item) 

1100 e = w.itemWidget(item, 0) # e is a QLineEdit 

1101 e.setObjectName('headline') 

1102 wrapper = self.connectEditorWidget(e, item) 

1103 self.sizeTreeEditor(c, e) 

1104 return e, wrapper 

1105 #@+node:ekr.20110605121601.18421: *4* qtree.createTreeItem 

1106 def createTreeItem(self, p: Pos, parent_item: Item) -> Item: 

1107 

1108 w = self.treeWidget 

1109 itemOrTree = parent_item or w 

1110 item = QtWidgets.QTreeWidgetItem(itemOrTree) 

1111 if isQt6: 

1112 item.setFlags(item.flags() | ItemFlag.ItemIsEditable) 

1113 ChildIndicatorPolicy = QtWidgets.QTreeWidgetItem.ChildIndicatorPolicy 

1114 item.setChildIndicatorPolicy(ChildIndicatorPolicy.DontShowIndicatorWhenChildless) # pylint: disable=no-member 

1115 else: 

1116 item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable | item.DontShowIndicatorWhenChildless) 

1117 try: 

1118 g.visit_tree_item(self.c, p, item) 

1119 except leoPlugins.TryNext: 

1120 pass 

1121 return item 

1122 #@+node:ekr.20110605121601.18423: *4* qtree.getCurrentItem 

1123 def getCurrentItem(self) -> Item: 

1124 w = self.treeWidget 

1125 return w.currentItem() 

1126 #@+node:ekr.20110605121601.18424: *4* qtree.getItemText 

1127 def getItemText(self, item: Item) -> str: 

1128 """Return the text of the item.""" 

1129 return item.text(0) if item else '<no item>' 

1130 #@+node:ekr.20110605121601.18425: *4* qtree.getParentItem 

1131 def getParentItem(self, item: Item) -> Item: 

1132 return item and item.parent() 

1133 #@+node:ekr.20110605121601.18426: *4* qtree.getSelectedItems 

1134 def getSelectedItems(self) -> List: 

1135 w = self.treeWidget 

1136 return w.selectedItems() 

1137 #@+node:ekr.20110605121601.18427: *4* qtree.getTreeEditorForItem 

1138 def getTreeEditorForItem(self, item: Item) -> Editor: 

1139 """Return the edit widget if it exists. 

1140 Do *not* create one if it does not exist. 

1141 """ 

1142 w = self.treeWidget 

1143 e = w.itemWidget(item, 0) 

1144 return e 

1145 #@+node:ekr.20110605121601.18428: *4* qtree.getWrapper 

1146 def getWrapper(self, e: Editor, item: Item) -> Wrapper: 

1147 """Return headlineWrapper that wraps e (a QLineEdit).""" 

1148 c = self.c 

1149 if e: 

1150 wrapper = self.editWidgetsDict.get(e) 

1151 if wrapper: 

1152 pass 

1153 else: 

1154 if item: 

1155 # 2011/02/12: item can be None. 

1156 wrapper = self.headlineWrapper(c, item, name='head', widget=e) 

1157 self.editWidgetsDict[e] = wrapper 

1158 return wrapper 

1159 g.trace('no e') 

1160 return None 

1161 #@+node:ekr.20110605121601.18429: *4* qtree.nthChildItem 

1162 def nthChildItem(self, n: int, parent_item: Item) -> Item: 

1163 children = self.childItems(parent_item) 

1164 if n < len(children): 

1165 item = children[n] 

1166 else: 

1167 # This is **not* an error. 

1168 # It simply means that we need to redraw the tree. 

1169 item = None 

1170 return item 

1171 #@+node:ekr.20110605121601.18430: *4* qtree.scrollToItem 

1172 def scrollToItem(self, item: Item) -> None: 

1173 """ 

1174 Scroll the tree widget so that item is visible. 

1175 Leo's core no longer calls this method. 

1176 """ 

1177 w = self.treeWidget 

1178 hPos, vPos = self.getScroll() 

1179 # Fix #265: Erratic scrolling bug. 

1180 # w.PositionAtCenter causes unwanted scrolling. 

1181 w.scrollToItem(item, w.EnsureVisible) 

1182 self.setHScroll(0) # Necessary 

1183 #@+node:ekr.20110605121601.18431: *4* qtree.setCurrentItemHelper 

1184 def setCurrentItemHelper(self, item: Item) -> None: 

1185 w = self.treeWidget 

1186 w.setCurrentItem(item) 

1187 #@+node:ekr.20110605121601.18432: *4* qtree.setItemText 

1188 def setItemText(self, item: Item, s: str) -> None: 

1189 if item: 

1190 item.setText(0, s) 

1191 if self.use_declutter: 

1192 item._real_text = s 

1193 #@+node:tbrown.20160406221505.1: *4* qtree.sizeTreeEditor 

1194 @staticmethod 

1195 def sizeTreeEditor(c: Cmdr, editor: Editor) -> None: 

1196 """Size a QLineEdit in a tree headline so scrolling occurs""" 

1197 # space available in tree widget 

1198 space = c.frame.tree.treeWidget.size().width() 

1199 # left hand edge of editor within tree widget 

1200 used = editor.geometry().x() + 4 # + 4 for edit cursor 

1201 # limit width to available space 

1202 editor.resize(space - used, editor.size().height()) 

1203 #@+node:ekr.20110605121601.18433: *3* qtree.Scroll bars 

1204 #@+node:ekr.20110605121601.18434: *4* qtree.getSCroll 

1205 def getScroll(self) -> Tuple[int, int]: 

1206 """Return the hPos,vPos for the tree's scrollbars.""" 

1207 w = self.treeWidget 

1208 hScroll = w.horizontalScrollBar() 

1209 vScroll = w.verticalScrollBar() 

1210 hPos = hScroll.sliderPosition() 

1211 vPos = vScroll.sliderPosition() 

1212 return hPos, vPos 

1213 #@+node:btheado.20111110215920.7164: *4* qtree.scrollDelegate 

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

1215 """ 

1216 Scroll a QTreeWidget up or down or right or left. 

1217 kind is in ('down-line','down-page','up-line','up-page', 'right', 'left') 

1218 """ 

1219 c = self.c 

1220 w = self.treeWidget 

1221 if kind in ('left', 'right'): 

1222 hScroll = w.horizontalScrollBar() 

1223 if kind == 'right': 

1224 delta = hScroll.pageStep() 

1225 else: 

1226 delta = -hScroll.pageStep() 

1227 hScroll.setValue(hScroll.value() + delta) 

1228 else: 

1229 vScroll = w.verticalScrollBar() 

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

1231 lineSpacing = w.fontMetrics().lineSpacing() 

1232 n = h / lineSpacing 

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

1234 delta = n / 2 

1235 elif kind == 'down-line': 

1236 delta = 1 

1237 elif kind == 'down-page': 

1238 delta = n 

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

1240 delta = -n / 2 

1241 elif kind == 'up-line': 

1242 delta = -1 

1243 elif kind == 'up-page': 

1244 delta = -n 

1245 else: 

1246 delta = 0 

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

1248 val = vScroll.value() 

1249 vScroll.setValue(val + delta) 

1250 c.treeWantsFocus() 

1251 #@+node:ekr.20110605121601.18435: *4* qtree.setH/VScroll 

1252 def setHScroll(self, hPos: int) -> None: 

1253 

1254 w = self.treeWidget 

1255 hScroll = w.horizontalScrollBar() 

1256 hScroll.setValue(hPos) 

1257 

1258 def setVScroll(self, vPos: int) -> None: 

1259 

1260 w = self.treeWidget 

1261 vScroll = w.verticalScrollBar() 

1262 vScroll.setValue(vPos) 

1263 #@+node:ekr.20110605121601.17905: *3* qtree.Selecting & editing 

1264 #@+node:ekr.20110605121601.17908: *4* qtree.edit_widget 

1265 def edit_widget(self, p: Pos) -> Wrapper: 

1266 """Returns the edit widget for position p.""" 

1267 item = self.position2item(p) 

1268 if item: 

1269 e = self.getTreeEditorForItem(item) 

1270 if e: 

1271 # Create a wrapper widget for Leo's core. 

1272 w = self.getWrapper(e, item) 

1273 return w 

1274 # This is not an error 

1275 # But warning: calling this method twice might not work! 

1276 return None 

1277 return None 

1278 #@+node:ekr.20110605121601.17909: *4* qtree.editLabel and helper 

1279 def editLabel(self, 

1280 p: Pos, selectAll: bool=False, selection: Selection=None, 

1281 ) -> Tuple[Editor, Wrapper]: 

1282 """Start editing p's headline.""" 

1283 if self.busy: 

1284 return None 

1285 c = self.c 

1286 # Do any scheduled redraw. 

1287 # This won't do anything in the new redraw scheme. 

1288 c.outerUpdate() 

1289 item = self.position2item(p) 

1290 if item: 

1291 if self.use_declutter: 

1292 item.setText(0, item._real_text) 

1293 e, wrapper = self.editLabelHelper(item, selectAll, selection) 

1294 else: 

1295 e, wrapper = None, None 

1296 self.error(f"no item for {p}") 

1297 if e: 

1298 self.sizeTreeEditor(c, e) 

1299 # A nice hack: just set the focus request. 

1300 c.requestedFocusWidget = e 

1301 return e, wrapper 

1302 #@+node:ekr.20110605121601.18422: *5* qtree.editLabelHelper 

1303 def editLabelHelper(self, 

1304 item: Any, selectAll: bool=False, selection: Selection=None, 

1305 ) -> Tuple[Item, Wrapper]: 

1306 """Helper for qtree.editLabel.""" 

1307 c, vc = self.c, self.c.vimCommands 

1308 w = self.treeWidget 

1309 # Must do this first. 

1310 # This generates a call to onTreeSelect. 

1311 w.setCurrentItem(item) 

1312 w.editItem(item) # Generates focus-in event that tree doesn't report. 

1313 e = w.itemWidget(item, 0) # A QLineEdit. 

1314 s = e.text() 

1315 if s == 'newHeadline': 

1316 selectAll = True 

1317 start: int 

1318 n: int 

1319 if selection: 

1320 # pylint: disable=unpacking-non-sequence 

1321 # Fix bug https://groups.google.com/d/msg/leo-editor/RAzVPihqmkI/-tgTQw0-LtwJ 

1322 # Note: negative lengths are allowed. 

1323 i, j, ins = selection 

1324 if ins is None: 

1325 start, n = i, abs(i - j) 

1326 # This case doesn't happen for searches. 

1327 elif ins == j: 

1328 start, n = i, j - i 

1329 else: 

1330 start, n = j, i - j 

1331 elif selectAll: 

1332 start, n, ins = 0, len(s), len(s) 

1333 else: 

1334 start, n, ins = len(s), 0, len(s) 

1335 e.setObjectName('headline') 

1336 e.setSelection(start, n) 

1337 # e.setCursorPosition(ins) # Does not work. 

1338 e.setFocus() 

1339 wrapper = self.connectEditorWidget(e, item) # Hook up the widget. 

1340 if vc and c.vim_mode: # and selectAll 

1341 # For now, *always* enter insert mode. 

1342 if vc.is_text_wrapper(wrapper): 

1343 vc.begin_insert_mode(w=wrapper) 

1344 else: 

1345 g.trace('not a text widget!', wrapper) 

1346 return e, wrapper 

1347 #@+node:ekr.20110605121601.17911: *4* qtree.endEditLabel 

1348 def endEditLabel(self) -> None: 

1349 """ 

1350 Override LeoTree.endEditLabel. 

1351 

1352 Just end editing of the presently-selected QLineEdit! 

1353 This will trigger the editingFinished_callback defined in createEditorForItem. 

1354 """ 

1355 item = self.getCurrentItem() 

1356 if not item: 

1357 return 

1358 e = self.getTreeEditorForItem(item) 

1359 if not e: 

1360 return 

1361 # Trigger the end-editing event. 

1362 w = self.treeWidget 

1363 w.closeEditor(e, EndEditHint.NoHint) 

1364 w.setCurrentItem(item) 

1365 #@+node:ekr.20110605121601.17915: *4* qtree.getSelectedPositions 

1366 def getSelectedPositions(self) -> Any: 

1367 items = self.getSelectedItems() 

1368 pl = leoNodes.PosList(self.item2position(it) for it in items) 

1369 return pl 

1370 #@+node:ekr.20110605121601.17914: *4* qtree.setHeadline 

1371 def setHeadline(self, p: Pos, s: str) -> None: 

1372 """Force the actual text of the headline widget to p.h.""" 

1373 # This is used by unit tests to force the headline and p into alignment. 

1374 if not p: 

1375 return 

1376 # Don't do this here: the caller should do it. 

1377 # p.setHeadString(s) 

1378 e = self.edit_widget(p) 

1379 if e: 

1380 e.setAllText(s) 

1381 else: 

1382 item = self.position2item(p) 

1383 if item: 

1384 self.setItemText(item, s) 

1385 #@+node:ekr.20110605121601.17913: *4* qtree.setItemForCurrentPosition 

1386 def setItemForCurrentPosition(self) -> None: 

1387 """Select the item for c.p""" 

1388 p = self.c.p 

1389 if self.busy: 

1390 return None 

1391 if not p: 

1392 return None 

1393 item = self.position2item(p) 

1394 if not item: 

1395 # This is not necessarily an error. 

1396 # We often attempt to select an item before redrawing it. 

1397 return None 

1398 item2 = self.getCurrentItem() 

1399 if item == item2: 

1400 return item 

1401 try: 

1402 self.busy = True 

1403 # This generates gui events, so we must use a lockout. 

1404 self.treeWidget.setCurrentItem(item) 

1405 finally: 

1406 self.busy = False 

1407 return item 

1408 #@+node:ekr.20190613080606.1: *4* qtree.unselectItem 

1409 def unselectItem(self, p: Pos) -> None: 

1410 

1411 item = self.position2item(p) 

1412 if item: 

1413 item.setSelected(False) 

1414 #@-others 

1415#@-others 

1416#@@language python 

1417#@@tabwidth -4 

1418#@@pagewidth 80 

1419#@-leo