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

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2#@+leo-ver=5-thin
3#@+node:ekr.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 self.treeWidget = w = frame.top.treeWidget # An internal ivar.
50 # w is a LeoQTreeWidget, a subclass of QTreeWidget.
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
64 def dropMimeData(self, data, action, row, col, parent):
65 g.trace()
67 # w.dropMimeData = dropMimeData
69 def mimeData(self, indexes):
70 g.trace()
72 # Early inits...
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())
127 def traceItem(self, item):
128 if item:
129 # A QTreeWidgetItem.
130 return f"item {id(item)}: {self.getItemText(item)}"
131 return '<no item>'
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.
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.
305 # Compatibility
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
314 :param commander c: commander containing node
315 :param position p: position of node
316 :param QWidgetItem item: tree node widget item
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
346 replacement, s = None, None
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()
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)
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)
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))
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 #
527 # Update the data structures.
528 itemHash = self.itemHash(item)
529 self.position2itemDict[p.key()] = item
530 self.item2positionDict[itemHash] = p.copy() # was item
531 self.item2vnodeDict[itemHash] = v # was item
532 d = self.vnode2itemsDict
533 aList = d.get(v, [])
534 if item not in aList:
535 aList.append(item)
536 d[v] = aList
537 # Set the headline and maybe the icon.
538 self.setItemText(item, p.h)
539 # #1310: Add a tool tip.
540 item.setToolTip(0, p.h)
541 if self.use_declutter:
542 icon = self.declutter_node(c, p, item)
543 if icon:
544 item.setIcon(0, icon)
545 return item
546 # Draw the icon.
547 v.iconVal = v.computeIcon()
548 icon = self.getCompositeIconImage(p, v.iconVal)
549 # **Slow**, but allows per-vnode icons.
550 if icon:
551 item.setIcon(0, icon)
552 return item
553 #@+node:ekr.20110605121601.17876: *5* qtree.drawTopTree
554 def drawTopTree(self, p):
555 """Draw the tree rooted at p."""
556 trace = 'drawing' in g.app.debug and not g.unitTesting
557 if trace:
558 t1 = time.process_time()
559 c = self.c
560 self.clear()
561 # Draw all top-level nodes and their visible descendants.
562 if c.hoistStack:
563 bunch = c.hoistStack[-1]
564 p = bunch.p
565 h = p.h
566 if len(c.hoistStack) == 1 and h.startswith('@chapter') and p.hasChildren():
567 p = p.firstChild()
568 while p:
569 self.drawTree(p)
570 p.moveToNext()
571 else:
572 self.drawTree(p)
573 else:
574 p = c.rootPosition()
575 while p:
576 self.drawTree(p)
577 p.moveToNext()
578 if trace:
579 t2 = time.process_time()
580 g.trace(f"{t2 - t1:5.2f} sec.", g.callers(5))
581 #@+node:ekr.20110605121601.17877: *5* qtree.drawTree
582 def drawTree(self, p, parent_item=None):
583 if g.app.gui.isNullGui:
584 return
585 # Draw the (visible) parent node.
586 item = self.drawNode(p, parent_item)
587 # Draw all the visible children.
588 self.drawChildren(p, parent_item=item)
589 #@+node:ekr.20110605121601.17878: *5* qtree.initData
590 def initData(self):
591 self.item2positionDict = {}
592 self.item2vnodeDict = {}
593 self.position2itemDict = {}
594 self.vnode2itemsDict = {}
595 self.editWidgetsDict = {}
596 #@+node:ekr.20110605121601.17880: *4* qtree.redraw_after_contract
597 def redraw_after_contract(self, p):
599 if self.busy:
600 return
601 self.update_expansion(p)
602 #@+node:ekr.20110605121601.17881: *4* qtree.redraw_after_expand
603 def redraw_after_expand(self, p):
605 if 0: # Does not work. Newly visible nodes do not show children correctly.
606 c = self.c
607 c.selectPosition(p)
608 self.update_expansion(p)
609 else:
610 self.full_redraw(p)
611 # Don't try to shortcut this!
612 #@+node:ekr.20110605121601.17882: *4* qtree.redraw_after_head_changed
613 def redraw_after_head_changed(self):
614 """Redraw all Qt outline items cloned to c.p."""
615 if self.busy:
616 return
617 p = self.c.p
618 if p:
619 h = p.h # 2010/02/09: Fix bug 518823.
620 for item in self.vnode2items(p.v):
621 if self.isValidItem(item):
622 self.setItemText(item, h)
623 # Bug fix: 2009/10/06
624 self.redraw_after_icons_changed()
625 #@+node:ekr.20110605121601.17883: *4* qtree.redraw_after_icons_changed
626 def redraw_after_icons_changed(self):
628 if self.busy:
629 return
630 self.redrawCount += 1 # To keep a unit test happy.
631 c = self.c
632 try:
633 self.busy = True
634 # Suppress call to setHeadString in onItemChanged!
635 self.getCurrentItem()
636 for p in c.rootPosition().self_and_siblings(copy=False):
637 # Updates icons in p and all visible descendants of p.
638 self.updateVisibleIcons(p)
639 finally:
640 self.busy = False
641 #@+node:ekr.20110605121601.17884: *4* qtree.redraw_after_select
642 def redraw_after_select(self, p=None):
643 """Redraw the entire tree when an invisible node is selected."""
644 if self.busy:
645 return
646 self.full_redraw(p)
647 # c.redraw_after_select calls tree.select indirectly.
648 # Do not call it again here.
649 #@+node:ekr.20140907201613.18986: *4* qtree.repaint (not used)
650 def repaint(self):
651 """Repaint the widget."""
652 w = self.treeWidget
653 w.repaint()
654 w.resizeColumnToContents(0) # 2009/12/22
655 #@+node:ekr.20180817043619.1: *4* qtree.update_expansion
656 def update_expansion(self, p):
657 """Update expansion bits for p, including all clones."""
658 c = self.c
659 w = self.treeWidget
660 expand = c.shouldBeExpanded(p)
661 if 'drawing' in g.app.debug:
662 g.trace('expand' if expand else 'contract')
663 item = self.position2itemDict.get(p.key())
664 if p:
665 try:
666 # These generate events, which would trigger a full redraw.
667 self.busy = True
668 if expand:
669 w.expandItem(item)
670 else:
671 w.collapseItem(item)
672 finally:
673 self.busy = False
674 w.repaint()
675 else:
676 g.trace('NO P')
677 c.redraw()
678 #@+node:ekr.20110605121601.17885: *3* qtree.Event handlers
679 #@+node:ekr.20110605121601.17887: *4* qtree.Click Box
680 #@+node:ekr.20110605121601.17888: *5* qtree.onClickBoxClick
681 def onClickBoxClick(self, event, p=None):
682 if self.busy:
683 return
684 c = self.c
685 g.doHook("boxclick1", c=c, p=p, event=event)
686 g.doHook("boxclick2", c=c, p=p, event=event)
687 c.outerUpdate()
688 #@+node:ekr.20110605121601.17889: *5* qtree.onClickBoxRightClick
689 def onClickBoxRightClick(self, event, p=None):
690 if self.busy:
691 return
692 c = self.c
693 g.doHook("boxrclick1", c=c, p=p, event=event)
694 g.doHook("boxrclick2", c=c, p=p, event=event)
695 c.outerUpdate()
696 #@+node:ekr.20110605121601.17890: *5* qtree.onPlusBoxRightClick
697 def onPlusBoxRightClick(self, event, p=None):
698 if self.busy:
699 return
700 c = self.c
701 g.doHook('rclick-popup', c=c, p=p, event=event, context_menu='plusbox')
702 c.outerUpdate()
703 #@+node:ekr.20110605121601.17891: *4* qtree.Icon Box
704 # For Qt, there seems to be no way to trigger these events.
705 #@+node:ekr.20110605121601.17892: *5* qtree.onIconBoxClick
706 def onIconBoxClick(self, event, p=None):
707 if self.busy:
708 return
709 c = self.c
710 g.doHook("iconclick1", c=c, p=p, event=event)
711 g.doHook("iconclick2", c=c, p=p, event=event)
712 c.outerUpdate()
713 #@+node:ekr.20110605121601.17893: *5* qtree.onIconBoxRightClick
714 def onIconBoxRightClick(self, event, p=None):
715 """Handle a right click in any outline widget."""
716 if self.busy:
717 return
718 c = self.c
719 g.doHook("iconrclick1", c=c, p=p, event=event)
720 g.doHook("iconrclick2", c=c, p=p, event=event)
721 c.outerUpdate()
722 #@+node:ekr.20110605121601.17894: *5* qtree.onIconBoxDoubleClick
723 def onIconBoxDoubleClick(self, event, p=None):
724 if self.busy:
725 return
726 c = self.c
727 if not p:
728 p = c.p
729 if not g.doHook("icondclick1", c=c, p=p, event=event):
730 self.endEditLabel()
731 self.OnIconDoubleClick(p) # Call the method in the base class.
732 g.doHook("icondclick2", c=c, p=p, event=event)
733 c.outerUpdate()
734 #@+node:ekr.20110605121601.18437: *4* qtree.onContextMenu
735 def onContextMenu(self, point):
736 """LeoQtTree: Callback for customContextMenuRequested events."""
737 # #1286.
738 c, w = self.c, self.treeWidget
739 g.app.gui.onContextMenu(c, w, point)
740 #@+node:ekr.20110605121601.17896: *4* qtree.onItemClicked
741 def onItemClicked(self, item, col):
742 """Handle a click in a BaseNativeTree widget item."""
743 # This is called after an item is selected.
744 if self.busy:
745 return
746 c = self.c
747 try:
748 self.busy = True
749 p = self.item2position(item)
750 if p:
751 auto_edit = self.prev_v == p.v
752 # Fix #1049.
753 self.prev_v = p.v
754 event = None
755 #
756 # Careful. We may have switched gui during unit testing.
757 if hasattr(g.app.gui, 'qtApp'):
758 mods = g.app.gui.qtApp.keyboardModifiers()
759 isCtrl = bool(mods & KeyboardModifier.ControlModifier)
760 # We could also add support for QtConst.ShiftModifier, QtConst.AltModifier
761 # & QtConst.MetaModifier.
762 if isCtrl:
763 if g.doHook("iconctrlclick1", c=c, p=p, event=event) is None:
764 c.frame.tree.OnIconCtrlClick(p)
765 # Call the base class method.
766 g.doHook("iconctrlclick2", c=c, p=p, event=event)
767 else:
768 # 2014/02/21: generate headclick1/2 instead of iconclick1/2
769 g.doHook("headclick1", c=c, p=p, event=event)
770 g.doHook("headclick2", c=c, p=p, event=event)
771 else:
772 auto_edit = None
773 g.trace('*** no p')
774 # 2011/05/27: click here is like ctrl-g.
775 c.k.keyboardQuit(setFocus=False)
776 c.treeWantsFocus() # 2011/05/08: Focus must stay in the tree!
777 c.outerUpdate()
778 # 2011/06/01: A second *single* click on a selected node
779 # enters editing state.
780 if auto_edit and self.auto_edit:
781 e, wrapper = self.createTreeEditorForItem(item)
782 finally:
783 self.busy = False
784 #@+node:ekr.20110605121601.17895: *4* qtree.onItemCollapsed
785 def onItemCollapsed(self, item):
787 if self.busy:
788 return
789 c = self.c
790 p = self.item2position(item)
791 if not p:
792 self.error('no p')
793 return
794 # Do **not** set lockouts here.
795 # Only methods that actually generate events should set lockouts.
796 if p.isExpanded():
797 p.contract()
798 c.redraw_after_contract(p)
799 self.select(p)
800 c.outerUpdate()
801 #@+node:ekr.20110605121601.17897: *4* qtree.onItemDoubleClicked
802 def onItemDoubleClicked(self, item, col):
803 """Handle a double click in a BaseNativeTree widget item."""
804 if self.busy: # Required.
805 return
806 c = self.c
807 try:
808 self.busy = True
809 e, wrapper = self.createTreeEditorForItem(item)
810 if not e:
811 g.trace('*** no e')
812 p = self.item2position(item)
813 # 2011/07/28: End the lockout here, not at the end.
814 finally:
815 self.busy = False
816 if not p:
817 self.error('no p')
818 return
819 # 2014/02/21: generate headddlick1/2 instead of icondclick1/2.
820 if g.doHook("headdclick1", c=c, p=p, event=None) is None:
821 c.frame.tree.OnIconDoubleClick(p) # Call the base class method.
822 g.doHook("headclick2", c=c, p=p, event=None)
823 c.outerUpdate()
824 #@+node:ekr.20110605121601.17898: *4* qtree.onItemExpanded
825 def onItemExpanded(self, item):
826 """Handle and tree-expansion event."""
827 if self.busy: # Required
828 return
829 c = self.c
830 p = self.item2position(item)
831 if not p:
832 self.error('no p')
833 return
834 # Do **not** set lockouts here.
835 # Only methods that actually generate events should set lockouts.
836 if not p.isExpanded():
837 p.expand()
838 c.redraw_after_expand(p)
839 self.select(p)
840 c.outerUpdate()
841 #@+node:ekr.20110605121601.17899: *4* qtree.onTreeSelect
842 def onTreeSelect(self):
843 """Select the proper position when a tree node is selected."""
844 if self.busy: # Required
845 return
846 c = self.c
847 item = self.getCurrentItem()
848 p = self.item2position(item)
849 if not p:
850 self.error(f"no p for item: {item}")
851 return
852 # Do **not** set lockouts here.
853 # Only methods that actually generate events should set lockouts.
854 self.select(p)
855 # This is a call to LeoTree.select(!!)
856 c.outerUpdate()
857 #@+node:ekr.20110605121601.17944: *3* qtree.Focus
858 def getFocus(self):
859 return g.app.gui.get_focus(self.c) # Bug fix: 2009/6/30
861 findFocus = getFocus
863 def setFocus(self):
864 g.app.gui.set_focus(self.c, self.treeWidget)
865 #@+node:ekr.20110605121601.18409: *3* qtree.Icons
866 #@+node:ekr.20110605121601.18410: *4* qtree.drawIcon
867 def drawIcon(self, p):
868 """Redraw the icon at p."""
869 return self.updateIcon(p)
870 # the following code is wrong. It constructs a new item
871 # and assignes the icon to it. However this item is never
872 # added to the treeWidget so it is soon garbage collected
873 # w = self.treeWidget
874 # itemOrTree = self.position2item(p) or w
875 # item = QtWidgets.QTreeWidgetItem(itemOrTree)
876 # icon = self.getIcon(p)
877 # self.setItemIcon(item, icon)
878 #@+node:ekr.20110605121601.18411: *4* qtree.getIcon & helper
879 def getIcon(self, p):
880 """Return the proper icon for position p."""
881 if self.use_declutter:
882 item = self.position2item(p)
883 return item and self.declutter_node(self.c, p, item)
884 p.v.iconVal = iv = p.v.computeIcon()
885 return self.getCompositeIconImage(p, iv)
888 #@+node:vitalije.20200329153148.1: *5* qtree.icon_filenames_for_node
889 def icon_filenames_for_node(self, p, val):
890 """Prepares and returns a list of icon filenames
891 related to this node.
892 """
893 nicon = f'box{val:02d}.png'
894 fnames = self.nodeIconsDict.get(p.gnx)
895 if not fnames:
896 icons = self.c.editCommands.getIconList(p)
897 fnames = [x['file'] for x in icons if x['where'] == 'beforeIcon']
898 fnames.append(nicon)
899 fnames.extend(x['file'] for x in icons if x['where'] == 'beforeHeadline')
900 self.nodeIconsDict[p.gnx] = fnames
901 pat = re.compile(r'^box\d\d\.png$')
902 loaded_images = self.loaded_images
903 for i, f in enumerate(fnames):
904 if pat.match(f):
905 fnames[i] = nicon
906 self.nodeIconsDict[p.gnx] = fnames
907 f = nicon
908 if f not in loaded_images:
909 loaded_images[f] = g.app.gui.getImageImage(f)
910 return fnames
911 #@+node:vitalije.20200329153154.1: *5* qtree.make_composite_icon
912 def make_composite_icon(self, images):
913 hsep = self.c.config.getInt('tree-icon-separation') or 0
914 images = [x for x in images if x]
915 height = max([i.height() for i in images])
916 images = [i.scaledToHeight(height) for i in images]
917 width = sum([i.width() for i in images]) + hsep * (len(images) - 1)
918 pix = QtGui.QImage(width, height, Format.Format_ARGB32_Premultiplied)
919 pix.fill(QtGui.QColor(0, 0, 0, 0).rgba()) # transparent fill, rgbA
920 # .rgba() call required for Qt4.7, later versions work with straight color
921 painter = QtGui.QPainter()
922 if not painter.begin(pix):
923 print("Failed to init. painter for icon")
924 # don't return, the code still makes an icon for the cache
925 # which stops this being called again and again
926 x = 0
927 for i in images:
928 painter.drawPixmap(x, 0, i)
929 x += i.width() + hsep
930 painter.end()
931 return QtGui.QIcon(QtGui.QPixmap.fromImage(pix))
932 #@+node:ekr.20110605121601.18412: *5* qtree.getCompositeIconImage
933 def getCompositeIconImage(self, p, val):
934 """Get the icon at position p."""
935 fnames = self.icon_filenames_for_node(p, val)
936 h = ':'.join(fnames)
937 icon = g.app.gui.iconimages.get(h)
938 loaded_images = self.loaded_images
939 images = list(map(loaded_images.get, fnames))
940 if not icon:
941 icon = self.make_composite_icon(images)
942 g.app.gui.iconimages[h] = icon
943 return icon
944 #@+node:ekr.20110605121601.17950: *4* qtree.setItemIcon
945 def setItemIcon(self, item, icon):
947 valid = item and self.isValidItem(item)
948 if icon and valid:
949 # Important: do not set lockouts here.
950 # This will generate changed events,
951 # but there is no itemChanged event handler.
952 item.setIcon(0, icon)
954 #@+node:ekr.20110605121601.17951: *4* qtree.updateIcon & updateAllIcons
955 def updateIcon(self, p):
956 """Update p's icon."""
957 if not p:
958 return
959 val = p.v.computeIcon()
960 if p.v.iconVal != val:
961 self.nodeIconsDict.pop(p.gnx, None)
962 self.getIcon(p) # sets p.v.iconVal
964 def updateAllIcons(self, p):
965 if not p:
966 return
967 self.nodeIconsDict.pop(p.gnx, None)
968 icon = self.getIcon(p) # sets p.v.iconVal
969 # Update all cloned items.
970 items = self.vnode2items(p.v)
971 for item in items:
972 self.setItemIcon(item, icon)
973 #@+node:ekr.20110605121601.17952: *4* qtree.updateVisibleIcons
974 def updateVisibleIcons(self, p):
975 """Update the icon for p and the icons
976 for all visible descendants of p."""
977 self.updateAllIcons(p)
978 if p.hasChildren() and p.isExpanded():
979 for child in p.children():
980 self.updateVisibleIcons(child)
981 #@+node:ekr.20110605121601.18414: *3* qtree.Items
982 #@+node:ekr.20110605121601.17943: *4* qtree.item dict getters
983 def itemHash(self, item):
984 return f"{repr(item)} at {str(id(item))}"
986 def item2position(self, item):
987 itemHash = self.itemHash(item)
988 p = self.item2positionDict.get(itemHash) # was item
989 return p
991 def item2vnode(self, item):
992 itemHash = self.itemHash(item)
993 return self.item2vnodeDict.get(itemHash) # was item
995 def position2item(self, p):
996 item = self.position2itemDict.get(p.key())
997 return item
999 def vnode2items(self, v):
1000 return self.vnode2itemsDict.get(v, [])
1002 def isValidItem(self, item):
1003 itemHash = self.itemHash(item)
1004 return itemHash in self.item2vnodeDict # was item.
1005 #@+node:ekr.20110605121601.18415: *4* qtree.childIndexOfItem
1006 def childIndexOfItem(self, item):
1007 parent = item and item.parent()
1008 if parent:
1009 n = parent.indexOfChild(item)
1010 else:
1011 w = self.treeWidget
1012 n = w.indexOfTopLevelItem(item)
1013 return n
1014 #@+node:ekr.20110605121601.18416: *4* qtree.childItems
1015 def childItems(self, parent_item):
1016 """
1017 Return the list of child items of the parent item,
1018 or the top-level items if parent_item is None.
1019 """
1020 if parent_item:
1021 n = parent_item.childCount()
1022 items = [parent_item.child(z) for z in range(n)]
1023 else:
1024 w = self.treeWidget
1025 n = w.topLevelItemCount()
1026 items = [w.topLevelItem(z) for z in range(n)]
1027 return items
1028 #@+node:ekr.20110605121601.18418: *4* qtree.connectEditorWidget & callback
1029 def connectEditorWidget(self, e, item):
1030 """
1031 Connect QLineEdit e to QTreeItem item.
1033 Also callback for when the editor ends.
1035 New in Leo 6.4: The callback handles all updates w/o calling onHeadChanged.
1036 """
1037 c, p, u = self.c, self.c.p, self.c.undoer
1038 #@+others # define the callback.
1039 #@+node:ekr.20201109043641.1: *5* function: editingFinished_callback
1040 def editingFinished_callback():
1041 """Called when Qt emits the editingFinished signal."""
1042 s = e.text()
1043 i = s.find('\n')
1044 # Truncate to one line.
1045 if i > -1:
1046 s = s[:i]
1047 # #1310: update the tooltip.
1048 if p.h != s:
1049 # Update p.h and handle undo.
1050 item.setToolTip(0, s)
1051 undoData = u.beforeChangeHeadline(p)
1052 p.v.setHeadString(s) # Set v.h *after* calling the undoer's before method.
1053 if not c.changed:
1054 c.setChanged()
1055 # We must recolor the body because
1056 # the headline may contain directives.
1057 c.frame.body.recolor(p)
1058 p.setDirty()
1059 u.afterChangeHeadline(p, 'Edit Headline', undoData)
1060 self.redraw_after_head_changed()
1061 c.outerUpdate()
1062 #@-others
1063 if e:
1064 # Hook up the widget.
1065 wrapper = self.getWrapper(e, item)
1066 e.editingFinished.connect(editingFinished_callback)
1067 return wrapper # 2011/02/12
1068 g.trace('can not happen: no e')
1069 return None
1070 #@+node:ekr.20110605121601.18419: *4* qtree.contractItem & expandItem
1071 def contractItem(self, item):
1072 self.treeWidget.collapseItem(item)
1074 def expandItem(self, item):
1075 self.treeWidget.expandItem(item)
1076 #@+node:ekr.20110605121601.18420: *4* qtree.createTreeEditorForItem
1077 def createTreeEditorForItem(self, item):
1079 c = self.c
1080 w = self.treeWidget
1081 w.setCurrentItem(item) # Must do this first.
1082 if self.use_declutter:
1083 item.setText(0, item._real_text)
1084 w.editItem(item)
1085 e = w.itemWidget(item, 0) # e is a QLineEdit
1086 e.setObjectName('headline')
1087 wrapper = self.connectEditorWidget(e, item)
1088 self.sizeTreeEditor(c, e)
1089 return e, wrapper
1090 #@+node:ekr.20110605121601.18421: *4* qtree.createTreeItem
1091 def createTreeItem(self, p, parent_item):
1093 w = self.treeWidget
1094 itemOrTree = parent_item or w
1095 item = QtWidgets.QTreeWidgetItem(itemOrTree)
1096 if isQt6:
1097 item.setFlags(item.flags() | ItemFlag.ItemIsEditable)
1098 ChildIndicatorPolicy = QtWidgets.QTreeWidgetItem.ChildIndicatorPolicy
1099 item.setChildIndicatorPolicy(ChildIndicatorPolicy.DontShowIndicatorWhenChildless) # pylint: disable=no-member
1100 else:
1101 item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable | item.DontShowIndicatorWhenChildless)
1102 try:
1103 g.visit_tree_item(self.c, p, item)
1104 except leoPlugins.TryNext:
1105 pass
1106 return item
1107 #@+node:ekr.20110605121601.18423: *4* qtree.getCurrentItem
1108 def getCurrentItem(self):
1109 w = self.treeWidget
1110 return w.currentItem()
1111 #@+node:ekr.20110605121601.18424: *4* qtree.getItemText
1112 def getItemText(self, item):
1113 """Return the text of the item."""
1114 return item.text(0) if item else '<no item>'
1115 #@+node:ekr.20110605121601.18425: *4* qtree.getParentItem
1116 def getParentItem(self, item):
1117 return item and item.parent()
1118 #@+node:ekr.20110605121601.18426: *4* qtree.getSelectedItems
1119 def getSelectedItems(self):
1120 w = self.treeWidget
1121 return w.selectedItems()
1122 #@+node:ekr.20110605121601.18427: *4* qtree.getTreeEditorForItem
1123 def getTreeEditorForItem(self, item):
1124 """Return the edit widget if it exists.
1125 Do *not* create one if it does not exist.
1126 """
1127 w = self.treeWidget
1128 e = w.itemWidget(item, 0)
1129 return e
1130 #@+node:ekr.20110605121601.18428: *4* qtree.getWrapper
1131 def getWrapper(self, e, item):
1132 """Return headlineWrapper that wraps e (a QLineEdit)."""
1133 c = self.c
1134 if e:
1135 wrapper = self.editWidgetsDict.get(e)
1136 if wrapper:
1137 pass
1138 else:
1139 if item:
1140 # 2011/02/12: item can be None.
1141 wrapper = self.headlineWrapper(c, item, name='head', widget=e)
1142 self.editWidgetsDict[e] = wrapper
1143 return wrapper
1144 g.trace('no e')
1145 return None
1146 #@+node:ekr.20110605121601.18429: *4* qtree.nthChildItem
1147 def nthChildItem(self, n, parent_item):
1148 children = self.childItems(parent_item)
1149 if n < len(children):
1150 item = children[n]
1151 else:
1152 # This is **not* an error.
1153 # It simply means that we need to redraw the tree.
1154 item = None
1155 return item
1156 #@+node:ekr.20110605121601.18430: *4* qtree.scrollToItem
1157 def scrollToItem(self, item):
1158 """
1159 Scroll the tree widget so that item is visible.
1160 Leo's core no longer calls this method.
1161 """
1162 w = self.treeWidget
1163 hPos, vPos = self.getScroll()
1164 w.scrollToItem(item, w.EnsureVisible)
1165 # Fix #265: Erratic scrolling bug.
1166 # w.PositionAtCenter causes unwanted scrolling.
1167 self.setHScroll(0)
1168 # Necessary
1169 #@+node:ekr.20110605121601.18431: *4* qtree.setCurrentItemHelper
1170 def setCurrentItemHelper(self, item):
1171 w = self.treeWidget
1172 w.setCurrentItem(item)
1173 #@+node:ekr.20110605121601.18432: *4* qtree.setItemText
1174 def setItemText(self, item, s):
1175 if item:
1176 item.setText(0, s)
1177 if self.use_declutter:
1178 item._real_text = s
1179 #@+node:tbrown.20160406221505.1: *4* qtree.sizeTreeEditor
1180 @staticmethod
1181 def sizeTreeEditor(c, editor):
1182 """Size a QLineEdit in a tree headline so scrolling occurs"""
1183 # space available in tree widget
1184 space = c.frame.tree.treeWidget.size().width()
1185 # left hand edge of editor within tree widget
1186 used = editor.geometry().x() + 4 # + 4 for edit cursor
1187 # limit width to available space
1188 editor.resize(space - used, editor.size().height())
1189 #@+node:ekr.20110605121601.18433: *3* qtree.Scroll bars
1190 #@+node:ekr.20110605121601.18434: *4* qtree.getSCroll
1191 def getScroll(self):
1192 """Return the hPos,vPos for the tree's scrollbars."""
1193 w = self.treeWidget
1194 hScroll = w.horizontalScrollBar()
1195 vScroll = w.verticalScrollBar()
1196 hPos = hScroll.sliderPosition()
1197 vPos = vScroll.sliderPosition()
1198 return hPos, vPos
1199 #@+node:btheado.20111110215920.7164: *4* qtree.scrollDelegate
1200 def scrollDelegate(self, kind):
1201 """
1202 Scroll a QTreeWidget up or down or right or left.
1203 kind is in ('down-line','down-page','up-line','up-page', 'right', 'left')
1204 """
1205 c = self.c
1206 w = self.treeWidget
1207 if kind in ('left', 'right'):
1208 hScroll = w.horizontalScrollBar()
1209 if kind == 'right':
1210 delta = hScroll.pageStep()
1211 else:
1212 delta = -hScroll.pageStep()
1213 hScroll.setValue(hScroll.value() + delta)
1214 else:
1215 vScroll = w.verticalScrollBar()
1216 h = w.size().height()
1217 lineSpacing = w.fontMetrics().lineSpacing()
1218 n = h / lineSpacing
1219 if kind == 'down-half-page':
1220 delta = n / 2
1221 elif kind == 'down-line':
1222 delta = 1
1223 elif kind == 'down-page':
1224 delta = n
1225 elif kind == 'up-half-page':
1226 delta = -n / 2
1227 elif kind == 'up-line':
1228 delta = -1
1229 elif kind == 'up-page':
1230 delta = -n
1231 else:
1232 delta = 0
1233 g.trace('bad kind:', kind)
1234 val = vScroll.value()
1235 vScroll.setValue(val + delta)
1236 c.treeWantsFocus()
1237 #@+node:ekr.20110605121601.18435: *4* qtree.setH/VScroll
1238 def setHScroll(self, hPos):
1240 w = self.treeWidget
1241 hScroll = w.horizontalScrollBar()
1242 hScroll.setValue(hPos)
1244 def setVScroll(self, vPos):
1246 w = self.treeWidget
1247 vScroll = w.verticalScrollBar()
1248 vScroll.setValue(vPos)
1249 #@+node:ekr.20110605121601.17905: *3* qtree.Selecting & editing
1250 #@+node:ekr.20110605121601.17908: *4* qtree.edit_widget
1251 def edit_widget(self, p):
1252 """Returns the edit widget for position p."""
1253 item = self.position2item(p)
1254 if item:
1255 e = self.getTreeEditorForItem(item)
1256 if e:
1257 # Create a wrapper widget for Leo's core.
1258 w = self.getWrapper(e, item)
1259 return w
1260 # This is not an error
1261 # But warning: calling this method twice might not work!
1262 return None
1263 return None
1264 #@+node:ekr.20110605121601.17909: *4* qtree.editLabel and helper
1265 def editLabel(self, p, selectAll=False, selection=None):
1266 """Start editing p's headline."""
1267 if self.busy:
1268 return None
1269 c = self.c
1270 c.outerUpdate()
1271 # Do any scheduled redraw.
1272 # This won't do anything in the new redraw scheme.
1273 item = self.position2item(p)
1274 if item:
1275 if self.use_declutter:
1276 item.setText(0, item._real_text)
1277 e, wrapper = self.editLabelHelper(item, selectAll, selection)
1278 else:
1279 e, wrapper = None, None
1280 self.error(f"no item for {p}")
1281 if e:
1282 self.sizeTreeEditor(c, e)
1283 # A nice hack: just set the focus request.
1284 c.requestedFocusWidget = e
1285 return e, wrapper
1286 #@+node:ekr.20110605121601.18422: *5* qtree.editLabelHelper
1287 def editLabelHelper(self, item, selectAll=False, selection=None):
1288 """Helper for qtree.editLabel."""
1289 c, vc = self.c, self.c.vimCommands
1290 w = self.treeWidget
1291 w.setCurrentItem(item)
1292 # Must do this first.
1293 # This generates a call to onTreeSelect.
1294 w.editItem(item)
1295 # Generates focus-in event that tree doesn't report.
1296 e = w.itemWidget(item, 0) # A QLineEdit.
1297 s = e.text()
1298 if s == 'newHeadline':
1299 selectAll = True
1300 if selection:
1301 # pylint: disable=unpacking-non-sequence
1302 # Fix bug https://groups.google.com/d/msg/leo-editor/RAzVPihqmkI/-tgTQw0-LtwJ
1303 # Note: negative lengths are allowed.
1304 i, j, ins = selection
1305 if ins is None:
1306 start, n = i, abs(i - j)
1307 # This case doesn't happen for searches.
1308 elif ins == j:
1309 start, n = i, j - i
1310 else:
1311 start = start, n = j, i - j
1312 elif selectAll:
1313 start, n, ins = 0, len(s), len(s)
1314 else:
1315 start, n, ins = len(s), 0, len(s)
1316 e.setObjectName('headline')
1317 e.setSelection(start, n)
1318 # e.setCursorPosition(ins) # Does not work.
1319 e.setFocus()
1320 wrapper = self.connectEditorWidget(e, item) # Hook up the widget.
1321 if vc and c.vim_mode: # and selectAll
1322 # For now, *always* enter insert mode.
1323 if vc.is_text_wrapper(wrapper):
1324 vc.begin_insert_mode(w=wrapper)
1325 else:
1326 g.trace('not a text widget!', wrapper)
1327 return e, wrapper
1328 #@+node:ekr.20110605121601.17911: *4* qtree.endEditLabel
1329 def endEditLabel(self):
1330 """
1331 Override LeoTree.endEditLabel.
1333 Just end editing of the presently-selected QLineEdit!
1334 This will trigger the editingFinished_callback defined in createEditorForItem.
1335 """
1336 item = self.getCurrentItem()
1337 if not item:
1338 return
1339 e = self.getTreeEditorForItem(item)
1340 if not e:
1341 return
1342 # Trigger the end-editing event.
1343 w = self.treeWidget
1344 w.closeEditor(e, EndEditHint.NoHint)
1345 w.setCurrentItem(item)
1346 #@+node:ekr.20110605121601.17915: *4* qtree.getSelectedPositions
1347 def getSelectedPositions(self):
1348 items = self.getSelectedItems()
1349 pl = leoNodes.PosList(self.item2position(it) for it in items)
1350 return pl
1351 #@+node:ekr.20110605121601.17914: *4* qtree.setHeadline
1352 def setHeadline(self, p, s):
1353 """Force the actual text of the headline widget to p.h."""
1354 # This is used by unit tests to force the headline and p into alignment.
1355 if not p:
1356 return
1357 # Don't do this here: the caller should do it.
1358 # p.setHeadString(s)
1359 e = self.edit_widget(p)
1360 if e:
1361 e.setAllText(s)
1362 else:
1363 item = self.position2item(p)
1364 if item:
1365 self.setItemText(item, s)
1366 #@+node:ekr.20110605121601.17913: *4* qtree.setItemForCurrentPosition
1367 def setItemForCurrentPosition(self):
1368 """Select the item for c.p"""
1369 p = self.c.p
1370 if self.busy:
1371 return None
1372 if not p:
1373 return None
1374 item = self.position2item(p)
1375 if not item:
1376 # This is not necessarily an error.
1377 # We often attempt to select an item before redrawing it.
1378 return None
1379 item2 = self.getCurrentItem()
1380 if item == item2:
1381 return item
1382 try:
1383 self.busy = True
1384 self.treeWidget.setCurrentItem(item)
1385 # This generates gui events, so we must use a lockout.
1386 finally:
1387 self.busy = False
1388 return item
1389 #@+node:ekr.20190613080606.1: *4* qtree.unselectItem
1390 def unselectItem(self, p):
1392 item = self.position2item(p)
1393 if item:
1394 item.setSelected(False)
1395 #@-others
1396#@-others
1397#@@language python
1398#@@tabwidth -4
1399#@@pagewidth 80
1400#@-leo