Coverage for C:\leo.repo\leo-editor\leo\plugins\qt_tree.py: 14%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

941 statements  

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

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

8import re 

9import time 

10from typing import Any, List 

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

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

13from leo.core import leoGlobals as g 

14from leo.core import leoFrame 

15from leo.core import leoNodes 

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

17from leo.plugins import qt_text 

18#@-<< imports >> 

19#@+others 

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

21class LeoQtTree(leoFrame.LeoTree): 

22 """Leo Qt tree class""" 

23 #@+others 

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

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

26 def __init__(self, c, frame): 

27 """Ctor for the LeoQtTree class.""" 

28 super().__init__(frame) 

29 self.c = c 

30 # Widget independent status ivars... 

31 self.prev_v = None 

32 self.redrawCount = 0 # Count for debugging. 

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

34 self.busy = False 

35 # Debugging... 

36 self.traceCallersFlag = False # Enable traceCallers method. 

37 # Associating items with position and vnodes... 

38 self.items = [] 

39 self.item2positionDict = {} 

40 self.item2vnodeDict = {} 

41 self.nodeIconsDict = {} # keys are gnx, values are declutter generated icons 

42 self.position2itemDict = {} 

43 self.vnode2itemsDict = {} # values are lists of items. 

44 self.editWidgetsDict = {} # keys are native edit widgets, values are wrappers. 

45 self.reloadSettings() 

46 # Components. 

47 self.canvas = self # An official ivar used by Leo's core. 

48 self.headlineWrapper = qt_text.QHeadlineWrapper # This is a class. 

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

50 self.treeWidget = w = frame.top.treeWidget # An internal ivar. 

51 # 

52 # "declutter", node appearance tweaking 

53 self.declutter_patterns = None # list of pairs of patterns for decluttering 

54 self.declutter_data = {} 

55 self.loaded_images = {} 

56 if 0: # Drag and drop 

57 w.setDragEnabled(True) 

58 w.viewport().setAcceptDrops(True) 

59 w.showDropIndicator = True 

60 w.setAcceptDrops(True) 

61 w.setDragDropMode(w.InternalMove) 

62 if 1: # Does not work 

63 

64 def dropMimeData(self, data, action, row, col, parent): 

65 g.trace() 

66 

67 # w.dropMimeData = dropMimeData 

68 

69 def mimeData(self, indexes): 

70 g.trace() 

71 

72 # Early inits... 

73 

74 try: 

75 w.headerItem().setHidden(True) 

76 except Exception: 

77 pass 

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

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

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

81 def getName(self): 

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

83 return 'canvas(tree)' 

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

85 def initAfterLoad(self): 

86 """Do late-state inits.""" 

87 # Called by Leo's core. 

88 c = self.c 

89 # w = c.frame.top 

90 tw = self.treeWidget 

91 tw.itemDoubleClicked.connect(self.onItemDoubleClicked) 

92 tw.itemClicked.connect(self.onItemClicked) 

93 tw.itemSelectionChanged.connect(self.onTreeSelect) 

94 tw.itemCollapsed.connect(self.onItemCollapsed) 

95 tw.itemExpanded.connect(self.onItemExpanded) 

96 tw.customContextMenuRequested.connect(self.onContextMenu) 

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

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

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

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

101 # c.clearChanged() 

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

103 def reloadSettings(self): 

104 """LeoQtTree.""" 

105 c = self.c 

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

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

108 self.select_all_text_when_editing_headlines = c.config.getBool( 

109 'select_all_text_when_editing_headlines') 

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

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

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

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

114 def wrapQLineEdit(self, w): 

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

116 c = self.c 

117 if isinstance(w, QtWidgets.QLineEdit): 

118 wrapper = self.edit_widget(c.p) 

119 else: 

120 wrapper = w 

121 return wrapper 

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

123 def error(self, s): 

124 if not g.unitTesting: 

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

126 

127 def traceItem(self, item): 

128 if item: 

129 # A QTreeWidgetItem. 

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

131 return '<no item>' 

132 

133 def traceCallers(self): 

134 if self.traceCallersFlag: 

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

136 return '' 

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

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

139 def clear(self): 

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

141 w = self.treeWidget 

142 w.clear() 

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

144 def drawVisible(self, p): 

145 """ 

146 Add only the visible nodes to the outline. 

147 

148 Not used, as this causes scrolling issues. 

149 """ 

150 t1 = time.process_time() 

151 c = self.c 

152 parents: List[Any] = [] 

153 # Clear the widget. 

