Coverage for C:\Repos\leo-editor\leo\plugins\qt_tree.py: 15%
957 statements
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
1# -*- coding: utf-8 -*-
2#@+leo-ver=5-thin
3#@+node:ekr.20140907131341.18707: * @file ../plugins/qt_tree.py
4#@@first
5"""Leo's Qt tree class."""
6#@+<< imports: qt_tree.py >>
7#@+node:ekr.20140907131341.18709: ** << imports: qt_tree.py >>
8import re
9import time
10from typing import Any, Callable, Dict, Generator, List, Tuple
11from typing import TYPE_CHECKING
12from leo.core.leoQt import isQt6, QtCore, QtGui, QtWidgets
13from leo.core.leoQt import EndEditHint, Format, ItemFlag, KeyboardModifier
14from leo.core import leoGlobals as g
15from leo.core import leoFrame
16from leo.core import leoNodes
17from leo.core import leoPlugins # Uses leoPlugins.TryNext.
18from leo.plugins import qt_text
19#@-<< imports: qt_tree.py >>
20#@+<< type aliases: qt_tree.py >>
21#@+node:ekr.20220417193741.1: ** << type aliases: qt_tree.py >>
22if TYPE_CHECKING: # Always False at runtime.
23 from leo.core.leoCommands import Commands as Cmdr
24 from leo.core.leoNodes import Position as Pos
25 from leo.core.leoNodes import VNode
26else:
27 Cmdr = Any
28 Pos = Any
29 VNode = Any
30Editor = Any
31Event = Any
32Icon = Any
33Item = Any
34Selection = Tuple[int, int, int]
35Widget = Any
36Wrapper = Any
37#@-<< type aliases: qt_tree.py >>
38#@+others
39#@+node:ekr.20160514120051.1: ** class LeoQtTree
40class LeoQtTree(leoFrame.LeoTree):
41 """Leo Qt tree class"""
42 #@+others
43 #@+node:ekr.20110605121601.18404: *3* qtree.Birth
44 #@+node:ekr.20110605121601.18405: *4* qtree.__init__
45 def __init__(self, c: Cmdr, frame: Wrapper) -> None:
46 """Ctor for the LeoQtTree class."""
47 super().__init__(frame)
48 self.c = c
49 # Widget independent status ivars...
50 self.prev_v = None
51 self.redrawCount = 0 # Count for debugging.
52 self.revertHeadline = None # Previous headline text for abortEditLabel.
53 self.busy = False
54 # Debugging...
55 self.traceCallersFlag = False # Enable traceCallers method.
56 # Associating items with position and vnodes...
57 self.items: List[Item] = []
58 self.item2positionDict: Dict[str, Pos] = {} # Keys are gnxs.
59 self.item2vnodeDict: Dict[str, VNode] = {} # Keys are gnxs.
60 self.nodeIconsDict: Dict[str, List[Icon]] = {} # keys are gnxs, values are declutter generated icons
61 self.position2itemDict: Dict[str, Item] = {} # Keys are gnxs.
62 self.vnode2itemsDict: Dict[VNode, List[Item]] = {} # values are lists of items.
63 self.editWidgetsDict: Dict[Editor, Wrapper] = {} # keys are native edit widgets, values are wrappers.
64 self.reloadSettings()
65 # Components.
66 self.canvas: Wrapper = self # An official ivar used by Leo's core.
67 self.headlineWrapper: Wrapper = qt_text.QHeadlineWrapper # This is a class.
68 # w is a LeoQTreeWidget, a subclass of QTreeWidget.
69 self.treeWidget: Widget = frame.top.treeWidget # An internal ivar.
70 w = self.treeWidget
71 #
72 # "declutter", node appearance tweaking
73 self.declutter_patterns: List[Any] = None # list of pairs of patterns for decluttering
74 self.declutter_data: Dict[Any, Any] = {}
75 self.loaded_images: Dict[str, Icon] = {}
76 if 0: # Drag and drop
77 w.setDragEnabled(True)
78 w.viewport().setAcceptDrops(True)
79 w.showDropIndicator = True
80 w.setAcceptDrops(True)
81 w.setDragDropMode(w.InternalMove)
82 if 1: # Does not work
84 def dropMimeData(self, data: str, action: str, row: str, col: str, parent: str) -> None:
85 g.trace()
87 # w.dropMimeData = dropMimeData
89 def mimeData(self, indexes: str) -> None:
90 g.trace()
92 # Early inits...
94 try:
95 w.headerItem().setHidden(True)
96 except Exception:
97 pass
98 n = c.config.getInt('icon-height') or 16
99 w.setIconSize(QtCore.QSize(160, n))
100 #@+node:ekr.20110605121601.17866: *4* qtree.get_name
101 def getName(self) -> str:
102 """Return the name of this widget: must start with "canvas"."""
103 return 'canvas(tree)'
104 #@+node:ekr.20110605121601.18406: *4* qtree.initAfterLoad
105 def initAfterLoad(self) -> None:
106 """Do late-state inits."""
107 # Called by Leo's core.
108 c = self.c
109 # w = c.frame.top
110 tw = self.treeWidget
111 tw.itemDoubleClicked.connect(self.onItemDoubleClicked)
112 tw.itemClicked.connect(self.onItemClicked)
113 tw.itemSelectionChanged.connect(self.onTreeSelect)
114 tw.itemCollapsed.connect(self.onItemCollapsed)
115 tw.itemExpanded.connect(self.onItemExpanded)
116 tw.customContextMenuRequested.connect(self.onContextMenu)
117 # tw.onItemChanged.connect(self.onItemChanged)
118 g.app.gui.setFilter(c, tw, self, tag='tree')
119 # 2010/01/24: Do not set this here.
120 # The read logic sets c.changed to indicate nodes have changed.
121 # c.clearChanged()
122 #@+node:ekr.20110605121601.17871: *4* qtree.reloadSettings
123 def reloadSettings(self) -> None:
124 """LeoQtTree."""
125 c = self.c
126 self.auto_edit = c.config.getBool('single-click-auto-edits-headline', False)
127 self.enable_drag_messages = c.config.getBool("enable-drag-messages")
128 self.select_all_text_when_editing_headlines = c.config.getBool(
129 'select_all_text_when_editing_headlines')
130 self.stayInTree = c.config.getBool('stayInTreeAfterSelect')
131 self.use_chapters = c.config.getBool('use-chapters')
132 self.use_declutter = c.config.getBool('tree-declutter', default=False)
133 #@+node:ekr.20110605121601.17940: *4* qtree.wrapQLineEdit
134 def wrapQLineEdit(self, w: Wrapper) -> Wrapper:
135 """A wretched kludge for MacOs k.masterMenuHandler."""
136 c = self.c
137 if isinstance(w, QtWidgets.QLineEdit):
138 wrapper = self.edit_widget(c.p)
139 else:
140 wrapper = w
141 return wrapper
142 #@+node:ekr.20110605121601.17868: *3* qtree.Debugging & tracing
143 def error(self, s: str) -> None:
144 if not g.unitTesting:
145 g.trace('LeoQtTree Error: ', s, g.callers())
147 def traceItem(self, item: Item) -> str:
148 if item:
149 # A QTreeWidgetItem.
150 return f"item {id(item)}: {self.getItemText(item)}"
151 return '<no item>'
153 def traceCallers(self) -> str:
154 if self.traceCallersFlag:
155 return g.callers(5, excludeCaller=True)
156 return ''
157 #@+node:ekr.20110605121601.17872: *3* qtree.Drawing
158 #@+node:ekr.20110605121601.18408: *4* qtree.clear
159 def clear(self) -> None:
160 """Clear all widgets in the tree."""
161 w = self.treeWidget
162 w.clear()
163 #@+node:ekr.20180810052056.1: *4* qtree.drawVisible & helpers (not used)
164 def drawVisible(self, p: Pos) -> None:
165 """
166 Add only the visible nodes to the outline.
168 Not used, as this causes scrolling issues.
169 """
170 t1 = time.process_time()
171 c = self.c
172 parents: List[Any] = []
173 # Clear the widget.
174 w = self.treeWidget
175 w.clear()
176 # Clear the dicts.
177 self.initData()
178 if c.hoistStack:
179 first_p = c.hoistStack[-1].p
180 target_p = first_p.nodeAfterTree().visBack(c)
181 else:
182 first_p = c.rootPosition()
183 target_p = None
184 n = 0
185 for p in self.yieldVisible(first_p, target_p):
186 n += 1
187 level = p.level()
188 parent_item = w if level == 0 else parents[level - 1]
189 item = QtWidgets.QTreeWidgetItem(parent_item)
190 item.setFlags(item.flags() | ItemFlag.ItemIsEditable)
191 item.setChildIndicatorPolicy(
192 item.ShowIndicator if p.hasChildren()
193 else item.DontShowIndicator)
194 item.setExpanded(bool(p.hasChildren() and p.isExpanded()))
195 self.items.append(item)
196 # Update parents.
197 parents = [] if level == 0 else parents[:level]
198 parents.append(item)
199 # Update the dicts.
200 itemHash = self.itemHash(item)
201 self.item2positionDict[itemHash] = p.copy()
202 self.item2vnodeDict[itemHash] = p.v
203 self.position2itemDict[p.key()] = item
204 d = self.vnode2itemsDict
205 v = p.v
206 aList = d.get(v, [])
207 aList.append(item)
208 d[v] = aList
209 # Enter the headline.
210 item.setText(0, p.h)
211 if self.use_declutter:
212 item._real_text = p.h
213 # Draw the icon.
214 v.iconVal = v.computeIcon()
215 icon = self.getCompositeIconImage(p, v.iconVal)
216 if icon:
217 self.setItemIcon(item, icon)
218 # Set current item.
219 if p == c.p:
220 w.setCurrentItem(item)
221 # Useful, for now.
222 t2 = time.process_time()
223 if t2 - t1 > 0.1:
224 g.trace(f"{n} nodes, {t2 - t1:5.2f} sec")
225 #@+node:ekr.20180810052056.2: *5* qtree.yieldVisible (not used)
226 def yieldVisible(self, first_p: Pos, target_p: Pos=None) -> Generator:
227 """
228 A generator yielding positions from first_p to target_p.
229 """
230 c = self.c
231 p = first_p.copy()
232 yield p
233 while p:
234 if p == target_p:
235 return
236 v = p.v
237 if (v.children and (
238 # Use slower test for clones:
239 len(v.parents) > 1 and p in v.expandedPositions or
240 # Use a quick test for non-clones:
241 len(v.parents) <= 1 and (v.statusBits & v.expandedBit) != 0
242 )):
243 # p.moveToFirstChild()
244 p.stack.append((v, p._childIndex),)
245 p.v = v.children[0]
246 p._childIndex = 0
247 yield p
248 continue
249 # if p.hasNext():
250 parent_v = p.stack[-1][0] if p.stack else c.hiddenRootNode
251 if p._childIndex + 1 < len(parent_v.children):
252 # p.moveToNext()
253 p._childIndex += 1
254 p.v = parent_v.children[p._childIndex]
255 yield p
256 continue
257 #
258 # A fast version of p.moveToThreadNext().
259 # We look for a parent with a following sibling.
260 while p.stack:
261 # p.moveToParent()
262 p.v, p._childIndex = p.stack.pop()
263 # if p.hasNext():
264 parent_v = p.stack[-1][0] if p.stack else c.hiddenRootNode
265 if p._childIndex + 1 < len(parent_v.children):
266 # p.moveToNext()
267 p._childIndex += 1
268 p.v = parent_v.children[p._childIndex]
269 break # Found: moveToThreadNext()
270 else:
271 break # Not found.
272 # Found moveToThreadNext()
273 yield p
274 continue
275 if target_p:
276 g.trace('NOT FOUND:', target_p.h)
277 #@+node:ekr.20180810052056.3: *5* qtree.slowYieldVisible
278 def slowYieldVisible(self, first_p: Pos, target_p: Pos=None) -> Generator:
279 """
280 A generator yielding positions from first_p to target_p.
281 """
282 c = self.c
283 p = first_p.copy()
284 while p:
285 yield p
286 if p == target_p:
287 return
288 p.moveToVisNext(c)
289 if target_p:
290 g.trace('NOT FOUND:', target_p.h)
291 #@+node:ekr.20110605121601.17873: *4* qtree.full_redraw & helpers
292 def full_redraw(self, p: Pos=None) -> Pos:
293 """
294 Redraw all visible nodes of the tree.
295 Preserve the vertical scrolling unless scroll is True.
296 """
297 c = self.c
298 if g.app.disable_redraw:
299 return None
300 if self.busy:
301 return None
302 # Cancel the delayed redraw request.
303 c.requestLaterRedraw = False
304 if not p:
305 p = c.currentPosition()
306 elif c.hoistStack and p.h.startswith('@chapter') and p.hasChildren():
307 # Make sure the current position is visible.
308 # Part of fix of bug 875323: Hoist an @chapter node leaves a non-visible node selected.
309 p = p.firstChild()
310 c.frame.tree.select(p)
311 c.setCurrentPosition(p)
312 else:
313 c.setCurrentPosition(p)
314 assert not self.busy, g.callers()
315 self.redrawCount += 1
316 self.initData()
317 try:
318 self.busy = True
319 self.drawTopTree(p)
320 finally:
321 self.busy = False
322 self.setItemForCurrentPosition()
323 return p # Return the position, which may have changed.
325 # Compatibility
327 # mypy complains that there is a mismatch with the base redraw method.
328 redraw = full_redraw # type:ignore
329 redraw_now = full_redraw #type:ignore
330 #@+node:vitalije.20200329160945.1: *5* tree declutter code
331 #@+node:tbrown.20150807090639.1: *6* qtree.declutter_node & helpers
332 def declutter_node(self, c: Cmdr, p: Pos, item: Item) -> Icon:
333 """declutter_node - change the appearance of a node
335 :param commander c: commander containing node
336 :param position p: position of node
337 :param QWidgetItem item: tree node widget item
339 returns composite icon for this node
340 """
341 dd = self.declutter_data
342 iconVal = p.v.computeIcon()
343 iconName = f'box{iconVal:02d}.png'
344 loaded_images = self.loaded_images
345 #@+others
346 #@+node:vitalije.20200329153544.1: *7* sorted_icons
347 def sorted_icons(p: Pos) -> List[str]:
348 """
349 Returns a list of icon filenames for this node.
350 The list is sorted to owner the 'where' key of image dicts.
351 """
352 icons = c.editCommands.getIconList(p)
353 a = [x['file'] for x in icons if x['where'] == 'beforeIcon']
354 a.append(iconName)
355 a.extend(x['file'] for x in icons if x['where'] == 'beforeHeadline')
356 return a
357 #@+node:ekr.20171122064635.1: *7* declutter_replace
358 def declutter_replace(arg: str, cmd: Callable) -> Tuple[Callable, str]:
359 """
360 Executes cmd if cmd is any replace command and returns
361 pair (commander, s), where 'commander' corresponds
362 to the executed replacement operation, 's' is the substituted string.
363 If cmd is not a replacement command returns (None, None)
364 """
365 # pylint: disable=undefined-loop-variable
367 replacement, s = None, None
369 if cmd == 'REPLACE':
370 s = pattern.sub(arg, text)
371 elif cmd == 'REPLACE-HEAD':
372 s = text[: m.start()].rstrip()
373 elif cmd == 'REPLACE-TAIL':
374 s = text[m.end() :].lstrip()
375 elif cmd == 'REPLACE-REST':
376 s = (text[:m.start] + text[m.end() :]).strip()
378 # 's' is string when 'cmd' is recognised
379 # and is None otherwise
380 if isinstance(s, str):
381 # Save the operation
382 replacement = lambda item, s: item.setText(0, s)
383 # ... and apply it
384 replacement(item, s)
386 return replacement, s
387 #@+node:ekr.20171122055719.1: *7* declutter_style
388 def declutter_style(arg: str, cmd: Callable) -> Tuple[Callable, str]:
389 """
390 Handles style options and returns pair '(commander, param)',
391 where 'commander' is the applied style-modifying operation,
392 param - the saved argument of that operation.
393 Returns (None, param) if 'cmd' is not a style option.
394 """
395 # pylint: disable=function-redefined
396 param = c.styleSheetManager.expand_css_constants(arg).split()[0]
397 modifier: Callable = None
398 if cmd == 'ICON':
399 def modifier(item: Item, param: str) -> None:
400 # Does not fit well this function. And we cannot
401 # wrap list 'new_icons' in a saved argument as
402 # the list is recreated before each call.
403 new_icons.append(param)
404 elif cmd == 'BG':
405 def modifier(item: Item, param: str) -> None:
406 item.setBackground(0, QtGui.QBrush(QtGui.QColor(param)))
407 elif cmd == 'FG':
408 def modifier(item: Item, param: str) -> None:
409 item.setForeground(0, QtGui.QBrush(QtGui.QColor(param)))
410 elif cmd == 'FONT':
411 def modifier(item: Item, param: str) -> None:
412 item.setFont(0, QtGui.QFont(param))
413 elif cmd == 'ITALIC':
414 def modifier(item: Item, param: str) -> None:
415 font = item.font(0)
416 font.setItalic(bool(int(param)))
417 item.setFont(0, font)
418 elif cmd == 'WEIGHT':
419 def modifier(item: Item, param: str) -> None:
420 arg = getattr(QtGui.QFont, param, 75)
421 font = item.font(0)
422 font.setWeight(arg)
423 item.setFont(0, font)
424 elif cmd == 'PX':
425 def modifier(item: Item, param: str) -> None:
426 font = item.font(0)
427 font.setPixelSize(int(param))
428 item.setFont(0, font)
429 elif cmd == 'PT':
430 def modifier(item: Item, param: str) -> None:
431 font = item.font(0)
432 font.setPointSize(int(param))
433 item.setFont(0, font)
434 # Apply the style update
435 if modifier:
436 modifier(item, param)
437 return modifier, param
438 #@+node:vitalije.20200327163522.1: *7* apply_declutter_rules
439 def apply_declutter_rules(cmds: List[Tuple[Callable, str]]) -> List[Any]:
440 """
441 Applies all commands for the matched rule. Returns the list
442 of the applied operations paired with their single parameter.
443 """
444 modifiers = []
445 for cmd, arg in cmds:
446 modifier, param = declutter_replace(arg, cmd)
447 if not modifier:
448 modifier, param = declutter_style(arg, cmd)
449 if modifier:
450 modifiers.append((modifier, param))
451 return modifiers
452 #@+node:vitalije.20200329162015.1: *7* preload_images
453 def preload_images() -> None:
454 for f in new_icons:
455 if f not in loaded_images:
456 loaded_images[f] = g.app.gui.getImageImage(f)
457 #@-others
458 if (p.h, iconVal) in dd:
459 # Apply saved adjustments to the text and to the _style_
460 # of the node
461 new_icons, modifiers_and_args = dd[(p.h, iconVal)]
462 for modifier, arg in modifiers_and_args:
463 modifier(item, arg)
465 new_icons = sorted_icons(p) + new_icons
466 else:
467 text = p.h
468 new_icons = []
469 modifiers_and_args = []
470 for pattern, cmds in self.get_declutter_patterns():
471 m = pattern.match(text) or pattern.search(text)
472 if m:
473 modifiers_and_args.extend(apply_declutter_rules(cmds))
475 # Save the lists of the icons and the adjusting operations
476 # for future reuse.
477 dd[(p.h, iconVal)] = new_icons, modifiers_and_args
478 new_icons = sorted_icons(p) + new_icons
479 preload_images()
480 self.nodeIconsDict[p.gnx] = new_icons
481 h = ':'.join(new_icons)
482 icon = g.app.gui.iconimages.get(h)
483 if not icon:
484 preload_images()
485 images = [loaded_images.get(x) for x in new_icons]
486 icon = self.make_composite_icon(images)
487 g.app.gui.iconimages[h] = icon
488 return icon
489 #@+node:vitalije.20200327162532.1: *6* qtree.get_declutter_patterns
490 def get_declutter_patterns(self) -> List[Any]:
491 "Initializes self.declutter_patterns from configuration and returns it"
492 if self.declutter_patterns is not None:
493 return self.declutter_patterns
494 c = self.c
495 patterns: List[Any] = []
496 warned = False
497 lines = c.config.getData("tree-declutter-patterns")
498 for line in lines:
499 try:
500 cmd, arg = line.split(None, 1)
501 except ValueError:
502 # Allow empty arg, and guard against user errors.
503 cmd = line.strip()
504 arg = ''
505 if cmd.startswith('#'):
506 pass
507 elif cmd == 'RULE':
508 patterns.append((re.compile(arg), []))
509 else:
510 if patterns:
511 patterns[-1][1].append((cmd, arg))
512 elif not warned:
513 warned = True
514 g.log('Declutter patterns must start with RULE*',
515 color='error')
516 self.declutter_patterns = patterns
517 return patterns
518 #@+node:ekr.20110605121601.17874: *5* qtree.drawChildren
519 def drawChildren(self, p: Pos, parent_item: Item) -> None:
520 """Draw the children of p if they should be expanded."""
521 if not p:
522 g.trace('can not happen: no p')
523 return
524 if p.hasChildren():
525 if p.isExpanded():
526 self.expandItem(parent_item)
527 child = p.firstChild()
528 while child:
529 self.drawTree(child, parent_item)
530 child.moveToNext()
531 else:
532 # Draw the hidden children.
533 child = p.firstChild()
534 while child:
535 self.drawNode(child, parent_item)
536 child.moveToNext()
537 self.contractItem(parent_item)
538 else:
539 self.contractItem(parent_item)
540 #@+node:ekr.20110605121601.17875: *5* qtree.drawNode
541 def drawNode(self, p: Pos, parent_item: Item) -> Item:
542 """Draw the node p."""
543 c = self.c
544 v = p.v
545 # Allocate the QTreeWidgetItem.
546 item = self.createTreeItem(p, parent_item)
547 # Update the data structures.
548 itemHash = self.itemHash(item)
549 self.position2itemDict[p.key()] = item
550 self.item2positionDict[itemHash] = p.copy() # was item
551 self.item2vnodeDict[itemHash] = v # was item
552 d = self.vnode2itemsDict
553 aList = d.get(v, [])
554 if item not in aList:
555 aList.append(item)
556 d[v] = aList
557 # Set the headline and maybe the icon.
558 self.setItemText(item, p.h)
559 # #1310: Add a tool tip.
560 item.setToolTip(0, p.h)
561 if self.use_declutter:
562 icon = self.declutter_node(c, p, item)
563 if icon:
564 item.setIcon(0, icon)
565 return item
566 # Draw the icon.
567 v.iconVal = v.computeIcon()
568 # **Slow**, but allows per-vnode icons.
569 icon = self.getCompositeIconImage(p, v.iconVal)
570 if icon:
571 item.setIcon(0, icon)
572 return item
573 #@+node:ekr.20110605121601.17876: *5* qtree.drawTopTree
574 def drawTopTree(self, p: Pos) -> None:
575 """Draw the tree rooted at p."""
576 trace = 'drawing' in g.app.debug and not g.unitTesting
577 if trace:
578 t1 = time.process_time()
579 c = self.c
580 self.clear()
581 # Draw all top-level nodes and their visible descendants.
582 if c.hoistStack:
583 bunch = c.hoistStack[-1]
584 p = bunch.p
585 h = p.h
586 if len(c.hoistStack) == 1 and h.startswith('@chapter') and p.hasChildren():
587 p = p.firstChild()
588 while p:
589 self.drawTree(p)
590 p.moveToNext()
591 else:
592 self.drawTree(p)
593 else:
594 p = c.rootPosition()
595 while p:
596 self.drawTree(p)
597 p.moveToNext()
598 if trace:
599 t2 = time.process_time()
600 g.trace(f"{t2 - t1:5.2f} sec.", g.callers(5))
601 #@+node:ekr.20110605121601.17877: *5* qtree.drawTree
602 def drawTree(self, p: Pos, parent_item: Item=None) -> None:
603 if g.app.gui.isNullGui:
604 return
605 # Draw the (visible) parent node.
606 item = self.drawNode(p, parent_item)
607 # Draw all the visible children.
608 self.drawChildren(p, parent_item=item)
609 #@+node:ekr.20110605121601.17878: *5* qtree.initData
610 def initData(self) -> None:
611 self.item2positionDict = {}
612 self.item2vnodeDict = {}
613 self.position2itemDict = {}
614 self.vnode2itemsDict = {}
615 self.editWidgetsDict = {}
616 #@+node:ekr.20110605121601.17880: *4* qtree.redraw_after_contract
617 def redraw_after_contract(self, p: Pos) -> None:
619 if self.busy:
620 return
621 self.update_expansion(p)
622 #@+node:ekr.20110605121601.17881: *4* qtree.redraw_after_expand
623 def redraw_after_expand(self, p: Pos) -> None:
625 if 0: # Does not work. Newly visible nodes do not show children correctly.
626 c = self.c
627 c.selectPosition(p)
628 self.update_expansion(p)
629 else:
630 self.full_redraw(p) # Don't try to shortcut this!
631 #@+node:ekr.20110605121601.17882: *4* qtree.redraw_after_head_changed
632 def redraw_after_head_changed(self) -> None:
633 """Redraw all Qt outline items cloned to c.p."""
634 if self.busy:
635 return
636 p = self.c.p
637 if p:
638 h = p.h # 2010/02/09: Fix bug 518823.
639 for item in self.vnode2items(p.v):
640 if self.isValidItem(item):
641 self.setItemText(item, h)
642 # Bug fix: 2009/10/06
643 self.redraw_after_icons_changed()
644 #@+node:ekr.20110605121601.17883: *4* qtree.redraw_after_icons_changed
645 def redraw_after_icons_changed(self) -> None:
647 if self.busy:
648 return
649 self.redrawCount += 1 # To keep a unit test happy.
650 c = self.c
651 try:
652 self.busy = True # Suppress call to setHeadString in onItemChanged!
653 self.getCurrentItem()
654 for p in c.rootPosition().self_and_siblings(copy=False):
655 # Updates icons in p and all visible descendants of p.
656 self.updateVisibleIcons(p)
657 finally:
658 self.busy = False
659 #@+node:ekr.20110605121601.17884: *4* qtree.redraw_after_select
660 def redraw_after_select(self, p: Pos=None) -> None:
661 """Redraw the entire tree when an invisible node is selected."""
662 if self.busy:
663 return
664 self.full_redraw(p)
665 # c.redraw_after_select calls tree.select indirectly.
666 # Do not call it again here.
667 #@+node:ekr.20140907201613.18986: *4* qtree.repaint (not used)
668 def repaint(self) -> None:
669 """Repaint the widget."""
670 w = self.treeWidget
671 w.repaint()
672 w.resizeColumnToContents(0) # 2009/12/22
673 #@+node:ekr.20180817043619.1: *4* qtree.update_expansion
674 def update_expansion(self, p: Pos) -> None:
675 """Update expansion bits for p, including all clones."""
676 c = self.c
677 w = self.treeWidget
678 expand = c.shouldBeExpanded(p)
679 if 'drawing' in g.app.debug:
680 g.trace('expand' if expand else 'contract')
681 item = self.position2itemDict.get(p.key())
682 if p:
683 try:
684 # These generate events, which would trigger a full redraw.
685 self.busy = True
686 if expand:
687 w.expandItem(item)
688 else:
689 w.collapseItem(item)
690 finally:
691 self.busy = False
692 w.repaint()
693 else:
694 g.trace('NO P')
695 c.redraw()
696 #@+node:ekr.20110605121601.17885: *3* qtree.Event handlers
697 #@+node:ekr.20110605121601.17887: *4* qtree.Click Box
698 #@+node:ekr.20110605121601.17888: *5* qtree.onClickBoxClick
699 def onClickBoxClick(self, event: Event, p: Pos=None) -> None:
700 if self.busy:
701 return
702 c = self.c
703 g.doHook("boxclick1", c=c, p=p, event=event)
704 g.doHook("boxclick2", c=c, p=p, event=event)
705 c.outerUpdate()
706 #@+node:ekr.20110605121601.17889: *5* qtree.onClickBoxRightClick
707 def onClickBoxRightClick(self, event: Event, p: Pos=None) -> None:
708 if self.busy:
709 return
710 c = self.c
711 g.doHook("boxrclick1", c=c, p=p, event=event)
712 g.doHook("boxrclick2", c=c, p=p, event=event)
713 c.outerUpdate()
714 #@+node:ekr.20110605121601.17890: *5* qtree.onPlusBoxRightClick
715 def onPlusBoxRightClick(self, event: Event, p: Pos=None) -> None:
716 if self.busy:
717 return
718 c = self.c
719 g.doHook('rclick-popup', c=c, p=p, event=event, context_menu='plusbox')
720 c.outerUpdate()
721 #@+node:ekr.20110605121601.17891: *4* qtree.Icon Box
722 # For Qt, there seems to be no way to trigger these events.
723 #@+node:ekr.20110605121601.17892: *5* qtree.onIconBoxClick
724 def onIconBoxClick(self, event: Event, p: Pos=None) -> None:
725 if self.busy:
726 return
727 c = self.c
728 g.doHook("iconclick1", c=c, p=p, event=event)
729 g.doHook("iconclick2", c=c, p=p, event=event)
730 c.outerUpdate()
731 #@+node:ekr.20110605121601.17893: *5* qtree.onIconBoxRightClick
732 def onIconBoxRightClick(self, event: Event, p: Pos=None) -> None:
733 """Handle a right click in any outline widget."""
734 if self.busy:
735 return
736 c = self.c
737 g.doHook("iconrclick1", c=c, p=p, event=event)
738 g.doHook("iconrclick2", c=c, p=p, event=event)
739 c.outerUpdate()
740 #@+node:ekr.20110605121601.17894: *5* qtree.onIconBoxDoubleClick
741 def onIconBoxDoubleClick(self, event: Event, p: Pos=None) -> None:
742 if self.busy:
743 return
744 c = self.c
745 if not p:
746 p = c.p
747 if not g.doHook("icondclick1", c=c, p=p, event=event):
748 self.endEditLabel()
749 self.OnIconDoubleClick(p) # Call the method in the base class.
750 g.doHook("icondclick2", c=c, p=p, event=event)
751 c.outerUpdate()
752 #@+node:ekr.20110605121601.18437: *4* qtree.onContextMenu
753 def onContextMenu(self, point: Any) -> None:
754 """LeoQtTree: Callback for customContextMenuRequested events."""
755 # #1286.
756 c, w = self.c, self.treeWidget
757 g.app.gui.onContextMenu(c, w, point)
758 #@+node:ekr.20110605121601.17896: *4* qtree.onItemClicked
759 def onItemClicked(self, item: Item, col: int) -> None: # Col not used.
760 """Handle a click in a BaseNativeTree widget item."""
761 # This is called after an item is selected.
762 if self.busy:
763 return
764 c = self.c
765 try:
766 self.busy = True
767 p = self.item2position(item)
768 if p:
769 auto_edit = self.prev_v == p.v # #1049.
770 self.prev_v = p.v
771 event = None
772 #
773 # Careful. We may have switched gui during unit testing.
774 if hasattr(g.app.gui, 'qtApp'):
775 mods = g.app.gui.qtApp.keyboardModifiers()
776 isCtrl = bool(mods & KeyboardModifier.ControlModifier)
777 # We could also add support for QtConst.ShiftModifier, QtConst.AltModifier
778 # & QtConst.MetaModifier.
779 if isCtrl:
780 if g.doHook("iconctrlclick1", c=c, p=p, event=event) is None:
781 c.frame.tree.OnIconCtrlClick(p) # Call the base class method.
782 g.doHook("iconctrlclick2", c=c, p=p, event=event)
783 else:
784 # 2014/02/21: generate headclick1/2 instead of iconclick1/2
785 g.doHook("headclick1", c=c, p=p, event=event)
786 g.doHook("headclick2", c=c, p=p, event=event)
787 else:
788 auto_edit = None
789 g.trace('*** no p')
790 # 2011/05/27: click here is like ctrl-g.
791 c.k.keyboardQuit(setFocus=False)
792 c.treeWantsFocus() # 2011/05/08: Focus must stay in the tree!
793 c.outerUpdate()
794 # 2011/06/01: A second *single* click on a selected node
795 # enters editing state.
796 if auto_edit and self.auto_edit:
797 e, wrapper = self.createTreeEditorForItem(item)
798 finally:
799 self.busy = False
800 #@+node:ekr.20110605121601.17895: *4* qtree.onItemCollapsed
801 def onItemCollapsed(self, item: Item) -> None:
803 if self.busy:
804 return
805 c = self.c
806 p = self.item2position(item)
807 if not p:
808 self.error('no p')
809 return
810 # Do **not** set lockouts here.
811 # Only methods that actually generate events should set lockouts.
812 if p.isExpanded():
813 p.contract()
814 c.redraw_after_contract(p)
815 self.select(p)
816 c.outerUpdate()
817 #@+node:ekr.20110605121601.17897: *4* qtree.onItemDoubleClicked
818 def onItemDoubleClicked(self, item: Item, col: Any) -> None: # col not used.
819 """Handle a double click in a BaseNativeTree widget item."""
820 if self.busy: # Required.
821 return
822 c = self.c
823 try:
824 self.busy = True
825 e, wrapper = self.createTreeEditorForItem(item)
826 if not e:
827 g.trace('*** no e')
828 p = self.item2position(item)
829 # 2011/07/28: End the lockout here, not at the end.
830 finally:
831 self.busy = False
832 if not p:
833 self.error('no p')
834 return
835 # 2014/02/21: generate headddlick1/2 instead of icondclick1/2.
836 if g.doHook("headdclick1", c=c, p=p, event=None) is None:
837 c.frame.tree.OnIconDoubleClick(p) # Call the base class method.
838 g.doHook("headclick2", c=c, p=p, event=None)
839 c.outerUpdate()
840 #@+node:ekr.20110605121601.17898: *4* qtree.onItemExpanded
841 def onItemExpanded(self, item: Item) -> None:
842 """Handle and tree-expansion event."""
843 if self.busy: # Required
844 return
845 c = self.c
846 p = self.item2position(item)
847 if not p:
848 self.error('no p')
849 return
850 # Do **not** set lockouts here.
851 # Only methods that actually generate events should set lockouts.
852 if not p.isExpanded():
853 p.expand()
854 c.redraw_after_expand(p)
855 self.select(p)
856 c.outerUpdate()
857 #@+node:ekr.20110605121601.17899: *4* qtree.onTreeSelect
858 def onTreeSelect(self) -> None:
859 """Select the proper position when a tree node is selected."""
860 if self.busy: # Required
861 return
862 c = self.c
863 item = self.getCurrentItem()
864 p = self.item2position(item)
865 if not p:
866 self.error(f"no p for item: {item}")
867 return
868 # Do **not** set lockouts here.
869 # Only methods that actually generate events should set lockouts.
870 self.select(p) # This is a call to LeoTree.select(!!)
871 c.outerUpdate()
872 #@+node:ekr.20110605121601.17944: *3* qtree.Focus
873 def getFocus(self) -> Any:
874 return g.app.gui.get_focus(self.c) # Bug fix: 2009/6/30
876 findFocus = getFocus
878 def setFocus(self) -> None:
879 g.app.gui.set_focus(self.c, self.treeWidget)
880 #@+node:ekr.20110605121601.18409: *3* qtree.Icons
881 #@+node:ekr.20110605121601.18410: *4* qtree.drawIcon
882 def drawIcon(self, p: Pos) -> None:
883 """Redraw the icon at p."""
884 self.updateIcon(p)
885 # the following code is wrong. It constructs a new item
886 # and assignes the icon to it. However this item is never
887 # added to the treeWidget so it is soon garbage collected
888 # w = self.treeWidget
889 # itemOrTree = self.position2item(p) or w
890 # item = QtWidgets.QTreeWidgetItem(itemOrTree)
891 # icon = self.getIcon(p)
892 # self.setItemIcon(item, icon)
893 #@+node:ekr.20110605121601.18411: *4* qtree.getIcon & helper
894 def getIcon(self, p: Pos) -> Icon:
895 """Return the proper icon for position p."""
896 if self.use_declutter:
897 item = self.position2item(p)
898 return item and self.declutter_node(self.c, p, item)
899 p.v.iconVal = iv = p.v.computeIcon()
900 return self.getCompositeIconImage(p, iv)
903 #@+node:vitalije.20200329153148.1: *5* qtree.icon_filenames_for_node
904 def icon_filenames_for_node(self, p: Pos, val: int) -> List[str]:
905 """Prepares and returns a list of icon filenames
906 related to this node.
907 """
908 nicon = f'box{val:02d}.png'
909 fnames = self.nodeIconsDict.get(p.gnx)
910 if not fnames:
911 icons = self.c.editCommands.getIconList(p)
912 fnames = [x['file'] for x in icons if x['where'] == 'beforeIcon']
913 fnames.append(nicon)
914 fnames.extend(x['file'] for x in icons if x['where'] == 'beforeHeadline')
915 self.nodeIconsDict[p.gnx] = fnames
916 pat = re.compile(r'^box\d\d\.png$')
917 loaded_images = self.loaded_images
918 for i, f in enumerate(fnames):
919 if pat.match(f):
920 fnames[i] = nicon
921 self.nodeIconsDict[p.gnx] = fnames
922 f = nicon
923 if f not in loaded_images:
924 loaded_images[f] = g.app.gui.getImageImage(f)
925 return fnames
926 #@+node:vitalije.20200329153154.1: *5* qtree.make_composite_icon
927 def make_composite_icon(self, images: List[Any]) -> Icon:
928 hsep = self.c.config.getInt('tree-icon-separation') or 0
929 images = [x for x in images if x]
930 height = max([i.height() for i in images])
931 images = [i.scaledToHeight(height) for i in images]
932 width = sum([i.width() for i in images]) + hsep * (len(images) - 1)
933 pix = QtGui.QImage(width, height, Format.Format_ARGB32_Premultiplied)
934 pix.fill(QtGui.QColor(0, 0, 0, 0).rgba()) # transparent fill, rgbA
935 # .rgba() call required for Qt4.7, later versions work with straight color
936 painter = QtGui.QPainter()
937 if not painter.begin(pix):
938 print("Failed to init. painter for icon")
939 # don't return, the code still makes an icon for the cache
940 # which stops this being called again and again
941 x = 0
942 for i in images:
943 painter.drawPixmap(x, 0, i)
944 x += i.width() + hsep
945 painter.end()
946 return QtGui.QIcon(QtGui.QPixmap.fromImage(pix))
947 #@+node:ekr.20110605121601.18412: *5* qtree.getCompositeIconImage
948 def getCompositeIconImage(self, p: Pos, val: int) -> Icon:
949 """Get the icon at position p."""
950 fnames = self.icon_filenames_for_node(p, val)
951 h = ':'.join(fnames)
952 icon = g.app.gui.iconimages.get(h)
953 loaded_images = self.loaded_images
954 images = list(map(loaded_images.get, fnames))
955 if not icon:
956 icon = self.make_composite_icon(images)
957 g.app.gui.iconimages[h] = icon
958 return icon
959 #@+node:ekr.20110605121601.17950: *4* qtree.setItemIcon
960 def setItemIcon(self, item: Item, icon: str) -> None:
962 valid = item and self.isValidItem(item)
963 if icon and valid:
964 # Important: do not set lockouts here.
965 # This will generate changed events,
966 # but there is no itemChanged event handler.
967 item.setIcon(0, icon)
969 #@+node:ekr.20110605121601.17951: *4* qtree.updateIcon & updateAllIcons
970 def updateIcon(self, p: Pos) -> None:
971 """Update p's icon."""
972 if not p:
973 return
974 val = p.v.computeIcon()
975 if p.v.iconVal != val:
976 self.nodeIconsDict.pop(p.gnx, None)
977 self.getIcon(p) # sets p.v.iconVal
979 def updateAllIcons(self, p: Pos) -> None:
980 if not p:
981 return
982 self.nodeIconsDict.pop(p.gnx, None)
983 icon = self.getIcon(p) # sets p.v.iconVal
984 # Update all cloned items.
985 items = self.vnode2items(p.v)
986 for item in items:
987 self.setItemIcon(item, icon)
988 #@+node:ekr.20110605121601.17952: *4* qtree.updateVisibleIcons
989 def updateVisibleIcons(self, p: Pos) -> None:
990 """Update the icon for p and the icons
991 for all visible descendants of p."""
992 self.updateAllIcons(p)
993 if p.hasChildren() and p.isExpanded():
994 for child in p.children():
995 self.updateVisibleIcons(child)
996 #@+node:ekr.20110605121601.18414: *3* qtree.Items
997 #@+node:ekr.20110605121601.17943: *4* qtree.item dict getters
998 def itemHash(self, item: Item) -> str:
999 return f"{repr(item)} at {str(id(item))}"
1001 def item2position(self, item: Item) -> Pos:
1002 itemHash = self.itemHash(item)
1003 p = self.item2positionDict.get(itemHash) # was item
1004 return p
1006 def item2vnode(self, item: Item) -> VNode:
1007 itemHash = self.itemHash(item)
1008 return self.item2vnodeDict.get(itemHash) # was item
1010 def position2item(self, p: Pos) -> Item:
1011 item = self.position2itemDict.get(p.key())
1012 return item
1014 def vnode2items(self, v: VNode) -> List[Item]:
1015 return self.vnode2itemsDict.get(v, [])
1017 def isValidItem(self, item: Item) -> bool:
1018 itemHash = self.itemHash(item)
1019 return itemHash in self.item2vnodeDict # was item.
1020 #@+node:ekr.20110605121601.18415: *4* qtree.childIndexOfItem
1021 def childIndexOfItem(self, item: Item) -> int:
1022 parent = item and item.parent()
1023 if parent:
1024 n = parent.indexOfChild(item)
1025 else:
1026 w = self.treeWidget
1027 n = w.indexOfTopLevelItem(item)
1028 return n
1029 #@+node:ekr.20110605121601.18416: *4* qtree.childItems
1030 def childItems(self, parent_item: Item) -> List[Item]:
1031 """
1032 Return the list of child items of the parent item,
1033 or the top-level items if parent_item is None.
1034 """
1035 if parent_item:
1036 n = parent_item.childCount()
1037 items = [parent_item.child(z) for z in range(n)]
1038 else:
1039 w = self.treeWidget
1040 n = w.topLevelItemCount()
1041 items = [w.topLevelItem(z) for z in range(n)]
1042 return items
1043 #@+node:ekr.20110605121601.18418: *4* qtree.connectEditorWidget & callback
1044 def connectEditorWidget(self, e: Editor, item: Item) -> Wrapper:
1045 """
1046 Connect QLineEdit e to QTreeItem item.
1048 Also callback for when the editor ends.
1050 New in Leo 6.4: The callback handles all updates w/o calling onHeadChanged.
1051 """
1052 c, p, u = self.c, self.c.p, self.c.undoer
1053 #@+others # define the callback.
1054 #@+node:ekr.20201109043641.1: *5* function: editingFinished_callback
1055 def editingFinished_callback() -> None:
1056 """Called when Qt emits the editingFinished signal."""
1057 s = e.text()
1058 i = s.find('\n')
1059 # Truncate to one line.
1060 if i > -1:
1061 s = s[:i]
1062 # #1310: update the tooltip.
1063 if p.h != s:
1064 # Update p.h and handle undo.
1065 item.setToolTip(0, s)
1066 undoData = u.beforeChangeHeadline(p)
1067 p.v.setHeadString(s) # Set v.h *after* calling the undoer's before method.
1068 if not c.changed:
1069 c.setChanged()
1070 # We must recolor the body because
1071 # the headline may contain directives.
1072 c.frame.body.recolor(p)
1073 p.setDirty()
1074 u.afterChangeHeadline(p, 'Edit Headline', undoData)
1075 self.redraw_after_head_changed()
1076 c.outerUpdate()
1077 #@-others
1078 if e:
1079 # Hook up the widget.
1080 wrapper = self.getWrapper(e, item)
1081 e.editingFinished.connect(editingFinished_callback)
1082 return wrapper # 2011/02/12
1083 g.trace('can not happen: no e')
1084 return None
1085 #@+node:ekr.20110605121601.18419: *4* qtree.contractItem & expandItem
1086 def contractItem(self, item: Item) -> None:
1087 self.treeWidget.collapseItem(item)
1089 def expandItem(self, item: Item) -> None:
1090 self.treeWidget.expandItem(item)
1091 #@+node:ekr.20110605121601.18420: *4* qtree.createTreeEditorForItem
1092 def createTreeEditorForItem(self, item: Item) -> Tuple[Editor, Wrapper]:
1094 c = self.c
1095 w = self.treeWidget
1096 w.setCurrentItem(item) # Must do this first.
1097 if self.use_declutter:
1098 item.setText(0, item._real_text)
1099 w.editItem(item)
1100 e = w.itemWidget(item, 0) # e is a QLineEdit
1101 e.setObjectName('headline')
1102 wrapper = self.connectEditorWidget(e, item)
1103 self.sizeTreeEditor(c, e)
1104 return e, wrapper
1105 #@+node:ekr.20110605121601.18421: *4* qtree.createTreeItem
1106 def createTreeItem(self, p: Pos, parent_item: Item) -> Item:
1108 w = self.treeWidget
1109 itemOrTree = parent_item or w
1110 item = QtWidgets.QTreeWidgetItem(itemOrTree)
1111 if isQt6:
1112 item.setFlags(item.flags() | ItemFlag.ItemIsEditable)
1113 ChildIndicatorPolicy = QtWidgets.QTreeWidgetItem.ChildIndicatorPolicy
1114 item.setChildIndicatorPolicy(ChildIndicatorPolicy.DontShowIndicatorWhenChildless) # pylint: disable=no-member
1115 else:
1116 item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable | item.DontShowIndicatorWhenChildless)
1117 try:
1118 g.visit_tree_item(self.c, p, item)
1119 except leoPlugins.TryNext:
1120 pass
1121 return item
1122 #@+node:ekr.20110605121601.18423: *4* qtree.getCurrentItem
1123 def getCurrentItem(self) -> Item:
1124 w = self.treeWidget
1125 return w.currentItem()
1126 #@+node:ekr.20110605121601.18424: *4* qtree.getItemText
1127 def getItemText(self, item: Item) -> str:
1128 """Return the text of the item."""
1129 return item.text(0) if item else '<no item>'
1130 #@+node:ekr.20110605121601.18425: *4* qtree.getParentItem
1131 def getParentItem(self, item: Item) -> Item:
1132 return item and item.parent()
1133 #@+node:ekr.20110605121601.18426: *4* qtree.getSelectedItems
1134 def getSelectedItems(self) -> List:
1135 w = self.treeWidget
1136 return w.selectedItems()
1137 #@+node:ekr.20110605121601.18427: *4* qtree.getTreeEditorForItem
1138 def getTreeEditorForItem(self, item: Item) -> Editor:
1139 """Return the edit widget if it exists.
1140 Do *not* create one if it does not exist.
1141 """
1142 w = self.treeWidget
1143 e = w.itemWidget(item, 0)
1144 return e
1145 #@+node:ekr.20110605121601.18428: *4* qtree.getWrapper
1146 def getWrapper(self, e: Editor, item: Item) -> Wrapper:
1147 """Return headlineWrapper that wraps e (a QLineEdit)."""
1148 c = self.c
1149 if e:
1150 wrapper = self.editWidgetsDict.get(e)
1151 if wrapper:
1152 pass
1153 else:
1154 if item:
1155 # 2011/02/12: item can be None.
1156 wrapper = self.headlineWrapper(c, item, name='head', widget=e)
1157 self.editWidgetsDict[e] = wrapper
1158 return wrapper
1159 g.trace('no e')
1160 return None
1161 #@+node:ekr.20110605121601.18429: *4* qtree.nthChildItem
1162 def nthChildItem(self, n: int, parent_item: Item) -> Item:
1163 children = self.childItems(parent_item)
1164 if n < len(children):
1165 item = children[n]
1166 else:
1167 # This is **not* an error.
1168 # It simply means that we need to redraw the tree.
1169 item = None
1170 return item
1171 #@+node:ekr.20110605121601.18430: *4* qtree.scrollToItem
1172 def scrollToItem(self, item: Item) -> None:
1173 """
1174 Scroll the tree widget so that item is visible.
1175 Leo's core no longer calls this method.
1176 """
1177 w = self.treeWidget
1178 hPos, vPos = self.getScroll()
1179 # Fix #265: Erratic scrolling bug.
1180 # w.PositionAtCenter causes unwanted scrolling.
1181 w.scrollToItem(item, w.EnsureVisible)
1182 self.setHScroll(0) # Necessary
1183 #@+node:ekr.20110605121601.18431: *4* qtree.setCurrentItemHelper
1184 def setCurrentItemHelper(self, item: Item) -> None:
1185 w = self.treeWidget
1186 w.setCurrentItem(item)
1187 #@+node:ekr.20110605121601.18432: *4* qtree.setItemText
1188 def setItemText(self, item: Item, s: str) -> None:
1189 if item:
1190 item.setText(0, s)
1191 if self.use_declutter:
1192 item._real_text = s
1193 #@+node:tbrown.20160406221505.1: *4* qtree.sizeTreeEditor
1194 @staticmethod
1195 def sizeTreeEditor(c: Cmdr, editor: Editor) -> None:
1196 """Size a QLineEdit in a tree headline so scrolling occurs"""
1197 # space available in tree widget
1198 space = c.frame.tree.treeWidget.size().width()
1199 # left hand edge of editor within tree widget
1200 used = editor.geometry().x() + 4 # + 4 for edit cursor
1201 # limit width to available space
1202 editor.resize(space - used, editor.size().height())
1203 #@+node:ekr.20110605121601.18433: *3* qtree.Scroll bars
1204 #@+node:ekr.20110605121601.18434: *4* qtree.getSCroll
1205 def getScroll(self) -> Tuple[int, int]:
1206 """Return the hPos,vPos for the tree's scrollbars."""
1207 w = self.treeWidget
1208 hScroll = w.horizontalScrollBar()
1209 vScroll = w.verticalScrollBar()
1210 hPos = hScroll.sliderPosition()
1211 vPos = vScroll.sliderPosition()
1212 return hPos, vPos
1213 #@+node:btheado.20111110215920.7164: *4* qtree.scrollDelegate
1214 def scrollDelegate(self, kind: str) -> None:
1215 """
1216 Scroll a QTreeWidget up or down or right or left.
1217 kind is in ('down-line','down-page','up-line','up-page', 'right', 'left')
1218 """
1219 c = self.c
1220 w = self.treeWidget
1221 if kind in ('left', 'right'):
1222 hScroll = w.horizontalScrollBar()
1223 if kind == 'right':
1224 delta = hScroll.pageStep()
1225 else:
1226 delta = -hScroll.pageStep()
1227 hScroll.setValue(hScroll.value() + delta)
1228 else:
1229 vScroll = w.verticalScrollBar()
1230 h = w.size().height()
1231 lineSpacing = w.fontMetrics().lineSpacing()
1232 n = h / lineSpacing
1233 if kind == 'down-half-page':
1234 delta = n / 2
1235 elif kind == 'down-line':
1236 delta = 1
1237 elif kind == 'down-page':
1238 delta = n
1239 elif kind == 'up-half-page':
1240 delta = -n / 2
1241 elif kind == 'up-line':
1242 delta = -1
1243 elif kind == 'up-page':
1244 delta = -n
1245 else:
1246 delta = 0
1247 g.trace('bad kind:', kind)
1248 val = vScroll.value()
1249 vScroll.setValue(val + delta)
1250 c.treeWantsFocus()
1251 #@+node:ekr.20110605121601.18435: *4* qtree.setH/VScroll
1252 def setHScroll(self, hPos: int) -> None:
1254 w = self.treeWidget
1255 hScroll = w.horizontalScrollBar()
1256 hScroll.setValue(hPos)
1258 def setVScroll(self, vPos: int) -> None:
1260 w = self.treeWidget
1261 vScroll = w.verticalScrollBar()
1262 vScroll.setValue(vPos)
1263 #@+node:ekr.20110605121601.17905: *3* qtree.Selecting & editing
1264 #@+node:ekr.20110605121601.17908: *4* qtree.edit_widget
1265 def edit_widget(self, p: Pos) -> Wrapper:
1266 """Returns the edit widget for position p."""
1267 item = self.position2item(p)
1268 if item:
1269 e = self.getTreeEditorForItem(item)
1270 if e:
1271 # Create a wrapper widget for Leo's core.
1272 w = self.getWrapper(e, item)
1273 return w
1274 # This is not an error
1275 # But warning: calling this method twice might not work!
1276 return None
1277 return None
1278 #@+node:ekr.20110605121601.17909: *4* qtree.editLabel and helper
1279 def editLabel(self,
1280 p: Pos, selectAll: bool=False, selection: Selection=None,
1281 ) -> Tuple[Editor, Wrapper]:
1282 """Start editing p's headline."""
1283 if self.busy:
1284 return None
1285 c = self.c
1286 # Do any scheduled redraw.
1287 # This won't do anything in the new redraw scheme.
1288 c.outerUpdate()
1289 item = self.position2item(p)
1290 if item:
1291 if self.use_declutter:
1292 item.setText(0, item._real_text)
1293 e, wrapper = self.editLabelHelper(item, selectAll, selection)
1294 else:
1295 e, wrapper = None, None
1296 self.error(f"no item for {p}")
1297 if e:
1298 self.sizeTreeEditor(c, e)
1299 # A nice hack: just set the focus request.
1300 c.requestedFocusWidget = e
1301 return e, wrapper
1302 #@+node:ekr.20110605121601.18422: *5* qtree.editLabelHelper
1303 def editLabelHelper(self,
1304 item: Any, selectAll: bool=False, selection: Selection=None,
1305 ) -> Tuple[Item, Wrapper]:
1306 """Helper for qtree.editLabel."""
1307 c, vc = self.c, self.c.vimCommands
1308 w = self.treeWidget
1309 # Must do this first.
1310 # This generates a call to onTreeSelect.
1311 w.setCurrentItem(item)
1312 w.editItem(item) # Generates focus-in event that tree doesn't report.
1313 e = w.itemWidget(item, 0) # A QLineEdit.
1314 s = e.text()
1315 if s == 'newHeadline':
1316 selectAll = True
1317 start: int
1318 n: int
1319 if selection:
1320 # pylint: disable=unpacking-non-sequence
1321 # Fix bug https://groups.google.com/d/msg/leo-editor/RAzVPihqmkI/-tgTQw0-LtwJ
1322 # Note: negative lengths are allowed.
1323 i, j, ins = selection
1324 if ins is None:
1325 start, n = i, abs(i - j)
1326 # This case doesn't happen for searches.
1327 elif ins == j:
1328 start, n = i, j - i
1329 else:
1330 start, n = j, i - j
1331 elif selectAll:
1332 start, n, ins = 0, len(s), len(s)
1333 else:
1334 start, n, ins = len(s), 0, len(s)
1335 e.setObjectName('headline')
1336 e.setSelection(start, n)
1337 # e.setCursorPosition(ins) # Does not work.
1338 e.setFocus()
1339 wrapper = self.connectEditorWidget(e, item) # Hook up the widget.
1340 if vc and c.vim_mode: # and selectAll
1341 # For now, *always* enter insert mode.
1342 if vc.is_text_wrapper(wrapper):
1343 vc.begin_insert_mode(w=wrapper)
1344 else:
1345 g.trace('not a text widget!', wrapper)
1346 return e, wrapper
1347 #@+node:ekr.20110605121601.17911: *4* qtree.endEditLabel
1348 def endEditLabel(self) -> None:
1349 """
1350 Override LeoTree.endEditLabel.
1352 Just end editing of the presently-selected QLineEdit!
1353 This will trigger the editingFinished_callback defined in createEditorForItem.
1354 """
1355 item = self.getCurrentItem()
1356 if not item:
1357 return
1358 e = self.getTreeEditorForItem(item)
1359 if not e:
1360 return
1361 # Trigger the end-editing event.
1362 w = self.treeWidget
1363 w.closeEditor(e, EndEditHint.NoHint)
1364 w.setCurrentItem(item)
1365 #@+node:ekr.20110605121601.17915: *4* qtree.getSelectedPositions
1366 def getSelectedPositions(self) -> Any:
1367 items = self.getSelectedItems()
1368 pl = leoNodes.PosList(self.item2position(it) for it in items)
1369 return pl
1370 #@+node:ekr.20110605121601.17914: *4* qtree.setHeadline
1371 def setHeadline(self, p: Pos, s: str) -> None:
1372 """Force the actual text of the headline widget to p.h."""
1373 # This is used by unit tests to force the headline and p into alignment.
1374 if not p:
1375 return
1376 # Don't do this here: the caller should do it.
1377 # p.setHeadString(s)
1378 e = self.edit_widget(p)
1379 if e:
1380 e.setAllText(s)
1381 else:
1382 item = self.position2item(p)
1383 if item:
1384 self.setItemText(item, s)
1385 #@+node:ekr.20110605121601.17913: *4* qtree.setItemForCurrentPosition
1386 def setItemForCurrentPosition(self) -> None:
1387 """Select the item for c.p"""
1388 p = self.c.p
1389 if self.busy:
1390 return None
1391 if not p:
1392 return None
1393 item = self.position2item(p)
1394 if not item:
1395 # This is not necessarily an error.
1396 # We often attempt to select an item before redrawing it.
1397 return None
1398 item2 = self.getCurrentItem()
1399 if item == item2:
1400 return item
1401 try:
1402 self.busy = True
1403 # This generates gui events, so we must use a lockout.
1404 self.treeWidget.setCurrentItem(item)
1405 finally:
1406 self.busy = False
1407 return item
1408 #@+node:ekr.20190613080606.1: *4* qtree.unselectItem
1409 def unselectItem(self, p: Pos) -> None:
1411 item = self.position2item(p)
1412 if item:
1413 item.setSelected(False)
1414 #@-others
1415#@-others
1416#@@language python
1417#@@tabwidth -4
1418#@@pagewidth 80
1419#@-leo