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
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
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
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 # 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):
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):
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):
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):
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
858 findFocus = getFocus
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)
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):
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)
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
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))}"
983 def item2position(self, item):
984 itemHash = self.itemHash(item)
985 p = self.item2positionDict.get(itemHash) # was item
986 return p
988 def item2vnode(self, item):
989 itemHash = self.itemHash(item)
990 return self.item2vnodeDict.get(itemHash) # was item
992 def position2item(self, p):
993 item = self.position2itemDict.get(p.key())
994 return item
996 def vnode2items(self, v):
997 return self.vnode2itemsDict.get(v, [])
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.
1030 Also callback for when the editor ends.
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)
1071 def expandItem(self, item):
1072 self.treeWidget.expandItem(item)
1073 #@+node:ekr.20110605121601.18420: *4* qtree.createTreeEditorForItem
1074 def createTreeEditorForItem(self, item):
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):
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):
1237 w = self.treeWidget
1238 hScroll = w.horizontalScrollBar()
1239 hScroll.setValue(hPos)
1241 def setVScroll(self, vPos):
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.
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):
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