154 w = self.treeWidget 

155 w.clear() 

156 # Clear the dicts. 

157 self.initData() 

158 if c.hoistStack: 

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

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

161 else: 

162 first_p = c.rootPosition() 

163 target_p = None 

164 n = 0 

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

166 n += 1 

167 level = p.level() 

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

169 item = QtWidgets.QTreeWidgetItem(parent_item) 

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

171 item.setChildIndicatorPolicy( 

172 item.ShowIndicator if p.hasChildren() 

173 else item.DontShowIndicator) 

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

175 self.items.append(item) 

176 # Update parents. 

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

178 parents.append(item) 

179 # Update the dicts. 

180 itemHash = self.itemHash(item) 

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

182 self.item2vnodeDict[itemHash] = p.v 

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

184 d = self.vnode2itemsDict 

185 v = p.v 

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

187 aList.append(item) 

188 d[v] = aList 

189 # Enter the headline. 

190 item.setText(0, p.h) 

191 if self.use_declutter: 

192 item._real_text = p.h 

193 # Draw the icon. 

194 v.iconVal = v.computeIcon() 

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

196 if icon: 

197 self.setItemIcon(item, icon) 

198 # Set current item. 

199 if p == c.p: 

200 w.setCurrentItem(item) 

201 # Useful, for now. 

202 t2 = time.process_time() 

203 if t2 - t1 > 0.1: 

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

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

206 def yieldVisible(self, first_p, target_p=None): 

207 """ 

208 A generator yielding positions from first_p to target_p. 

209 """ 

210 c = self.c 

211 p = first_p.copy() 

212 yield p 

213 while p: 

214 if p == target_p: 

215 return 

216 v = p.v 

217 if (v.children and ( 

218 # Use slower test for clones: 

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

220 # Use a quick test for non-clones: 

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

222 )): 

223 # p.moveToFirstChild() 

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

225 p.v = v.children[0] 

226 p._childIndex = 0 

227 yield p 

228 continue 

229 # if p.hasNext(): 

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

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

232 # p.moveToNext() 

233 p._childIndex += 1 

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

235 yield p 

236 continue 

237 # 

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

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

240 while p.stack: 

241 # p.moveToParent() 

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

243 # if p.hasNext(): 

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

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

246 # p.moveToNext() 

247 p._childIndex += 1 

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

249 break # Found: moveToThreadNext() 

250 else: 

251 break # Not found. 

252 # Found moveToThreadNext() 

253 yield p 

254 continue 

255 if target_p: 

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

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

258 def slowYieldVisible(self, first_p, target_p=None): 

259 """ 

260 A generator yielding positions from first_p to target_p. 

261 """ 

262 c = self.c 

263 p = first_p.copy() 

264 while p: 

265 yield p 

266 if p == target_p: 

267 return 

268 p.moveToVisNext(c) 

269 if target_p: 

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

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

272 def full_redraw(self, p=None): 

273 """ 

274 Redraw all visible nodes of the tree. 

275 Preserve the vertical scrolling unless scroll is True. 

276 """ 

277 c = self.c 

278 if g.app.disable_redraw: 

279 return None 

280 if self.busy: 

281 return None 

282 # Cancel the delayed redraw request. 

283 c.requestLaterRedraw = False 

284 if not p: 

285 p = c.currentPosition() 

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

287 # Make sure the current position is visible. 

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

289 p = p.firstChild() 

290 c.frame.tree.select(p) 

291 c.setCurrentPosition(p) 

292 else: 

293 c.setCurrentPosition(p) 

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

295 self.redrawCount += 1 

296 self.initData() 

297 try: 

298 self.busy = True 

299 self.drawTopTree(p) 

300 finally: 

301 self.busy = False 

302 self.setItemForCurrentPosition() 

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

304 

305 # Compatibility 

306 

307 redraw = full_redraw 

308 redraw_now = full_redraw 

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

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

311 def declutter_node(self, c, p, item): 

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

313 

314 :param commander c: commander containing node 

315 :param position p: position of node 

316 :param QWidgetItem item: tree node widget item 

317 

318 returns composite icon for this node 

319 """ 

320 dd = self.declutter_data 

321 iconVal = p.v.computeIcon() 

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

323 loaded_images = self.loaded_images 

324 #@+others 

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

326 def sorted_icons(p): 

327 """ 

328 Returns a list of icon filenames for this node. 

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

330 """ 

331 icons = c.editCommands.getIconList(p) 

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

333 a.append(iconName) 

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

335 return a 

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

337 def declutter_replace(arg, cmd): 

338 """ 

339 Executes cmd if cmd is any replace command and returns 

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

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

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

343 """ 

344 # pylint: disable=undefined-loop-variable 

345 

346 replacement, s = None, None 

347 

348 if cmd == 'REPLACE': 

349 s = pattern.sub(arg, text) 

350 elif cmd == 'REPLACE-HEAD': 

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

352 elif cmd == 'REPLACE-TAIL': 

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

354 elif cmd == 'REPLACE-REST': 

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

356 

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

358 # and is None otherwise 

359 if isinstance(s, str): 

360 # Save the operation 

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

362 # ... and apply it 

363 replacement(item, s) 

364 

365 return replacement, s 

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

367 def declutter_style(arg, cmd): 

368 """ 

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

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

371 param - the saved argument of that operation. 

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

373 """ 

374 # pylint: disable=function-redefined 

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

376 modifier = None 

377 if cmd == 'ICON': 

378 def modifier(item, param): 

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

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

381 # the list is recreated before each call. 

382 new_icons.append(param) 

383 elif cmd == 'BG': 

384 def modifier(item, param): 

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

386 elif cmd == 'FG': 

387 def modifier(item, param): 

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

389 elif cmd == 'FONT': 

390 def modifier(item, param): 

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

392 elif cmd == 'ITALIC': 

393 def modifier(item, param): 

394 font = item.font(0) 

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

396 item.setFont(0, font) 

397 elif cmd == 'WEIGHT': 

398 def modifier(item, param): 

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

400 font = item.font(0) 

401 font.setWeight(arg) 

402 item.setFont(0, font) 

403 elif cmd == 'PX': 

404 def modifier(item, param): 

405 font = item.font(0) 

406 font.setPixelSize(int(param)) 

407 item.setFont(0, font) 

408 elif cmd == 'PT': 

409 def modifier(item, param): 

410 font = item.font(0) 

411 font.setPointSize(int(param)) 

412 item.setFont(0, font) 

413 # Apply the style update 

414 if modifier: 

415 modifier(item, param) 

416 return modifier, param 

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

418 def apply_declutter_rules(cmds): 

419 """ 

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

421 of the applied operations paired with their single parameter. 

422 """ 

423 modifiers = [] 

424 for cmd, arg in cmds: 

425 modifier, param = declutter_replace(arg, cmd) 

426 if not modifier: 

427 modifier, param = declutter_style(arg, cmd) 

428 if modifier: 

429 modifiers.append((modifier, param)) 

430 return modifiers 

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

432 def preload_images(): 

433 for f in new_icons: 

434 if f not in loaded_images: 

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

436 #@-others 

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

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

439 # of the node 

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

441 for modifier, arg in modifiers_and_args: 

442 modifier(item, arg) 

443 

444 new_icons = sorted_icons(p) + new_icons 

445 else: 

446 text = p.h 

447 new_icons = [] 

448 modifiers_and_args = [] 

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

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

451 if m: 

452 modifiers_and_args.extend(apply_declutter_rules(cmds)) 

453 

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

455 # for future reuse. 

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

457 new_icons = sorted_icons(p) + new_icons 

458 preload_images() 

459 self.nodeIconsDict[p.gnx] = new_icons 

460 h = ':'.join(new_icons) 

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

462 if not icon: 

463 preload_images() 

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

465 icon = self.make_composite_icon(images) 

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

467 return icon 

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

469 def get_declutter_patterns(self): 

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

471 if self.declutter_patterns is not None: 

472 return self.declutter_patterns 

473 c = self.c 

474 patterns: List[Any] = [] 

475 warned = False 

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

477 for line in lines: 

478 try: 

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

480 except ValueError: 

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

482 cmd = line.strip() 

483 arg = '' 

484 if cmd.startswith('#'): 

485 pass 

486 elif cmd == 'RULE': 

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

488 else: 

489 if patterns: 

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

491 elif not warned: 

492 warned = True 

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

494 color='error') 

495 self.declutter_patterns = patterns 

496 return patterns 

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

498 def drawChildren(self, p, parent_item): 

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

500 if not p: 

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

502 return 

503 if p.hasChildren(): 

504 if p.isExpanded(): 

505 self.expandItem(parent_item) 

506 child = p.firstChild() 

507 while child: 

508 self.drawTree(child, parent_item) 

509 child.moveToNext() 

510 else: 

511 # Draw the hidden children. 

512 child = p.firstChild() 

513 while child: 

514 self.drawNode(child, parent_item) 

515 child.moveToNext() 

516 self.contractItem(parent_item) 

517 else: 

518 self.contractItem(parent_item) 

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

520 def drawNode(self, p, parent_item): 

521 """Draw the node p.""" 

522 c = self.c 

523 v = p.v 

524 # Allocate the QTreeWidgetItem. 

525 item = self.createTreeItem(p, parent_item) 

526 # Update the data structures. 

527 itemHash = self.itemHash(item) 

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

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

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

531 d = self.vnode2itemsDict 

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

533 if item not in aList: 

534 aList.append(item) 

535 d[v] = aList 

536 # Set the headline and maybe the icon. 

537 self.setItemText(item, p.h) 

538 # #1310: Add a tool tip. 

539 item.setToolTip(0, p.h) 

540 if self.use_declutter: 

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

542 if icon: 

543 item.setIcon(0, icon) 

544 return item 

545 # Draw the icon. 

546 v.iconVal = v.computeIcon() 

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

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

549 if icon: 

550 item.setIcon(0, icon) 

551 return item 

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

553 def drawTopTree(self, p): 

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

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

556 if trace: 

557 t1 = time.process_time() 

558 c = self.c 

559 self.clear() 

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

561 if c.hoistStack: 

562 bunch = c.hoistStack[-1] 

563 p = bunch.p 

564 h = p.h 

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

566 p = p.firstChild() 

567 while p: 

568 self.drawTree(p) 

569 p.moveToNext() 

570 else: 

571 self.drawTree(p) 

572 else: 

573 p = c.rootPosition() 

574 while p: 

575 self.drawTree(p) 

576 p.moveToNext() 

577 if trace: 

578 t2 = time.process_time() 

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

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

581 def drawTree(self, p, parent_item=None): 

582 if g.app.gui.isNullGui: 

583 return 

584 # Draw the (visible) parent node. 

585 item = self.drawNode(p, parent_item) 

586 # Draw all the visible children. 

587 self.drawChildren(p, parent_item=item) 

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

589 def initData(self): 

590 self.item2positionDict = {} 

591 self.item2vnodeDict = {} 

592 self.position2itemDict = {} 

593 self.vnode2itemsDict = {} 

594 self.editWidgetsDict = {} 

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

596 def redraw_after_contract(self, p): 

597 

598 if self.busy: 

599 return 

600 self.update_expansion(p) 

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

602 def redraw_after_expand(self, p): 

603 

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

605 c = self.c 

606 c.selectPosition(p) 

607 self.update_expansion(p) 

608 else: 

609 self.full_redraw(p) 

610 # Don't try to shortcut this! 

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

612 def redraw_after_head_changed(self): 

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

614 if self.busy: 

615 return 

616 p = self.c.p 

617 if p: 

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

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

620 if self.isValidItem(item): 

621 self.setItemText(item, h) 

622 # Bug fix: 2009/10/06 

623 self.redraw_after_icons_changed() 

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

625 def redraw_after_icons_changed(self): 

626 

627 if self.busy: 

628 return 

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

630 c = self.c 

631 try: 

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

633 self.getCurrentItem() 

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

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

636 self.updateVisibleIcons(p) 

637 finally: 

638 self.busy = False 

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

640 def redraw_after_select(self, p=None): 

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

642 if self.busy: 

643 return 

644 self.full_redraw(p) 

645 # c.redraw_after_select calls tree.select indirectly. 

646 # Do not call it again here. 

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

648 def repaint(self): 

649 """Repaint the widget.""" 

650 w = self.treeWidget 

651 w.repaint() 

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

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

654 def update_expansion(self, p): 

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

656 c = self.c 

657 w = self.treeWidget 

658 expand = c.shouldBeExpanded(p) 

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

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

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

662 if p: 

663 try: 

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

665 self.busy = True 

666 if expand: 

667 w.expandItem(item) 

668 else: 

669 w.collapseItem(item) 

670 finally: 

671 self.busy = False 

672 w.repaint() 

673 else: 

674 g.trace('NO P') 

675 c.redraw() 

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

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

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

679 def onClickBoxClick(self, event, p=None): 

680 if self.busy: 

681 return 

682 c = self.c 

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

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

685 c.outerUpdate() 

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

687 def onClickBoxRightClick(self, event, p=None): 

688 if self.busy: 

689 return 

690 c = self.c 

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

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

693 c.outerUpdate() 

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

695 def onPlusBoxRightClick(self, event, p=None): 

696 if self.busy: 

697 return 

698 c = self.c 

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

700 c.outerUpdate() 

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

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

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

704 def onIconBoxClick(self, event, p=None): 

705 if self.busy: 

706 return 

707 c = self.c 

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

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

710 c.outerUpdate() 

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

712 def onIconBoxRightClick(self, event, p=None): 

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

714 if self.busy: 

715 return 

716 c = self.c 

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

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

719 c.outerUpdate() 

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

721 def onIconBoxDoubleClick(self, event, p=None): 

722 if self.busy: 

723 return 

724 c = self.c 

725 if not p: 

726 p = c.p 

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

728 self.endEditLabel() 

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

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

731 c.outerUpdate() 

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

733 def onContextMenu(self, point): 

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

735 # #1286. 

736 c, w = self.c, self.treeWidget 

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

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

739 def onItemClicked(self, item, col): 

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

741 # This is called after an item is selected. 

742 if self.busy: 

743 return 

744 c = self.c 

745 try: 

746 self.busy = True 

747 p = self.item2position(item) 

748 if p: 

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

750 self.prev_v = p.v 

751 event = None 

752 # 

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

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

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

756 isCtrl = bool(mods & KeyboardModifier.ControlModifier) 

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

758 # & QtConst.MetaModifier. 

759 if isCtrl: 

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

761 c.frame.tree.OnIconCtrlClick(p) 

762 # Call the base class method. 

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

764 else: 

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

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

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

768 else: 

769 auto_edit = None 

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

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

772 c.k.keyboardQuit(setFocus=False) 

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

774 c.outerUpdate() 

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

776 # enters editing state. 

777 if auto_edit and self.auto_edit: 

778 e, wrapper = self.createTreeEditorForItem(item) 

779 finally: 

780 self.busy = False 

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

782 def onItemCollapsed(self, item): 

783 

784 if self.busy: 

785 return 

786 c = self.c 

787 p = self.item2position(item) 

788 if not p: 

789 self.error('no p') 

790 return 

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

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

793 if p.isExpanded(): 

794 p.contract() 

795 c.redraw_after_contract(p) 

796 self.select(p) 

797 c.outerUpdate() 

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

799 def onItemDoubleClicked(self, item, col): 

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

801 if self.busy: # Required. 

802 return 

803 c = self.c 

804 try: 

805 self.busy = True 

806 e, wrapper = self.createTreeEditorForItem(item) 

807 if not e: 

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

809 p = self.item2position(item) 

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

811 finally: 

812 self.busy = False 

813 if not p: 

814 self.error('no p') 

815 return 

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

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

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

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

820 c.outerUpdate() 

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

822 def onItemExpanded(self, item): 

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

824 if self.busy: # Required 

825 return 

826 c = self.c 

827 p = self.item2position(item) 

828 if not p: 

829 self.error('no p') 

830 return 

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

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

833 if not p.isExpanded(): 

834 p.expand() 

835 c.redraw_after_expand(p) 

836 self.select(p) 

837 c.outerUpdate() 

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

839 def onTreeSelect(self): 

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

841 if self.busy: # Required 

842 return 

843 c = self.c 

844 item = self.getCurrentItem() 

845 p = self.item2position(item) 

846 if not p: 

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

848 return 

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

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

851 self.select(p) 

852 # This is a call to LeoTree.select(!!) 

853 c.outerUpdate() 

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

855 def getFocus(self): 

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

857 

858 findFocus = getFocus 

859 

860 def setFocus(self): 

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

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

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

864 def drawIcon(self, p): 

865 """Redraw the icon at p.""" 

866 return self.updateIcon(p) 

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

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

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

870 # w = self.treeWidget 

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

872 # item = QtWidgets.QTreeWidgetItem(itemOrTree) 

873 # icon = self.getIcon(p) 

874 # self.setItemIcon(item, icon) 

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

876 def getIcon(self, p): 

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

878 if self.use_declutter: 

879 item = self.position2item(p) 

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

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

882 return self.getCompositeIconImage(p, iv) 

883 

884 

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

886 def icon_filenames_for_node(self, p, val): 

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

888 related to this node. 

889 """ 

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

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

892 if not fnames: 

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

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

895 fnames.append(nicon) 

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

897 self.nodeIconsDict[p.gnx] = fnames 

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

899 loaded_images = self.loaded_images 

900 for i, f in enumerate(fnames): 

901 if pat.match(f): 

902 fnames[i] = nicon 

903 self.nodeIconsDict[p.gnx] = fnames 

904 f = nicon 

905 if f not in loaded_images: 

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

907 return fnames 

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

909 def make_composite_icon(self, images): 

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

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

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

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

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

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

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

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

918 painter = QtGui.QPainter() 

919 if not painter.begin(pix): 

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

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

922 # which stops this being called again and again 

923 x = 0 

924 for i in images: 

925 painter.drawPixmap(x, 0, i) 

926 x += i.width() + hsep 

927 painter.end() 

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

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

930 def getCompositeIconImage(self, p, val): 

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

932 fnames = self.icon_filenames_for_node(p, val) 

933 h = ':'.join(fnames) 

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

935 loaded_images = self.loaded_images 

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

937 if not icon: 

938 icon = self.make_composite_icon(images) 

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

940 return icon 

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

942 def setItemIcon(self, item, icon): 

943 

944 valid = item and self.isValidItem(item) 

945 if icon and valid: 

946 # Important: do not set lockouts here. 

947 # This will generate changed events, 

948 # but there is no itemChanged event handler. 

949 item.setIcon(0, icon) 

950 

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

952 def updateIcon(self, p): 

953 """Update p's icon.""" 

954 if not p: 

955 return 

956 val = p.v.computeIcon() 

957 if p.v.iconVal != val: 

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

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

960 

961 def updateAllIcons(self, p): 

962 if not p: 

963 return 

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

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

966 # Update all cloned items. 

967 items = self.vnode2items(p.v) 

968 for item in items: 

969 self.setItemIcon(item, icon) 

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

971 def updateVisibleIcons(self, p): 

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

973 for all visible descendants of p.""" 

974 self.updateAllIcons(p) 

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

976 for child in p.children(): 

977 self.updateVisibleIcons(child) 

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

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

980 def itemHash(self, item): 

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

982 

983 def item2position(self, item): 

984 itemHash = self.itemHash(item) 

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

986 return p 

987 

988 def item2vnode(self, item): 

989 itemHash = self.itemHash(item) 

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

991 

992 def position2item(self, p): 

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

994 return item 

995 

996 def vnode2items(self, v): 

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

998 

999 def isValidItem(self, item): 

1000 itemHash = self.itemHash(item) 

1001 return itemHash in self.item2vnodeDict # was item. 

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

1003 def childIndexOfItem(self, item): 

1004 parent = item and item.parent() 

1005 if parent: 

1006 n = parent.indexOfChild(item) 

1007 else: 

1008 w = self.treeWidget 

1009 n = w.indexOfTopLevelItem(item) 

1010 return n 

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

1012 def childItems(self, parent_item): 

1013 """ 

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

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

1016 """ 

1017 if parent_item: 

1018 n = parent_item.childCount() 

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

1020 else: 

1021 w = self.treeWidget 

1022 n = w.topLevelItemCount() 

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

1024 return items 

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

1026 def connectEditorWidget(self, e, item): 

1027 """ 

1028 Connect QLineEdit e to QTreeItem item. 

1029 

1030 Also callback for when the editor ends. 

1031 

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

1033 """ 

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

1035 #@+others # define the callback. 

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

1037 def editingFinished_callback(): 

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

1039 s = e.text() 

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

1041 # Truncate to one line. 

1042 if i > -1: 

1043 s = s[:i] 

1044 # #1310: update the tooltip. 

1045 if p.h != s: 

1046 # Update p.h and handle undo. 

1047 item.setToolTip(0, s) 

1048 undoData = u.beforeChangeHeadline(p) 

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

1050 if not c.changed: 

1051 c.setChanged() 

1052 # We must recolor the body because 

1053 # the headline may contain directives. 

1054 c.frame.body.recolor(p) 

1055 p.setDirty() 

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

1057 self.redraw_after_head_changed() 

1058 c.outerUpdate() 

1059 #@-others 

1060 if e: 

1061 # Hook up the widget. 

1062 wrapper = self.getWrapper(e, item) 

1063 e.editingFinished.connect(editingFinished_callback) 

1064 return wrapper # 2011/02/12 

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

1066 return None 

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

1068 def contractItem(self, item): 

1069 self.treeWidget.collapseItem(item) 

1070 

1071 def expandItem(self, item): 

1072 self.treeWidget.expandItem(item) 

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

1074 def createTreeEditorForItem(self, item): 

1075 

1076 c = self.c 

1077 w = self.treeWidget 

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

1079 if self.use_declutter: 

1080 item.setText(0, item._real_text) 

1081 w.editItem(item) 

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

1083 e.setObjectName('headline') 

1084 wrapper = self.connectEditorWidget(e, item) 

1085 self.sizeTreeEditor(c, e) 

1086 return e, wrapper 

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

1088 def createTreeItem(self, p, parent_item): 

1089 

1090 w = self.treeWidget 

1091 itemOrTree = parent_item or w 

1092 item = QtWidgets.QTreeWidgetItem(itemOrTree) 

1093 if isQt6: 

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

1095 ChildIndicatorPolicy = QtWidgets.QTreeWidgetItem.ChildIndicatorPolicy 

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

1097 else: 

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

1099 try: 

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

1101 except leoPlugins.TryNext: 

1102 pass 

1103 return item 

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

1105 def getCurrentItem(self): 

1106 w = self.treeWidget 

1107 return w.currentItem() 

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

1109 def getItemText(self, item): 

1110 """Return the text of the item.""" 

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

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

1113 def getParentItem(self, item): 

1114 return item and item.parent() 

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

1116 def getSelectedItems(self): 

1117 w = self.treeWidget 

1118 return w.selectedItems() 

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

1120 def getTreeEditorForItem(self, item): 

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

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

1123 """ 

1124 w = self.treeWidget 

1125 e = w.itemWidget(item, 0) 

1126 return e 

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

1128 def getWrapper(self, e, item): 

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

1130 c = self.c 

1131 if e: 

1132 wrapper = self.editWidgetsDict.get(e) 

1133 if wrapper: 

1134 pass 

1135 else: 

1136 if item: 

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

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

1139 self.editWidgetsDict[e] = wrapper 

1140 return wrapper 

1141 g.trace('no e') 

1142 return None 

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

1144 def nthChildItem(self, n, parent_item): 

1145 children = self.childItems(parent_item) 

1146 if n < len(children): 

1147 item = children[n] 

1148 else: 

1149 # This is **not* an error. 

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

1151 item = None 

1152 return item 

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

1154 def scrollToItem(self, item): 

1155 """ 

1156 Scroll the tree widget so that item is visible. 

1157 Leo's core no longer calls this method. 

1158 """ 

1159 w = self.treeWidget 

1160 hPos, vPos = self.getScroll() 

1161 w.scrollToItem(item, w.EnsureVisible) 

1162 # Fix #265: Erratic scrolling bug. 

1163 # w.PositionAtCenter causes unwanted scrolling. 

1164 self.setHScroll(0) 

1165 # Necessary 

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

1167 def setCurrentItemHelper(self, item): 

1168 w = self.treeWidget 

1169 w.setCurrentItem(item) 

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

1171 def setItemText(self, item, s): 

1172 if item: 

1173 item.setText(0, s) 

1174 if self.use_declutter: 

1175 item._real_text = s 

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

1177 @staticmethod 

1178 def sizeTreeEditor(c, editor): 

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

1180 # space available in tree widget 

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

1182 # left hand edge of editor within tree widget 

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

1184 # limit width to available space 

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

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

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

1188 def getScroll(self): 

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

1190 w = self.treeWidget 

1191 hScroll = w.horizontalScrollBar() 

1192 vScroll = w.verticalScrollBar() 

1193 hPos = hScroll.sliderPosition() 

1194 vPos = vScroll.sliderPosition() 

1195 return hPos, vPos 

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

1197 def scrollDelegate(self, kind): 

1198 """ 

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

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

1201 """ 

1202 c = self.c 

1203 w = self.treeWidget 

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

1205 hScroll = w.horizontalScrollBar() 

1206 if kind == 'right': 

1207 delta = hScroll.pageStep() 

1208 else: 

1209 delta = -hScroll.pageStep() 

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

1211 else: 

1212 vScroll = w.verticalScrollBar() 

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

1214 lineSpacing = w.fontMetrics().lineSpacing() 

1215 n = h / lineSpacing 

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

1217 delta = n / 2 

1218 elif kind == 'down-line': 

1219 delta = 1 

1220 elif kind == 'down-page': 

1221 delta = n 

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

1223 delta = -n / 2 

1224 elif kind == 'up-line': 

1225 delta = -1 

1226 elif kind == 'up-page': 

1227 delta = -n 

1228 else: 

1229 delta = 0 

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

1231 val = vScroll.value() 

1232 vScroll.setValue(val + delta) 

1233 c.treeWantsFocus() 

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

1235 def setHScroll(self, hPos): 

1236 

1237 w = self.treeWidget 

1238 hScroll = w.horizontalScrollBar() 

1239 hScroll.setValue(hPos) 

1240 

1241 def setVScroll(self, vPos): 

1242 

1243 w = self.treeWidget 

1244 vScroll = w.verticalScrollBar() 

1245 vScroll.setValue(vPos) 

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

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

1248 def edit_widget(self, p): 

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

1250 item = self.position2item(p) 

1251 if item: 

1252 e = self.getTreeEditorForItem(item) 

1253 if e: 

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

1255 w = self.getWrapper(e, item) 

1256 return w 

1257 # This is not an error 

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

1259 return None 

1260 return None 

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

1262 def editLabel(self, p, selectAll=False, selection=None): 

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

1264 if self.busy: 

1265 return None 

1266 c = self.c 

1267 c.outerUpdate() 

1268 # Do any scheduled redraw. 

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

1270 item = self.position2item(p) 

1271 if item: 

1272 if self.use_declutter: 

1273 item.setText(0, item._real_text) 

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

1275 else: 

1276 e, wrapper = None, None 

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

1278 if e: 

1279 self.sizeTreeEditor(c, e) 

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

1281 c.requestedFocusWidget = e 

1282 return e, wrapper 

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

1284 def editLabelHelper(self, item, selectAll=False, selection=None): 

1285 """Helper for qtree.editLabel.""" 

1286 c, vc = self.c, self.c.vimCommands 

1287 w = self.treeWidget 

1288 w.setCurrentItem(item) 

1289 # Must do this first. 

1290 # This generates a call to onTreeSelect. 

1291 w.editItem(item) 

1292 # Generates focus-in event that tree doesn't report. 

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

1294 s = e.text() 

1295 if s == 'newHeadline': 

1296 selectAll = True 

1297 if selection: 

1298 # pylint: disable=unpacking-non-sequence 

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

1300 # Note: negative lengths are allowed. 

1301 i, j, ins = selection 

1302 if ins is None: 

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

1304 # This case doesn't happen for searches. 

1305 elif ins == j: 

1306 start, n = i, j - i 

1307 else: 

1308 start = start, n = j, i - j 

1309 elif selectAll: 

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

1311 else: 

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

1313 e.setObjectName('headline') 

1314 e.setSelection(start, n) 

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

1316 e.setFocus() 

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

1318 if vc and c.vim_mode: # and selectAll 

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

1320 if vc.is_text_wrapper(wrapper): 

1321 vc.begin_insert_mode(w=wrapper) 

1322 else: 

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

1324 return e, wrapper 

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

1326 def endEditLabel(self): 

1327 """ 

1328 Override LeoTree.endEditLabel. 

1329 

1330 Just end editing of the presently-selected QLineEdit! 

1331 This will trigger the editingFinished_callback defined in createEditorForItem. 

1332 """ 

1333 item = self.getCurrentItem() 

1334 if not item: 

1335 return 

1336 e = self.getTreeEditorForItem(item) 

1337 if not e: 

1338 return 

1339 # Trigger the end-editing event. 

1340 w = self.treeWidget 

1341 w.closeEditor(e, EndEditHint.NoHint) 

1342 w.setCurrentItem(item) 

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

1344 def getSelectedPositions(self): 

1345 items = self.getSelectedItems() 

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

1347 return pl 

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

1349 def setHeadline(self, p, s): 

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

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

1352 if not p: 

1353 return 

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

1355 # p.setHeadString(s) 

1356 e = self.edit_widget(p) 

1357 if e: 

1358 e.setAllText(s) 

1359 else: 

1360 item = self.position2item(p) 

1361 if item: 

1362 self.setItemText(item, s) 

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

1364 def setItemForCurrentPosition(self): 

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

1366 p = self.c.p 

1367 if self.busy: 

1368 return None 

1369 if not p: 

1370 return None 

1371 item = self.position2item(p) 

1372 if not item: 

1373 # This is not necessarily an error. 

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

1375 return None 

1376 item2 = self.getCurrentItem() 

1377 if item == item2: 

1378 return item 

1379 try: 

1380 self.busy = True 

1381 self.treeWidget.setCurrentItem(item) 

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

1383 finally: 

1384 self.busy = False 

1385 return item 

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

1387 def unselectItem(self, p): 

1388 

1389 item = self.position2item(p) 

1390 if item: 

1391 item.setSelected(False) 

1392 #@-others 

1393#@-others 

1394#@@language python 

1395#@@tabwidth -4 

1396#@@pagewidth 80 

1397#@-leo