Coverage for C:\leo.repo\leo-editor\leo\core\leoFrame.py: 56%
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#@+leo-ver=5-thin
2#@+node:ekr.20031218072017.3655: * @file leoFrame.py
3"""
4The base classes for all Leo Windows, their body, log and tree panes, key bindings and menus.
6These classes should be overridden to create frames for a particular gui.
7"""
8#@+<< imports >>
9#@+node:ekr.20120219194520.10464: ** << imports >> (leoFrame)
10import time
11from leo.core import leoGlobals as g
12from leo.core import leoColorizer # NullColorizer is a subclass of ColorizerMixin
13from leo.core import leoMenu
14from leo.core import leoNodes
15assert time
16#@-<< imports >>
17#@+<< About handling events >>
18#@+node:ekr.20031218072017.2410: ** << About handling events >>
19#@+at Leo must handle events or commands that change the text in the outline
20# or body panes. We must ensure that headline and body text corresponds
21# to the VNode corresponding to presently selected outline, and vice
22# versa. For example, when the user selects a new headline in the
23# outline pane, we must ensure that:
24#
25# 1) All vnodes have up-to-date information and
26#
27# 2) the body pane is loaded with the correct data.
28#
29# Early versions of Leo attempted to satisfy these conditions when the user
30# switched outline nodes. Such attempts never worked well; there were too many
31# special cases. Later versions of Leo use a much more direct approach: every
32# keystroke in the body pane updates the presently selected VNode immediately.
33#
34# The LeoTree class contains all the event handlers for the tree pane, and the
35# LeoBody class contains the event handlers for the body pane. The following
36# convenience methods exists:
37#
38# - body.updateBody & tree.updateBody:
39# These are suprising complex.
40#
41# - body.bodyChanged & tree.headChanged:
42# Called by commands throughout Leo's core that change the body or headline.
43# These are thin wrappers for updateBody and updateTree.
44#@-<< About handling events >>
45#@+<< command decorators >>
46#@+node:ekr.20150509054428.1: ** << command decorators >> (leoFrame.py)
47def log_cmd(name): # Not used.
48 """Command decorator for the LeoLog class."""
49 return g.new_cmd_decorator(name, ['c', 'frame', 'log'])
51def body_cmd(name):
52 """Command decorator for the c.frame.body class."""
53 return g.new_cmd_decorator(name, ['c', 'frame', 'body'])
55def frame_cmd(name):
56 """Command decorator for the LeoFrame class."""
57 return g.new_cmd_decorator(name, ['c', 'frame',])
58#@-<< command decorators >>
59#@+others
60#@+node:ekr.20140907201613.18660: ** API classes
61# These classes are for documentation and unit testing.
62# They are the base class for no class.
63#@+node:ekr.20140904043623.18576: *3* class StatusLineAPI
64class StatusLineAPI:
65 """The required API for c.frame.statusLine."""
67 def __init__(self, c, parentFrame):
68 pass
70 def clear(self):
71 pass
73 def disable(self, background=None):
74 pass
76 def enable(self, background="white"):
77 pass
79 def get(self):
80 return ''
82 def isEnabled(self):
83 return False
85 def put(self, s, bg=None, fg=None):
86 pass
88 def setFocus(self):
89 pass
91 def update(self):
92 pass
93#@+node:ekr.20140907201613.18663: *3* class TreeAPI
94class TreeAPI:
95 """The required API for c.frame.tree."""
97 def __init__(self, frame):
98 pass
99 # Must be defined in subclasses.
101 def drawIcon(self, p):
102 pass
104 def editLabel(self, v, selectAll=False, selection=None):
105 pass
107 def edit_widget(self, p):
108 return None
110 def redraw(self, p=None):
111 pass
112 redraw_now = redraw
114 def scrollTo(self, p):
115 pass
116 # May be defined in subclasses.
118 def initAfterLoad(self):
119 pass
121 def onHeadChanged(self, p, undoType='Typing', s=None, e=None):
122 pass
123 # Hints for optimization. The proper default is c.redraw()
125 def redraw_after_contract(self, p):
126 pass
128 def redraw_after_expand(self, p):
129 pass
131 def redraw_after_head_changed(self):
132 pass
134 def redraw_after_icons_changed(self):
135 pass
137 def redraw_after_select(self, p=None):
138 pass
139 # Must be defined in the LeoTree class...
140 # def OnIconDoubleClick (self,p):
142 def OnIconCtrlClick(self, p):
143 pass
145 def endEditLabel(self):
146 pass
148 def getEditTextDict(self, v):
149 return None
151 def injectCallbacks(self):
152 pass
154 def onHeadlineKey(self, event):
155 pass
157 def select(self, p):
158 pass
160 def updateHead(self, event, w):
161 pass
162#@+node:ekr.20140903025053.18631: *3* class WrapperAPI
163class WrapperAPI:
164 """A class specifying the wrapper api used throughout Leo's core."""
166 def __init__(self, c):
167 pass
169 def appendText(self, s):
170 pass
172 def clipboard_append(self, s):
173 pass
175 def clipboard_clear(self):
176 pass
178 def delete(self, i, j=None):
179 pass
181 def deleteTextSelection(self):
182 pass
184 def disable(self):
185 pass
187 def enable(self, enabled=True):
188 pass
190 def flashCharacter(self, i, bg='white', fg='red', flashes=3, delay=75):
191 pass
193 def get(self, i, j):
194 return ''
196 def getAllText(self):
197 return ''
199 def getInsertPoint(self):
200 return 0
202 def getSelectedText(self):
203 return ''
205 def getSelectionRange(self):
206 return (0, 0)
208 def getXScrollPosition(self):
209 return 0
211 def getYScrollPosition(self):
212 return 0
214 def hasSelection(self):
215 return False
217 def insert(self, i, s):
218 pass
220 def see(self, i):
221 pass
223 def seeInsertPoint(self):
224 pass
226 def selectAllText(self, insert=None):
227 pass
229 def setAllText(self, s):
230 pass
232 def setFocus(self):
233 pass # Required: sets the focus to wrapper.widget.
235 def setInsertPoint(self, pos, s=None):
236 pass
238 def setSelectionRange(self, i, j, insert=None):
239 pass
241 def setXScrollPosition(self, i):
242 pass
244 def setYScrollPosition(self, i):
245 pass
247 def tag_configure(self, colorName, **keys):
248 pass
250 def toPythonIndex(self, index):
251 return 0
253 def toPythonIndexRowCol(self, index):
254 return (0, 0, 0)
255#@+node:ekr.20140904043623.18552: ** class IconBarAPI
256class IconBarAPI:
257 """The required API for c.frame.iconBar."""
259 def __init__(self, c, parentFrame):
260 pass
262 def add(self, *args, **keys):
263 pass
265 def addRow(self, height=None):
266 pass
268 def addRowIfNeeded(self):
269 pass
271 def addWidget(self, w):
272 pass
274 def clear(self):
275 pass
277 def createChaptersIcon(self):
278 pass
280 def deleteButton(self, w):
281 pass
283 def getNewFrame(self):
284 pass
286 def setCommandForButton(self, button, command, command_p, controller, gnx, script):
287 pass
288#@+node:ekr.20031218072017.3656: ** class LeoBody
289class LeoBody:
290 """The base class for the body pane in Leo windows."""
291 #@+others
292 #@+node:ekr.20031218072017.3657: *3* LeoBody.__init__
293 def __init__(self, frame, parentFrame):
294 """Ctor for LeoBody class."""
295 c = frame.c
296 frame.body = self
297 self.c = c
298 self.editorWrappers = {} # keys are pane names, values are text widgets
299 self.frame = frame
300 self.parentFrame = parentFrame # New in Leo 4.6.
301 self.totalNumberOfEditors = 0
302 # May be overridden in subclasses...
303 self.widget = None # set in LeoQtBody.set_widget.
304 self.wrapper = None # set in LeoQtBody.set_widget.
305 self.numberOfEditors = 1
306 self.pb = None # paned body widget.
307 # Must be overridden in subclasses...
308 self.colorizer = None
309 # Init user settings.
310 self.use_chapters = False # May be overridden in subclasses.
311 #@+node:ekr.20031218072017.3677: *3* LeoBody.Coloring
312 def forceFullRecolor(self):
313 pass
315 def getColorizer(self):
316 return self.colorizer
318 def updateSyntaxColorer(self, p):
319 return self.colorizer.updateSyntaxColorer(p.copy())
321 def recolor(self, p):
322 self.c.recolor()
324 recolor_now = recolor
325 #@+node:ekr.20140903103455.18574: *3* LeoBody.Defined in subclasses
326 # Methods of this class call the following methods of subclasses (LeoQtBody)
327 # Fail loudly if these methods are not defined.
329 def oops(self):
330 """Say that a required method in a subclass is missing."""
331 g.trace("(LeoBody) %s should be overridden in a subclass", g.callers())
333 def createEditorFrame(self, w):
334 self.oops()
336 def createTextWidget(self, parentFrame, p, name):
337 self.oops()
339 def packEditorLabelWidget(self, w):
340 self.oops()
342 def onFocusOut(self, obj):
343 pass
344 #@+node:ekr.20060528100747: *3* LeoBody.Editors
345 # This code uses self.pb, a paned body widget, created by tkBody.finishCreate.
346 #@+node:ekr.20070424053629: *4* LeoBody.entries
347 #@+node:ekr.20060528100747.1: *5* LeoBody.addEditor (overridden)
348 def addEditor(self, event=None):
349 """Add another editor to the body pane."""
350 c, p = self.c, self.c.p
351 self.totalNumberOfEditors += 1
352 self.numberOfEditors += 1
353 if self.numberOfEditors == 2:
354 # Inject the ivars into the first editor.
355 # Bug fix: Leo 4.4.8 rc1: The name of the last editor need not be '1'
356 d = self.editorWrappers
357 keys = list(d.keys())
358 if len(keys) == 1:
359 # Immediately create the label in the old editor.
360 w_old = d.get(keys[0])
361 self.updateInjectedIvars(w_old, p)
362 self.selectLabel(w_old)
363 else:
364 g.trace('can not happen: unexpected editorWrappers', d)
365 name = f"{self.totalNumberOfEditors}"
366 pane = self.pb.add(name)
367 panes = self.pb.panes()
368 minSize = float(1.0 / float(len(panes)))
369 # Create the text wrapper.
370 f = self.createEditorFrame(pane)
371 wrapper = self.createTextWidget(f, name=name, p=p)
372 wrapper.delete(0, 'end')
373 wrapper.insert('end', p.b)
374 wrapper.see(0)
375 c.k.completeAllBindingsForWidget(wrapper)
376 self.recolorWidget(p, wrapper)
377 self.editorWrappers[name] = wrapper
378 for pane in panes:
379 self.pb.configurepane(pane, size=minSize)
380 self.pb.updatelayout()
381 c.frame.body.wrapper = wrapper
382 # Finish...
383 self.updateInjectedIvars(wrapper, p)
384 self.selectLabel(wrapper)
385 self.selectEditor(wrapper)
386 self.updateEditors()
387 c.bodyWantsFocus()
388 #@+node:ekr.20060528132829: *5* LeoBody.assignPositionToEditor
389 def assignPositionToEditor(self, p):
390 """Called *only* from tree.select to select the present body editor."""
391 c = self.c
392 w = c.frame.body.widget
393 self.updateInjectedIvars(w, p)
394 self.selectLabel(w)
395 #@+node:ekr.20200415041750.1: *5* LeoBody.cycleEditorFocus (restored)
396 @body_cmd('editor-cycle-focus')
397 @body_cmd('cycle-editor-focus') # There is no LeoQtBody method
398 def cycleEditorFocus(self, event=None):
399 """Cycle keyboard focus between the body text editors."""
400 c = self.c
401 d = self.editorWrappers
402 w = c.frame.body.wrapper
403 values = list(d.values())
404 if len(values) > 1:
405 i = values.index(w) + 1
406 if i == len(values):
407 i = 0
408 w2 = values[i]
409 assert w != w2
410 self.selectEditor(w2)
411 c.frame.body.wrapper = w2
412 #@+node:ekr.20060528113806: *5* LeoBody.deleteEditor (overridden)
413 def deleteEditor(self, event=None):
414 """Delete the presently selected body text editor."""
415 c = self.c
416 w = c.frame.body.wapper
417 d = self.editorWrappers
418 if len(list(d.keys())) == 1:
419 return
420 name = w.leo_name
421 del d[name]
422 self.pb.delete(name)
423 panes = self.pb.panes()
424 minSize = float(1.0 / float(len(panes)))
425 for pane in panes:
426 self.pb.configurepane(pane, size=minSize)
427 # Select another editor.
428 w = list(d.values())[0]
429 # c.frame.body.wrapper = w # Don't do this now?
430 self.numberOfEditors -= 1
431 self.selectEditor(w)
432 #@+node:ekr.20070425180705: *5* LeoBody.findEditorForChapter
433 def findEditorForChapter(self, chapter, p):
434 """Return an editor to be assigned to chapter."""
435 c = self.c
436 d = self.editorWrappers
437 values = list(d.values())
438 # First, try to match both the chapter and position.
439 if p:
440 for w in values:
441 if (
442 hasattr(w, 'leo_chapter') and w.leo_chapter == chapter and
443 hasattr(w, 'leo_p') and w.leo_p and w.leo_p == p
444 ):
445 return w
446 # Next, try to match just the chapter.
447 for w in values:
448 if hasattr(w, 'leo_chapter') and w.leo_chapter == chapter:
449 return w
450 # As a last resort, return the present editor widget.
451 return c.frame.body.wrapper
452 #@+node:ekr.20060530210057: *5* LeoBody.select/unselectLabel
453 def unselectLabel(self, w):
454 self.createChapterIvar(w)
455 self.packEditorLabelWidget(w)
456 s = self.computeLabel(w)
457 if hasattr(w, 'leo_label') and w.leo_label:
458 w.leo_label.configure(text=s, bg='LightSteelBlue1')
460 def selectLabel(self, w):
461 if self.numberOfEditors > 1:
462 self.createChapterIvar(w)
463 self.packEditorLabelWidget(w)
464 s = self.computeLabel(w)
465 if hasattr(w, 'leo_label') and w.leo_label:
466 w.leo_label.configure(text=s, bg='white')
467 elif hasattr(w, 'leo_label') and w.leo_label:
468 w.leo_label.pack_forget()
469 w.leo_label = None
470 #@+node:ekr.20061017083312: *5* LeoBody.selectEditor & helpers
471 selectEditorLockout = False
473 def selectEditor(self, w):
474 """Select the editor given by w and node w.leo_p."""
475 # Called whenever wrapper must be selected.
476 c = self.c
477 if self.selectEditorLockout:
478 return None
479 if w and w == self.c.frame.body.widget:
480 if w.leo_p and w.leo_p != c.p:
481 c.selectPosition(w.leo_p)
482 c.bodyWantsFocus()
483 return None
484 try:
485 val = None
486 self.selectEditorLockout = True
487 val = self.selectEditorHelper(w)
488 finally:
489 self.selectEditorLockout = False
490 return val # Don't put a return in a finally clause.
491 #@+node:ekr.20070423102603: *6* LeoBody.selectEditorHelper
492 def selectEditorHelper(self, wrapper):
493 """Select the editor whose widget is given."""
494 c = self.c
495 if not (hasattr(wrapper, 'leo_p') and wrapper.leo_p):
496 g.trace('no wrapper.leo_p')
497 return
498 self.deactivateActiveEditor(wrapper)
499 # The actual switch.
500 c.frame.body.wrapper = wrapper
501 wrapper.leo_active = True
502 self.switchToChapter(wrapper)
503 self.selectLabel(wrapper)
504 if not self.ensurePositionExists(wrapper):
505 g.trace('***** no position editor!')
506 return
507 p = wrapper.leo_p
508 c.redraw(p)
509 c.recolor()
510 c.bodyWantsFocus()
511 #@+node:ekr.20060528131618: *5* LeoBody.updateEditors
512 # Called from addEditor and assignPositionToEditor
514 def updateEditors(self):
515 c, p = self.c, self.c.p
516 d = self.editorWrappers
517 if len(list(d.keys())) < 2:
518 return # There is only the main widget.
519 for key in d:
520 wrapper = d.get(key)
521 v = wrapper.leo_v
522 if v and v == p.v and wrapper != c.frame.body.wrapper:
523 wrapper.delete(0, 'end')
524 wrapper.insert('end', p.b)
525 self.recolorWidget(p, wrapper)
526 c.bodyWantsFocus()
527 #@+node:ekr.20070424053629.1: *4* LeoBody.utils
528 #@+node:ekr.20070422093128: *5* LeoBody.computeLabel
529 def computeLabel(self, w):
530 s = w.leo_label_s
531 if hasattr(w, 'leo_chapter') and w.leo_chapter:
532 s = f"{w.leo_chapter.name}: {s}"
533 return s
534 #@+node:ekr.20070422094710: *5* LeoBody.createChapterIvar
535 def createChapterIvar(self, w):
536 c = self.c
537 cc = c.chapterController
538 if not hasattr(w, 'leo_chapter') or not w.leo_chapter:
539 if cc and self.use_chapters:
540 w.leo_chapter = cc.getSelectedChapter()
541 else:
542 w.leo_chapter = None
543 #@+node:ekr.20070424084651: *5* LeoBody.ensurePositionExists
544 def ensurePositionExists(self, w):
545 """Return True if w.leo_p exists or can be reconstituted."""
546 c = self.c
547 if c.positionExists(w.leo_p):
548 return True
549 g.trace('***** does not exist', w.leo_name)
550 for p2 in c.all_unique_positions():
551 if p2.v and p2.v == w.leo_v:
552 w.leo_p = p2.copy()
553 return True
554 # This *can* happen when selecting a deleted node.
555 w.leo_p = c.p
556 return False
557 #@+node:ekr.20070424080640: *5* LeoBody.deactivateActiveEditor
558 # Not used in Qt.
560 def deactivateActiveEditor(self, w):
561 """Inactivate the previously active editor."""
562 d = self.editorWrappers
563 # Don't capture ivars here! assignPositionToEditor keeps them up-to-date. (??)
564 for key in d:
565 w2 = d.get(key)
566 if w2 != w and w2.leo_active:
567 w2.leo_active = False
568 self.unselectLabel(w2)
569 return
570 #@+node:ekr.20060530204135: *5* LeoBody.recolorWidget (QScintilla only)
571 def recolorWidget(self, p, w):
572 # Support QScintillaColorizer.colorize.
573 c = self.c
574 colorizer = c.frame.body.colorizer
575 if p and colorizer and hasattr(colorizer, 'colorize'):
576 old_wrapper = c.frame.body.wrapper
577 c.frame.body.wrapper = w
578 try:
579 c.frame.body.colorizer.colorize(p)
580 finally:
581 c.frame.body.wrapper = old_wrapper
582 #@+node:ekr.20070424084012: *5* LeoBody.switchToChapter
583 def switchToChapter(self, w):
584 """select w.leo_chapter."""
585 c = self.c
586 cc = c.chapterController
587 if hasattr(w, 'leo_chapter') and w.leo_chapter:
588 chapter = w.leo_chapter
589 name = chapter and chapter.name
590 oldChapter = cc.getSelectedChapter()
591 if chapter != oldChapter:
592 cc.selectChapterByName(name)
593 c.bodyWantsFocus()
594 #@+node:ekr.20070424092855: *5* LeoBody.updateInjectedIvars
595 # Called from addEditor and assignPositionToEditor.
597 def updateInjectedIvars(self, w, p):
598 """Inject updated ivars in w, a gui widget."""
599 if not w:
600 return
601 c = self.c
602 cc = c.chapterController
603 # Was in ctor.
604 use_chapters = c.config.getBool('use-chapters')
605 if cc and use_chapters:
606 w.leo_chapter = cc.getSelectedChapter()
607 else:
608 w.leo_chapter = None
609 w.leo_p = p.copy()
610 w.leo_v = w.leo_p.v
611 w.leo_label_s = p.h
612 #@+node:ekr.20031218072017.4018: *3* LeoBody.Text
613 #@+node:ekr.20031218072017.4030: *4* LeoBody.getInsertLines
614 def getInsertLines(self):
615 """
616 Return before,after where:
618 before is all the lines before the line containing the insert point.
619 sel is the line containing the insert point.
620 after is all the lines after the line containing the insert point.
622 All lines end in a newline, except possibly the last line.
623 """
624 body = self
625 w = body.wrapper
626 s = w.getAllText()
627 insert = w.getInsertPoint()
628 i, j = g.getLine(s, insert)
629 before = s[0:i]
630 ins = s[i:j]
631 after = s[j:]
632 before = g.checkUnicode(before)
633 ins = g.checkUnicode(ins)
634 after = g.checkUnicode(after)
635 return before, ins, after
636 #@+node:ekr.20031218072017.4031: *4* LeoBody.getSelectionAreas
637 def getSelectionAreas(self):
638 """
639 Return before,sel,after where:
641 before is the text before the selected text
642 (or the text before the insert point if no selection)
643 sel is the selected text (or "" if no selection)
644 after is the text after the selected text
645 (or the text after the insert point if no selection)
646 """
647 body = self
648 w = body.wrapper
649 s = w.getAllText()
650 i, j = w.getSelectionRange()
651 if i == j:
652 j = i + 1
653 before = s[0:i]
654 sel = s[i:j]
655 after = s[j:]
656 before = g.checkUnicode(before)
657 sel = g.checkUnicode(sel)
658 after = g.checkUnicode(after)
659 return before, sel, after
660 #@+node:ekr.20031218072017.2377: *4* LeoBody.getSelectionLines
661 def getSelectionLines(self):
662 """
663 Return before,sel,after where:
665 before is the all lines before the selected text
666 (or the text before the insert point if no selection)
667 sel is the selected text (or "" if no selection)
668 after is all lines after the selected text
669 (or the text after the insert point if no selection)
670 """
671 if g.app.batchMode:
672 return '', '', ''
673 # At present, called only by c.getBodyLines.
674 body = self
675 w = body.wrapper
676 s = w.getAllText()
677 i, j = w.getSelectionRange()
678 if i == j:
679 i, j = g.getLine(s, i)
680 else:
681 # #1742: Move j back if it is at the start of a line.
682 if j > i and j > 0 and s[j - 1] == '\n':
683 j -= 1
684 i, junk = g.getLine(s, i)
685 junk, j = g.getLine(s, j)
686 before = g.checkUnicode(s[0:i])
687 sel = g.checkUnicode(s[i:j])
688 after = g.checkUnicode(s[j : len(s)])
689 return before, sel, after # 3 strings.
690 #@-others
691#@+node:ekr.20031218072017.3678: ** class LeoFrame
692class LeoFrame:
693 """The base class for all Leo windows."""
694 instances = 0
695 #@+others
696 #@+node:ekr.20031218072017.3679: *3* LeoFrame.__init__ & reloadSettings
697 def __init__(self, c, gui):
698 self.c = c
699 self.gui = gui
700 self.iconBarClass = NullIconBarClass
701 self.statusLineClass = NullStatusLineClass
702 self.title = None # Must be created by subclasses.
703 # Objects attached to this frame.
704 self.body = None
705 self.colorPanel = None
706 self.comparePanel = None
707 self.findPanel = None
708 self.fontPanel = None
709 self.iconBar = None
710 self.isNullFrame = False
711 self.keys = None
712 self.log = None
713 self.menu = None
714 self.miniBufferWidget = None
715 self.outerFrame = None
716 self.prefsPanel = None
717 self.statusLine = g.NullObject() # For unit tests.
718 self.tree = None
719 self.useMiniBufferWidget = False
720 # Gui-independent data
721 self.cursorStay = True # May be overridden in subclass.reloadSettings.
722 self.componentsDict = {} # Keys are names, values are componentClass instances.
723 self.es_newlines = 0 # newline count for this log stream
724 self.openDirectory = ""
725 self.saved = False # True if ever saved
726 self.splitVerticalFlag = True # Set by initialRatios later.
727 self.startupWindow = False # True if initially opened window
728 self.stylesheet = None # The contents of <?xml-stylesheet...?> line.
729 self.tab_width = 0 # The tab width in effect in this pane.
730 #@+node:ekr.20051009045404: *4* frame.createFirstTreeNode
731 def createFirstTreeNode(self):
732 c = self.c
733 #
734 # #1631: Initialize here, not in p._linkAsRoot.
735 c.hiddenRootNode.children = []
736 #
737 # #1817: Clear the gnxDict.
738 c.fileCommands.gnxDict = {}
739 #
740 # Create the first node.
741 v = leoNodes.VNode(context=c)
742 p = leoNodes.Position(v)
743 v.initHeadString("NewHeadline")
744 #
745 # New in Leo 4.5: p.moveToRoot would be wrong:
746 # the node hasn't been linked yet.
747 p._linkAsRoot()
748 return v
749 #@+node:ekr.20061109125528: *3* LeoFrame.May be defined in subclasses
750 #@+node:ekr.20071027150501: *4* LeoFrame.event handlers
751 def OnBodyClick(self, event=None):
752 pass
754 def OnBodyRClick(self, event=None):
755 pass
756 #@+node:ekr.20031218072017.3688: *4* LeoFrame.getTitle & setTitle
757 def getTitle(self):
758 return self.title
760 def setTitle(self, title):
761 self.title = title
762 #@+node:ekr.20081005065934.3: *4* LeoFrame.initAfterLoad & initCompleteHint
763 def initAfterLoad(self):
764 """Provide offical hooks for late inits of components of Leo frames."""
765 frame = self
766 frame.body.initAfterLoad()
767 frame.log.initAfterLoad()
768 frame.menu.initAfterLoad()
769 # if frame.miniBufferWidget: frame.miniBufferWidget.initAfterLoad()
770 frame.tree.initAfterLoad()
772 def initCompleteHint(self):
773 pass
774 #@+node:ekr.20031218072017.3687: *4* LeoFrame.setTabWidth
775 def setTabWidth(self, w):
776 """Set the tab width in effect for this frame."""
777 # Subclasses may override this to affect drawing.
778 self.tab_width = w
779 #@+node:ekr.20061109125528.1: *3* LeoFrame.Must be defined in base class
780 #@+node:ekr.20031218072017.3689: *4* LeoFrame.initialRatios
781 def initialRatios(self):
782 c = self.c
783 s = c.config.get("initial_split_orientation", "string")
784 verticalFlag = s is None or (s != "h" and s != "horizontal")
785 if verticalFlag:
786 r = c.config.getRatio("initial-vertical-ratio")
787 if r is None or r < 0.0 or r > 1.0:
788 r = 0.5
789 r2 = c.config.getRatio("initial-vertical-secondary-ratio")
790 if r2 is None or r2 < 0.0 or r2 > 1.0:
791 r2 = 0.8
792 else:
793 r = c.config.getRatio("initial-horizontal-ratio")
794 if r is None or r < 0.0 or r > 1.0:
795 r = 0.3
796 r2 = c.config.getRatio("initial-horizontal-secondary-ratio")
797 if r2 is None or r2 < 0.0 or r2 > 1.0:
798 r2 = 0.8
799 return verticalFlag, r, r2
800 #@+node:ekr.20031218072017.3690: *4* LeoFrame.longFileName & shortFileName
801 def longFileName(self):
802 return self.c.mFileName
804 def shortFileName(self):
805 return g.shortFileName(self.c.mFileName)
806 #@+node:ekr.20031218072017.3691: *4* LeoFrame.oops
807 def oops(self):
808 g.pr("LeoFrame oops:", g.callers(4), "should be overridden in subclass")
809 #@+node:ekr.20031218072017.3692: *4* LeoFrame.promptForSave
810 def promptForSave(self):
811 """
812 Prompt the user to save changes.
813 Return True if the user vetos the quit or save operation.
814 """
815 c = self.c
816 theType = "quitting?" if g.app.quitting else "closing?"
817 # See if we are in quick edit/save mode.
818 root = c.rootPosition()
819 quick_save = not c.mFileName and not root.next() and root.isAtEditNode()
820 if quick_save:
821 name = g.shortFileName(root.atEditNodeName())
822 else:
823 name = c.mFileName if c.mFileName else self.title
824 answer = g.app.gui.runAskYesNoCancelDialog(
825 c,
826 title='Confirm',
827 message=f"Save changes to {g.splitLongFileName(name)} before {theType}",
828 )
829 if answer == "cancel":
830 return True # Veto.
831 if answer == "no":
832 return False # Don't save and don't veto.
833 if not c.mFileName:
834 root = c.rootPosition()
835 if not root.next() and root.isAtEditNode():
836 # There is only a single @edit node in the outline.
837 # A hack to allow "quick edit" of non-Leo files.
838 # See https://bugs.launchpad.net/leo-editor/+bug/381527
839 # Write the @edit node if needed.
840 if root.isDirty():
841 c.atFileCommands.writeOneAtEditNode(root)
842 return False # Don't save and don't veto.
843 c.mFileName = g.app.gui.runSaveFileDialog(c,
844 title="Save",
845 filetypes=[("Leo files", "*.leo")],
846 defaultextension=".leo")
847 c.bringToFront()
848 if c.mFileName:
849 if g.app.gui.guiName() == 'curses':
850 g.pr(f"Saving: {c.mFileName}")
851 ok = c.fileCommands.save(c.mFileName)
852 return not ok
853 # Veto if the save did not succeed.
854 return True # Veto.
855 #@+node:ekr.20031218072017.1375: *4* LeoFrame.frame.scanForTabWidth
856 def scanForTabWidth(self, p):
857 """Return the tab width in effect at p."""
858 c = self.c
859 tab_width = c.getTabWidth(p)
860 c.frame.setTabWidth(tab_width)
861 #@+node:ekr.20061119120006: *4* LeoFrame.Icon area convenience methods
862 def addIconButton(self, *args, **keys):
863 if self.iconBar:
864 return self.iconBar.add(*args, **keys)
865 return None
867 def addIconRow(self):
868 if self.iconBar:
869 return self.iconBar.addRow()
870 return None
872 def addIconWidget(self, w):
873 if self.iconBar:
874 return self.iconBar.addWidget(w)
875 return None
877 def clearIconBar(self):
878 if self.iconBar:
879 return self.iconBar.clear()
880 return None
882 def createIconBar(self):
883 c = self.c
884 if not self.iconBar:
885 self.iconBar = self.iconBarClass(c, self.outerFrame)
886 return self.iconBar
888 def getIconBar(self):
889 if not self.iconBar:
890 self.iconBar = self.iconBarClass(self.c, self.outerFrame)
891 return self.iconBar
893 getIconBarObject = getIconBar
895 def getNewIconFrame(self):
896 if not self.iconBar:
897 self.iconBar = self.iconBarClass(self.c, self.outerFrame)
898 return self.iconBar.getNewFrame()
900 def hideIconBar(self):
901 if self.iconBar:
902 self.iconBar.hide()
904 def showIconBar(self):
905 if self.iconBar:
906 self.iconBar.show()
907 #@+node:ekr.20041223105114.1: *4* LeoFrame.Status line convenience methods
908 def createStatusLine(self):
909 if not self.statusLine:
910 self.statusLine = self.statusLineClass(self.c, self.outerFrame) # type:ignore
911 return self.statusLine
913 def clearStatusLine(self):
914 if self.statusLine:
915 self.statusLine.clear()
917 def disableStatusLine(self, background=None):
918 if self.statusLine:
919 self.statusLine.disable(background)
921 def enableStatusLine(self, background="white"):
922 if self.statusLine:
923 self.statusLine.enable(background)
925 def getStatusLine(self):
926 return self.statusLine
928 getStatusObject = getStatusLine
930 def putStatusLine(self, s, bg=None, fg=None):
931 if self.statusLine:
932 self.statusLine.put(s, bg, fg)
934 def setFocusStatusLine(self):
935 if self.statusLine:
936 self.statusLine.setFocus()
938 def statusLineIsEnabled(self):
939 if self.statusLine:
940 return self.statusLine.isEnabled()
941 return False
943 def updateStatusLine(self):
944 if self.statusLine:
945 self.statusLine.update()
946 #@+node:ekr.20070130115927.4: *4* LeoFrame.Cut/Copy/Paste
947 #@+node:ekr.20070130115927.5: *5* LeoFrame.copyText
948 @frame_cmd('copy-text')
949 def copyText(self, event=None):
950 """Copy the selected text from the widget to the clipboard."""
951 # f = self
952 w = event and event.widget
953 # wname = c.widget_name(w)
954 if not w or not g.isTextWrapper(w):
955 return
956 # Set the clipboard text.
957 i, j = w.getSelectionRange()
958 if i == j:
959 ins = w.getInsertPoint()
960 i, j = g.getLine(w.getAllText(), ins)
961 # 2016/03/27: Fix a recent buglet.
962 # Don't clear the clipboard if we hit ctrl-c by mistake.
963 s = w.get(i, j)
964 if s:
965 g.app.gui.replaceClipboardWith(s)
967 OnCopyFromMenu = copyText
968 #@+node:ekr.20070130115927.6: *5* LeoFrame.cutText
969 @frame_cmd('cut-text')
970 def cutText(self, event=None):
971 """Invoked from the mini-buffer and from shortcuts."""
972 c, p, u = self.c, self.c.p, self.c.undoer
973 w = event and event.widget
974 if not w or not g.isTextWrapper(w):
975 return
976 bunch = u.beforeChangeBody(p)
977 name = c.widget_name(w)
978 oldText = w.getAllText()
979 i, j = w.getSelectionRange()
980 # Update the widget and set the clipboard text.
981 s = w.get(i, j)
982 if i != j:
983 w.delete(i, j)
984 w.see(i) # 2016/01/19: important
985 g.app.gui.replaceClipboardWith(s)
986 else:
987 ins = w.getInsertPoint()
988 i, j = g.getLine(oldText, ins)
989 s = w.get(i, j)
990 w.delete(i, j)
991 w.see(i) # 2016/01/19: important
992 g.app.gui.replaceClipboardWith(s)
993 if name.startswith('body'):
994 p.v.b = w.getAllText()
995 u.afterChangeBody(p, 'Cut', bunch)
996 elif name.startswith('head'):
997 # The headline is not officially changed yet.
998 s = w.getAllText()
999 else:
1000 pass
1002 OnCutFromMenu = cutText
1003 #@+node:ekr.20070130115927.7: *5* LeoFrame.pasteText
1004 @frame_cmd('paste-text')
1005 def pasteText(self, event=None, middleButton=False):
1006 """
1007 Paste the clipboard into a widget.
1008 If middleButton is True, support x-windows middle-mouse-button easter-egg.
1009 """
1010 c, p, u = self.c, self.c.p, self.c.undoer
1011 w = event and event.widget
1012 wname = c.widget_name(w)
1013 if not w or not g.isTextWrapper(w):
1014 return
1015 bunch = u.beforeChangeBody(p)
1016 if self.cursorStay and wname.startswith('body'):
1017 tCurPosition = w.getInsertPoint()
1018 i, j = w.getSelectionRange() # Returns insert point if no selection.
1019 if middleButton and c.k.previousSelection is not None:
1020 start, end = c.k.previousSelection
1021 s = w.getAllText()
1022 s = s[start:end]
1023 c.k.previousSelection = None
1024 else:
1025 s = g.app.gui.getTextFromClipboard()
1026 s = g.checkUnicode(s)
1027 singleLine = wname.startswith('head') or wname.startswith('minibuffer')
1028 if singleLine:
1029 # Strip trailing newlines so the truncation doesn't cause confusion.
1030 while s and s[-1] in ('\n', '\r'):
1031 s = s[:-1]
1032 # Save the horizontal scroll position.
1033 if hasattr(w, 'getXScrollPosition'):
1034 x_pos = w.getXScrollPosition()
1035 # Update the widget.
1036 if i != j:
1037 w.delete(i, j)
1038 w.insert(i, s)
1039 w.see(i + len(s) + 2)
1040 if wname.startswith('body'):
1041 if self.cursorStay:
1042 if tCurPosition == j:
1043 offset = len(s) - (j - i)
1044 else:
1045 offset = 0
1046 newCurPosition = tCurPosition + offset
1047 w.setSelectionRange(i=newCurPosition, j=newCurPosition)
1048 p.v.b = w.getAllText()
1049 u.afterChangeBody(p, 'Paste', bunch)
1050 elif singleLine:
1051 s = w.getAllText()
1052 while s and s[-1] in ('\n', '\r'):
1053 s = s[:-1]
1054 else:
1055 pass
1056 # Never scroll horizontally.
1057 if hasattr(w, 'getXScrollPosition'):
1058 w.setXScrollPosition(x_pos)
1060 OnPasteFromMenu = pasteText
1061 #@+node:ekr.20061016071937: *5* LeoFrame.OnPaste (support middle-button paste)
1062 def OnPaste(self, event=None):
1063 return self.pasteText(event=event, middleButton=True)
1064 #@+node:ekr.20031218072017.3980: *4* LeoFrame.Edit Menu
1065 #@+node:ekr.20031218072017.3982: *5* LeoFrame.endEditLabelCommand
1066 @frame_cmd('end-edit-headline')
1067 def endEditLabelCommand(self, event=None, p=None):
1068 """End editing of a headline and move focus to the body pane."""
1069 frame = self
1070 c = frame.c
1071 k = c.k
1072 if g.app.batchMode:
1073 c.notValidInBatchMode("End Edit Headline")
1074 return
1075 w = event and event.w or c.get_focus() # #1413.
1076 w_name = g.app.gui.widget_name(w)
1077 if w_name.startswith('head'):
1078 c.endEditing()
1079 c.treeWantsFocus()
1080 else:
1081 c.bodyWantsFocus()
1082 k.setDefaultInputState()
1083 k.showStateAndMode(w=c.frame.body.wrapper)
1084 # Recolor the *body* text, **not** the headline.
1085 #@+node:ekr.20031218072017.3680: *3* LeoFrame.Must be defined in subclasses
1086 def bringToFront(self):
1087 self.oops()
1089 def cascade(self, event=None):
1090 self.oops()
1092 def contractBodyPane(self, event=None):
1093 self.oops()
1095 def contractLogPane(self, event=None):
1096 self.oops()
1098 def contractOutlinePane(self, event=None):
1099 self.oops()
1101 def contractPane(self, event=None):
1102 self.oops()
1104 def deiconify(self):
1105 self.oops()
1107 def equalSizedPanes(self, event=None):
1108 self.oops()
1110 def expandBodyPane(self, event=None):
1111 self.oops()
1113 def expandLogPane(self, event=None):
1114 self.oops()
1116 def expandOutlinePane(self, event=None):
1117 self.oops()
1119 def expandPane(self, event=None):
1120 self.oops()
1122 def fullyExpandBodyPane(self, event=None):
1123 self.oops()
1125 def fullyExpandLogPane(self, event=None):
1126 self.oops()
1128 def fullyExpandOutlinePane(self, event=None):
1129 self.oops()
1131 def fullyExpandPane(self, event=None):
1132 self.oops()
1134 def get_window_info(self):
1135 self.oops()
1137 def hideBodyPane(self, event=None):
1138 self.oops()
1140 def hideLogPane(self, event=None):
1141 self.oops()
1143 def hideLogWindow(self, event=None):
1144 self.oops()
1146 def hideOutlinePane(self, event=None):
1147 self.oops()
1149 def hidePane(self, event=None):
1150 self.oops()
1152 def leoHelp(self, event=None):
1153 self.oops()
1155 def lift(self):
1156 self.oops()
1158 def minimizeAll(self, event=None):
1159 self.oops()
1161 def resizePanesToRatio(self, ratio, secondary_ratio):
1162 self.oops()
1164 def resizeToScreen(self, event=None):
1165 self.oops()
1167 def setInitialWindowGeometry(self):
1168 self.oops()
1170 def setTopGeometry(self, w, h, x, y):
1171 self.oops()
1173 def toggleActivePane(self, event=None):
1174 self.oops()
1176 def toggleSplitDirection(self, event=None):
1177 self.oops()
1178 #@-others
1179#@+node:ekr.20031218072017.3694: ** class LeoLog
1180class LeoLog:
1181 """The base class for the log pane in Leo windows."""
1182 #@+others
1183 #@+node:ekr.20150509054436.1: *3* LeoLog.Birth
1184 #@+node:ekr.20031218072017.3695: *4* LeoLog.ctor
1185 def __init__(self, frame, parentFrame):
1186 """Ctor for LeoLog class."""
1187 self.frame = frame
1188 self.c = frame.c if frame else None
1189 self.enabled = True
1190 self.newlines = 0
1191 self.isNull = False
1192 # Official ivars...
1193 self.canvasCtrl = None # Set below. Same as self.canvasDict.get(self.tabName)
1194 # Important: depending on the log *tab*, logCtrl may be either a wrapper or a widget.
1195 self.logCtrl = None # Set below. Same as self.textDict.get(self.tabName)
1196 self.tabName = None # The name of the active tab.
1197 self.tabFrame = None # Same as self.frameDict.get(self.tabName)
1198 self.canvasDict = {} # Keys are page names. Values are Tk.Canvas's.
1199 self.frameDict = {} # Keys are page names. Values are Tk.Frames.
1200 self.logNumber = 0 # To create unique name fields for text widgets.
1201 self.newTabCount = 0 # Number of new tabs created.
1202 self.textDict = {} # Keys are page names. Values are logCtrl's (text widgets).
1203 #@+node:ekr.20070302094848.1: *3* LeoLog.clearTab
1204 def clearTab(self, tabName, wrap='none'):
1205 self.selectTab(tabName, wrap=wrap)
1206 w = self.logCtrl
1207 if w:
1208 w.delete(0, 'end')
1209 #@+node:ekr.20070302094848.2: *3* LeoLog.createTab
1210 def createTab(self, tabName, createText=True, widget=None, wrap='none'):
1211 if createText:
1212 w = self.createTextWidget(self.tabFrame)
1213 self.canvasDict[tabName] = None
1214 self.textDict[tabName] = w
1215 else:
1216 self.canvasDict[tabName] = None
1217 self.textDict[tabName] = None
1218 self.frameDict[tabName] = tabName # tabFrame
1219 #@+node:ekr.20140903143741.18550: *3* LeoLog.LeoLog.createTextWidget
1220 def createTextWidget(self, parentFrame):
1221 return None
1222 #@+node:ekr.20070302094848.5: *3* LeoLog.deleteTab
1223 def deleteTab(self, tabName):
1224 c = self.c
1225 if tabName == 'Log':
1226 pass
1227 elif tabName in ('Find', 'Spell'):
1228 self.selectTab('Log')
1229 else:
1230 for d in (self.canvasDict, self.textDict, self.frameDict):
1231 if tabName in d:
1232 del d[tabName]
1233 self.tabName = None
1234 self.selectTab('Log')
1235 c.invalidateFocus()
1236 c.bodyWantsFocus()
1237 #@+node:ekr.20140903143741.18549: *3* LeoLog.enable/disable
1238 def disable(self):
1239 self.enabled = False
1241 def enable(self, enabled=True):
1242 self.enabled = enabled
1243 #@+node:ekr.20070302094848.7: *3* LeoLog.getSelectedTab
1244 def getSelectedTab(self):
1245 return self.tabName
1246 #@+node:ekr.20070302094848.6: *3* LeoLog.hideTab
1247 def hideTab(self, tabName):
1248 self.selectTab('Log')
1249 #@+node:ekr.20070302094848.8: *3* LeoLog.lower/raiseTab
1250 def lowerTab(self, tabName):
1251 self.c.invalidateFocus()
1252 self.c.bodyWantsFocus()
1254 def raiseTab(self, tabName):
1255 self.c.invalidateFocus()
1256 self.c.bodyWantsFocus()
1257 #@+node:ekr.20111122080923.10184: *3* LeoLog.orderedTabNames
1258 def orderedTabNames(self, LeoLog=None):
1259 return list(self.frameDict.values())
1260 #@+node:ekr.20070302094848.9: *3* LeoLog.numberOfVisibleTabs
1261 def numberOfVisibleTabs(self):
1262 return len([val for val in list(self.frameDict.values()) if val is not None])
1263 #@+node:ekr.20070302101304: *3* LeoLog.put & putnl
1264 # All output to the log stream eventually comes here.
1266 def put(self, s, color=None, tabName='Log', from_redirect=False, nodeLink=None):
1267 print(s)
1269 def putnl(self, tabName='Log'):
1270 pass # print ('')
1271 #@+node:ekr.20070302094848.10: *3* LeoLog.renameTab
1272 def renameTab(self, oldName, newName):
1273 pass
1274 #@+node:ekr.20070302094848.11: *3* LeoLog.selectTab
1275 def selectTab(self, tabName, createText=True, widget=None, wrap='none'): # widget unused.
1276 """Create the tab if necessary and make it active."""
1277 c = self.c
1278 tabFrame = self.frameDict.get(tabName)
1279 if not tabFrame:
1280 self.createTab(tabName, createText=createText)
1281 # Update the status vars.
1282 self.tabName = tabName
1283 self.canvasCtrl = self.canvasDict.get(tabName)
1284 self.logCtrl = self.textDict.get(tabName)
1285 self.tabFrame = self.frameDict.get(tabName)
1286 if 0:
1287 # Absolutely do not do this here!
1288 # It is a cause of the 'sticky focus' problem.
1289 c.widgetWantsFocusNow(self.logCtrl)
1290 return tabFrame
1291 #@-others
1292#@+node:ekr.20031218072017.3704: ** class LeoTree
1293class LeoTree:
1294 """The base class for the outline pane in Leo windows."""
1295 #@+others
1296 #@+node:ekr.20081005065934.8: *3* LeoTree.May be defined in subclasses
1297 # These are new in Leo 4.6.
1299 def initAfterLoad(self):
1300 """Do late initialization. Called in g.openWithFileName after a successful load."""
1302 # Hints for optimization. The proper default is c.redraw()
1304 def redraw_after_contract(self, p):
1305 self.c.redraw()
1307 def redraw_after_expand(self, p):
1308 self.c.redraw()
1310 def redraw_after_head_changed(self):
1311 self.c.redraw()
1313 def redraw_after_icons_changed(self):
1314 self.c.redraw()
1316 def redraw_after_select(self, p=None):
1317 self.c.redraw()
1318 #@+node:ekr.20040803072955.91: *4* LeoTree.onHeadChanged
1319 # Tricky code: do not change without careful thought and testing.
1320 # Important: This code *is* used by the leoBridge module.
1321 def onHeadChanged(self, p, undoType='Typing'):
1322 """
1323 Officially change a headline.
1324 Set the old undo text to the previous revert point.
1325 """
1326 c, u, w = self.c, self.c.undoer, self.edit_widget(p)
1327 if not w:
1328 g.trace('no w')
1329 return
1330 ch = '\n' # We only report the final keystroke.
1331 s = w.getAllText()
1332 #@+<< truncate s if it has multiple lines >>
1333 #@+node:ekr.20040803072955.94: *5* << truncate s if it has multiple lines >>
1334 # Remove trailing newlines before warning of truncation.
1335 while s and s[-1] == '\n':
1336 s = s[:-1]
1337 # Warn if there are multiple lines.
1338 i = s.find('\n')
1339 if i > -1:
1340 g.warning("truncating headline to one line")
1341 s = s[:i]
1342 limit = 1000
1343 if len(s) > limit:
1344 g.warning("truncating headline to", limit, "characters")
1345 s = s[:limit]
1346 s = g.checkUnicode(s or '')
1347 #@-<< truncate s if it has multiple lines >>
1348 # Make the change official, but undo to the *old* revert point.
1349 changed = s != p.h
1350 if not changed:
1351 return # Leo 6.4: only call the hooks if the headline has actually changed.
1352 if g.doHook("headkey1", c=c, p=p, ch=ch, changed=changed):
1353 return # The hook claims to have handled the event.
1354 # Handle undo.
1355 undoData = u.beforeChangeHeadline(p)
1356 p.initHeadString(s) # change p.h *after* calling undoer's before method.
1357 if not c.changed:
1358 c.setChanged()
1359 # New in Leo 4.4.5: we must recolor the body because
1360 # the headline may contain directives.
1361 c.frame.scanForTabWidth(p)
1362 c.frame.body.recolor(p)
1363 p.setDirty()
1364 u.afterChangeHeadline(p, undoType, undoData)
1365 c.redraw_after_head_changed()
1366 # Fix bug 1280689: don't call the non-existent c.treeEditFocusHelper
1367 g.doHook("headkey2", c=c, p=p, ch=ch, changed=changed)
1368 #@+node:ekr.20031218072017.3705: *3* LeoTree.__init__
1369 def __init__(self, frame):
1370 """Ctor for the LeoTree class."""
1371 self.frame = frame
1372 self.c = frame.c
1373 # New in 3.12: keys vnodes, values are edit_widgets.
1374 # New in 4.2: keys are vnodes, values are pairs (p,edit widgets).
1375 self.edit_text_dict = {}
1376 # "public" ivars: correspond to setters & getters.
1377 self.drag_p = None
1378 self.generation = 0 # low-level vnode methods increment this count.
1379 self.redrawCount = 0 # For traces
1380 self.use_chapters = False # May be overridden in subclasses.
1381 # Define these here to keep pylint happy.
1382 self.canvas = None
1383 #@+node:ekr.20061109165848: *3* LeoTree.Must be defined in base class
1384 #@+node:ekr.20040803072955.126: *4* LeoTree.endEditLabel
1385 def endEditLabel(self):
1386 """End editing of a headline and update p.h."""
1387 # Important: this will redraw if necessary.
1388 self.onHeadChanged(self.c.p)
1389 # Do *not* call setDefaultUnboundKeyAction here: it might put us in ignore mode!
1390 # k.setDefaultInputState()
1391 # k.showStateAndMode()
1392 # This interferes with the find command and interferes with focus generally!
1393 # c.bodyWantsFocus()
1394 #@+node:ekr.20031218072017.3716: *4* LeoTree.getEditTextDict
1395 def getEditTextDict(self, v):
1396 # New in 4.2: the default is an empty list.
1397 return self.edit_text_dict.get(v, [])
1398 #@+node:ekr.20040803072955.88: *4* LeoTree.onHeadlineKey
1399 def onHeadlineKey(self, event):
1400 """Handle a key event in a headline."""
1401 w = event.widget if event else None
1402 ch = event.char if event else ''
1403 # This test prevents flashing in the headline when the control key is held down.
1404 if ch:
1405 self.updateHead(event, w)
1406 #@+node:ekr.20120314064059.9739: *4* LeoTree.OnIconCtrlClick (@url)
1407 def OnIconCtrlClick(self, p):
1408 g.openUrl(p)
1409 #@+node:ekr.20031218072017.2312: *4* LeoTree.OnIconDoubleClick (do nothing)
1410 def OnIconDoubleClick(self, p):
1411 pass
1412 #@+node:ekr.20051026083544.2: *4* LeoTree.updateHead
1413 def updateHead(self, event, w):
1414 """
1415 Update a headline from an event.
1417 The headline officially changes only when editing ends.
1418 """
1419 k = self.c.k
1420 ch = event.char if event else ''
1421 i, j = w.getSelectionRange()
1422 ins = w.getInsertPoint()
1423 if i != j:
1424 ins = i
1425 if ch in ('\b', 'BackSpace'):
1426 if i != j:
1427 w.delete(i, j)
1428 # Bug fix: 2018/04/19.
1429 w.setSelectionRange(i, i, insert=i)
1430 elif i > 0:
1431 i -= 1
1432 w.delete(i)
1433 w.setSelectionRange(i, i, insert=i)
1434 else:
1435 w.setSelectionRange(0, 0, insert=0)
1436 elif ch and ch not in ('\n', '\r'):
1437 if i != j:
1438 w.delete(i, j)
1439 elif k.unboundKeyAction == 'overwrite':
1440 w.delete(i, i + 1)
1441 w.insert(ins, ch)
1442 w.setSelectionRange(ins + 1, ins + 1, insert=ins + 1)
1443 s = w.getAllText()
1444 if s.endswith('\n'):
1445 s = s[:-1]
1446 # 2011/11/14: Not used at present.
1447 # w.setWidth(self.headWidth(s=s))
1448 if ch in ('\n', '\r'):
1449 self.endEditLabel()
1450 #@+node:ekr.20031218072017.3706: *3* LeoTree.Must be defined in subclasses
1451 # Drawing & scrolling.
1453 def drawIcon(self, p):
1454 self.oops()
1456 def redraw(self, p=None):
1457 self.oops()
1458 redraw_now = redraw
1460 def scrollTo(self, p):
1461 self.oops()
1463 # Headlines.
1465 def editLabel(self, p, selectAll=False, selection=None):
1466 self.oops()
1468 def edit_widget(self, p):
1469 self.oops()
1470 #@+node:ekr.20040803072955.128: *3* LeoTree.select & helpers
1471 tree_select_lockout = False
1473 def select(self, p):
1474 """
1475 Select a node.
1476 Never redraws outline, but may change coloring of individual headlines.
1477 The scroll argument is used by the gui to suppress scrolling while dragging.
1478 """
1479 trace = 'select' in g.app.debug and not g.unitTesting
1480 tag = 'LeoTree.select'
1481 c = self.c
1482 if g.app.killed or self.tree_select_lockout: # Essential.
1483 return
1484 if trace:
1485 print(f"----- {tag}: {p.h}")
1486 # print(f"{tag:>30}: {c.frame.body.wrapper} {p.h}")
1487 # Format matches traces in leoflexx.py
1488 # print(f"{tag:30}: {len(p.b):4} {p.gnx} {p.h}")
1489 try:
1490 self.tree_select_lockout = True
1491 self.prev_v = c.p.v
1492 self.selectHelper(p)
1493 finally:
1494 self.tree_select_lockout = False
1495 if c.enableRedrawFlag:
1496 p = c.p
1497 # Don't redraw during unit testing: an important speedup.
1498 if c.expandAllAncestors(p) and not g.unitTesting:
1499 # This can happen when doing goto-next-clone.
1500 c.redraw_later()
1501 # This *does* happen sometimes.
1502 else:
1503 c.outerUpdate() # Bring the tree up to date.
1504 if hasattr(self, 'setItemForCurrentPosition'):
1505 # pylint: disable=no-member
1506 self.setItemForCurrentPosition() # type:ignore
1507 else:
1508 c.requestLaterRedraw = True
1509 #@+node:ekr.20070423101911: *4* LeoTree.selectHelper & helpers
1510 def selectHelper(self, p):
1511 """
1512 A helper function for leoTree.select.
1513 Do **not** "optimize" this by returning if p==c.p!
1514 """
1515 if not p:
1516 # This is not an error! We may be changing roots.
1517 # Do *not* test c.positionExists(p) here!
1518 return
1519 c = self.c
1520 if not c.frame.body.wrapper:
1521 return # Defensive.
1522 if p.v.context != c:
1523 # Selecting a foreign position will not be pretty.
1524 g.trace(f"Wrong context: {p.v.context!r} != {c!r}")
1525 return
1526 old_p = c.p
1527 call_event_handlers = p != old_p
1528 # Order is important...
1529 self.unselect_helper(old_p, p)
1530 # 1. Call c.endEditLabel.
1531 self.select_new_node(old_p, p)
1532 # 2. Call set_body_text_after_select.
1533 self.change_current_position(old_p, p)
1534 # 3. Call c.undoer.onSelect.
1535 self.scroll_cursor(p)
1536 # 4. Set cursor in body.
1537 self.set_status_line(p)
1538 # 5. Last tweaks.
1539 if call_event_handlers:
1540 g.doHook("select2", c=c, new_p=p, old_p=old_p, new_v=p, old_v=old_p)
1541 g.doHook("select3", c=c, new_p=p, old_p=old_p, new_v=p, old_v=old_p)
1542 #@+node:ekr.20140829053801.18453: *5* 1. LeoTree.unselect_helper
1543 def unselect_helper(self, old_p, p):
1544 """Unselect the old node, calling the unselect hooks."""
1545 c = self.c
1546 call_event_handlers = p != old_p
1547 if call_event_handlers:
1548 unselect = not g.doHook(
1549 "unselect1", c=c, new_p=p, old_p=old_p, new_v=p, old_v=old_p)
1550 else:
1551 unselect = True
1553 # Actually unselect the old node.
1554 if unselect and old_p and old_p != p:
1555 self.endEditLabel()
1556 # #1168: Ctrl-minus selects multiple nodes.
1557 if hasattr(self, 'unselectItem'):
1558 # pylint: disable=no-member
1559 self.unselectItem(old_p) # type:ignore
1560 if call_event_handlers:
1561 g.doHook("unselect2", c=c, new_p=p, old_p=old_p, new_v=p, old_v=old_p)
1562 #@+node:ekr.20140829053801.18455: *5* 2. LeoTree.select_new_node & helper
1563 def select_new_node(self, old_p, p):
1564 """Select the new node, part 1."""
1565 c = self.c
1566 call_event_handlers = p != old_p
1567 if (
1568 call_event_handlers and g.doHook("select1",
1569 c=c, new_p=p, old_p=old_p, new_v=p, old_v=old_p)
1570 ):
1571 if 'select' in g.app.debug:
1572 g.trace('select1 override')
1573 return
1574 c.frame.setWrap(p)
1575 # Not that expensive
1576 self.set_body_text_after_select(p, old_p)
1577 c.nodeHistory.update(p)
1578 #@+node:ekr.20090608081524.6109: *6* LeoTree.set_body_text_after_select
1579 def set_body_text_after_select(self, p, old_p):
1580 """Set the text after selecting a node."""
1581 c = self.c
1582 w = c.frame.body.wrapper
1583 s = p.v.b # Guaranteed to be unicode.
1584 # Part 1: get the old text.
1585 old_s = w.getAllText()
1586 if p and p == old_p and s == old_s:
1587 return
1588 # Part 2: set the new text. This forces a recolor.
1589 c.setCurrentPosition(p)
1590 # Important: do this *before* setting text,
1591 # so that the colorizer will have the proper c.p.
1592 w.setAllText(s)
1593 # This is now done after c.p has been changed.
1594 # p.restoreCursorAndScroll()
1595 #@+node:ekr.20140829053801.18458: *5* 3. LeoTree.change_current_position
1596 def change_current_position(self, old_p, p):
1597 """Select the new node, part 2."""
1598 c = self.c
1599 # c.setCurrentPosition(p)
1600 # This is now done in set_body_text_after_select.
1601 c.frame.scanForTabWidth(p)
1602 #GS I believe this should also get into the select1 hook
1603 use_chapters = c.config.getBool('use-chapters')
1604 if use_chapters:
1605 cc = c.chapterController
1606 theChapter = cc and cc.getSelectedChapter()
1607 if theChapter:
1608 theChapter.p = p.copy()
1609 # Do not call treeFocusHelper here!
1610 # c.treeFocusHelper()
1611 c.undoer.onSelect(old_p, p)
1612 #@+node:ekr.20140829053801.18459: *5* 4. LeoTree.scroll_cursor
1613 def scroll_cursor(self, p):
1614 """Scroll the cursor."""
1615 p.restoreCursorAndScroll()
1616 # Was in setBodyTextAfterSelect
1617 #@+node:ekr.20140829053801.18460: *5* 5. LeoTree.set_status_line
1618 def set_status_line(self, p):
1619 """Update the status line."""
1620 c = self.c
1621 c.frame.body.assignPositionToEditor(p)
1622 # New in Leo 4.4.1.
1623 c.frame.updateStatusLine()
1624 # New in Leo 4.4.1.
1625 c.frame.clearStatusLine()
1626 if p and p.v:
1627 c.frame.putStatusLine(p.get_UNL())
1628 #@+node:ekr.20031218072017.3718: *3* LeoTree.oops
1629 def oops(self):
1630 g.pr("LeoTree oops:", g.callers(4), "should be overridden in subclass")
1631 #@-others
1632#@+node:ekr.20070317073627: ** class LeoTreeTab
1633class LeoTreeTab:
1634 """A class representing a tabbed outline pane."""
1635 #@+others
1636 #@+node:ekr.20070317073627.1: *3* ctor (LeoTreeTab)
1637 def __init__(self, c, chapterController, parentFrame):
1638 self.c = c
1639 self.cc = chapterController
1640 self.nb = None # Created in createControl.
1641 self.parentFrame = parentFrame
1642 #@+node:ekr.20070317073755: *3* Must be defined in subclasses
1643 def createControl(self):
1644 self.oops()
1646 def createTab(self, tabName, select=True):
1647 self.oops()
1649 def destroyTab(self, tabName):
1650 self.oops()
1652 def selectTab(self, tabName):
1653 self.oops()
1655 def setTabLabel(self, tabName):
1656 self.oops()
1657 #@+node:ekr.20070317083104: *3* oops
1658 def oops(self):
1659 g.pr("LeoTreeTree oops:", g.callers(4), "should be overridden in subclass")
1660 #@-others
1661#@+node:ekr.20031218072017.2191: ** class NullBody (LeoBody)
1662class NullBody(LeoBody):
1663 """A do-nothing body class."""
1664 #@+others
1665 #@+node:ekr.20031218072017.2192: *3* NullBody.__init__
1666 def __init__(self, frame=None, parentFrame=None):
1667 """Ctor for NullBody class."""
1668 super().__init__(frame, parentFrame)
1669 self.insertPoint = 0
1670 self.selection = 0, 0
1671 self.s = "" # The body text
1672 self.widget = None
1673 self.wrapper = wrapper = StringTextWrapper(c=self.c, name='body')
1674 self.editorWrappers['1'] = wrapper
1675 self.colorizer = NullColorizer(self.c)
1676 #@+node:ekr.20031218072017.2197: *3* NullBody: LeoBody interface
1677 # Birth, death...
1679 def createControl(self, parentFrame, p):
1680 pass
1681 # Editors...
1683 def addEditor(self, event=None):
1684 pass
1686 def assignPositionToEditor(self, p):
1687 pass
1689 def createEditorFrame(self, w):
1690 return None
1692 def cycleEditorFocus(self, event=None):
1693 pass
1695 def deleteEditor(self, event=None):
1696 pass
1698 def selectEditor(self, w):
1699 pass
1701 def selectLabel(self, w):
1702 pass
1704 def setEditorColors(self, bg, fg):
1705 pass
1707 def unselectLabel(self, w):
1708 pass
1710 def updateEditors(self):
1711 pass
1712 # Events...
1714 def forceFullRecolor(self):
1715 pass
1717 def scheduleIdleTimeRoutine(self, function, *args, **keys):
1718 pass
1719 # Low-level gui...
1721 def setFocus(self):
1722 pass
1723 #@-others
1724#@+node:ekr.20031218072017.2218: ** class NullColorizer (BaseColorizer)
1725class NullColorizer(leoColorizer.BaseColorizer):
1726 """A colorizer class that doesn't color."""
1728 recolorCount = 0
1730 def colorize(self, p):
1731 self.recolorCount += 1
1732 # For #503: Use string/null gui for unit tests
1733#@+node:ekr.20031218072017.2222: ** class NullFrame (LeoFrame)
1734class NullFrame(LeoFrame):
1735 """A null frame class for tests and batch execution."""
1736 #@+others
1737 #@+node:ekr.20040327105706: *3* NullFrame.ctor
1738 def __init__(self, c, title, gui):
1739 """Ctor for the NullFrame class."""
1740 super().__init__(c, gui)
1741 assert self.c
1742 self.wrapper = None
1743 self.iconBar = NullIconBarClass(self.c, self)
1744 self.initComplete = True
1745 self.isNullFrame = True
1746 self.outerFrame = None
1747 self.ratio = self.secondary_ratio = 0.5
1748 self.statusLineClass = NullStatusLineClass
1749 self.title = title
1750 self.top = None # Always None.
1751 # Create the component objects.
1752 self.body = NullBody(frame=self, parentFrame=None)
1753 self.log = NullLog(frame=self, parentFrame=None)
1754 self.menu = leoMenu.NullMenu(frame=self)
1755 self.tree = NullTree(frame=self)
1756 # Default window position.
1757 self.w = 600
1758 self.h = 500
1759 self.x = 40
1760 self.y = 40
1761 #@+node:ekr.20061109124552: *3* NullFrame.do nothings
1762 def bringToFront(self):
1763 pass
1765 def cascade(self, event=None):
1766 pass
1768 def contractBodyPane(self, event=None):
1769 pass
1771 def contractLogPane(self, event=None):
1772 pass
1774 def contractOutlinePane(self, event=None):
1775 pass
1777 def contractPane(self, event=None):
1778 pass
1780 def deiconify(self):
1781 pass
1783 def destroySelf(self):
1784 pass
1786 def equalSizedPanes(self, event=None):
1787 pass
1789 def expandBodyPane(self, event=None):
1790 pass
1792 def expandLogPane(self, event=None):
1793 pass
1795 def expandOutlinePane(self, event=None):
1796 pass
1798 def expandPane(self, event=None):
1799 pass
1801 def forceWrap(self, p):
1802 pass
1804 def fullyExpandBodyPane(self, event=None):
1805 pass
1807 def fullyExpandLogPane(self, event=None):
1808 pass
1810 def fullyExpandOutlinePane(self, event=None):
1811 pass
1813 def fullyExpandPane(self, event=None):
1814 pass
1816 def get_window_info(self):
1817 return 600, 500, 20, 20
1819 def hideBodyPane(self, event=None):
1820 pass
1822 def hideLogPane(self, event=None):
1823 pass
1825 def hideLogWindow(self, event=None):
1826 pass
1828 def hideOutlinePane(self, event=None):
1829 pass
1831 def hidePane(self, event=None):
1832 pass
1834 def leoHelp(self, event=None):
1835 pass
1837 def lift(self):
1838 pass
1840 def minimizeAll(self, event=None):
1841 pass
1843 def oops(self):
1844 g.trace("NullFrame", g.callers(4))
1846 def resizePanesToRatio(self, ratio, secondary_ratio):
1847 pass
1849 def resizeToScreen(self, event=None):
1850 pass
1852 def setInitialWindowGeometry(self):
1853 pass
1855 def setTopGeometry(self, w, h, x, y):
1856 return 0, 0, 0, 0
1858 def setWrap(self, flag, force=False):
1859 pass
1861 def toggleActivePane(self, event=None):
1862 pass
1864 def toggleSplitDirection(self, event=None):
1865 pass
1867 def update(self):
1868 pass
1869 #@+node:ekr.20171112115045.1: *3* NullFrame.finishCreate
1870 def finishCreate(self):
1872 # 2017/11/12: For #503: Use string/null gui for unit tests.
1873 self.createFirstTreeNode()
1874 # Call the base LeoFrame method.
1875 #@-others
1876#@+node:ekr.20070301164543: ** class NullIconBarClass
1877class NullIconBarClass:
1878 """A class representing the singleton Icon bar"""
1879 #@+others
1880 #@+node:ekr.20070301164543.1: *3* NullIconBarClass.ctor
1881 def __init__(self, c, parentFrame):
1882 """Ctor for NullIconBarClass."""
1883 self.c = c
1884 self.iconFrame = None
1885 self.parentFrame = parentFrame
1886 self.w = g.NullObject()
1887 #@+node:ekr.20070301165343: *3* NullIconBarClass.Do nothing
1888 def addRow(self, height=None):
1889 pass
1891 def addRowIfNeeded(self):
1892 pass
1894 def addWidget(self, w):
1895 pass
1897 def createChaptersIcon(self):
1898 pass
1900 def deleteButton(self, w):
1901 pass
1903 def getNewFrame(self):
1904 return None
1906 def hide(self):
1907 pass
1909 def show(self):
1910 pass
1911 #@+node:ekr.20070301164543.2: *3* NullIconBarClass.add
1912 def add(self, *args, **keys):
1913 """Add a (virtual) button to the (virtual) icon bar."""
1914 command = keys.get('command')
1915 text = keys.get('text')
1916 try:
1917 g.app.iconWidgetCount += 1
1918 except Exception:
1919 g.app.iconWidgetCount = 1
1920 n = g.app.iconWidgetCount
1921 name = f"nullButtonWidget {n}"
1922 if not command:
1924 def commandCallback(name=name):
1925 g.pr(f"command for {name}")
1927 command = commandCallback
1930 class nullButtonWidget:
1932 def __init__(self, c, command, name, text):
1933 self.c = c
1934 self.command = command
1935 self.name = name
1936 self.text = text
1938 def __repr__(self):
1939 return self.name
1941 b = nullButtonWidget(self.c, command, name, text)
1942 return b
1943 #@+node:ekr.20140904043623.18574: *3* NullIconBarClass.clear
1944 def clear(self):
1945 g.app.iconWidgetCount = 0
1946 g.app.iconImageRefs = []
1947 #@+node:ekr.20140904043623.18575: *3* NullIconBarClass.setCommandForButton
1948 def setCommandForButton(self, button, command, command_p, controller, gnx, script):
1949 button.command = command
1950 try:
1951 # See PR #2441: Add rclick support.
1952 from leo.plugins.mod_scripting import build_rclick_tree
1953 rclicks = build_rclick_tree(command_p, top_level=True)
1954 button.rclicks = rclicks
1955 except Exception:
1956 pass
1957 #@-others
1958#@+node:ekr.20031218072017.2232: ** class NullLog (LeoLog)
1959class NullLog(LeoLog):
1960 """A do-nothing log class."""
1961 #@+others
1962 #@+node:ekr.20070302095500: *3* NullLog.Birth
1963 #@+node:ekr.20041012083237: *4* NullLog.__init__
1964 def __init__(self, frame=None, parentFrame=None):
1966 super().__init__(frame, parentFrame)
1967 self.isNull = True
1968 # self.logCtrl is now a property of the base LeoLog class.
1969 self.logNumber = 0
1970 self.widget = self.createControl(parentFrame)
1971 #@+node:ekr.20120216123546.10951: *4* NullLog.finishCreate
1972 def finishCreate(self):
1973 pass
1974 #@+node:ekr.20041012083237.1: *4* NullLog.createControl
1975 def createControl(self, parentFrame):
1976 return self.createTextWidget(parentFrame)
1977 #@+node:ekr.20070302095121: *4* NullLog.createTextWidge
1978 def createTextWidget(self, parentFrame):
1979 self.logNumber += 1
1980 c = self.c
1981 log = StringTextWrapper(c=c, name=f"log-{self.logNumber}")
1982 return log
1983 #@+node:ekr.20181119135041.1: *3* NullLog.hasSelection
1984 def hasSelection(self):
1985 return self.widget.hasSelection()
1986 #@+node:ekr.20111119145033.10186: *3* NullLog.isLogWidget
1987 def isLogWidget(self, w):
1988 return False
1989 #@+node:ekr.20041012083237.2: *3* NullLog.oops
1990 def oops(self):
1991 g.trace("NullLog:", g.callers(4))
1992 #@+node:ekr.20041012083237.3: *3* NullLog.put and putnl
1993 def put(self, s, color=None, tabName='Log', from_redirect=False, nodeLink=None):
1994 # print('(nullGui) print',repr(s))
1995 if self.enabled:
1996 try:
1997 g.pr(s, newline=False)
1998 except UnicodeError:
1999 s = s.encode('ascii', 'replace')
2000 g.pr(s, newline=False)
2002 def putnl(self, tabName='Log'):
2003 if self.enabled:
2004 g.pr('')
2005 #@+node:ekr.20060124085830: *3* NullLog.tabs
2006 def clearTab(self, tabName, wrap='none'):
2007 pass
2009 def createCanvas(self, tabName):
2010 pass
2012 def createTab(self, tabName, createText=True, widget=None, wrap='none'):
2013 pass
2015 def deleteTab(self, tabName):
2016 pass
2018 def getSelectedTab(self):
2019 return None
2021 def lowerTab(self, tabName):
2022 pass
2024 def raiseTab(self, tabName):
2025 pass
2027 def renameTab(self, oldName, newName):
2028 pass
2030 def selectTab(self, tabName, createText=True, widget=None, wrap='none'):
2031 pass
2032 #@-others
2033#@+node:ekr.20070302171509: ** class NullStatusLineClass
2034class NullStatusLineClass:
2035 """A do-nothing status line."""
2037 def __init__(self, c, parentFrame):
2038 """Ctor for NullStatusLine class."""
2039 self.c = c
2040 self.enabled = False
2041 self.parentFrame = parentFrame
2042 self.textWidget = StringTextWrapper(c, name='status-line')
2043 # Set the official ivars.
2044 c.frame.statusFrame = None
2045 c.frame.statusLabel = None
2046 c.frame.statusText = self.textWidget
2047 #@+others
2048 #@+node:ekr.20070302171917: *3* NullStatusLineClass.methods
2049 def disable(self, background=None):
2050 self.enabled = False
2051 # self.c.bodyWantsFocus()
2053 def enable(self, background="white"):
2054 self.c.widgetWantsFocus(self.textWidget)
2055 self.enabled = True
2057 def clear(self):
2058 self.textWidget.delete(0, 'end')
2060 def get(self):
2061 return self.textWidget.getAllText()
2063 def isEnabled(self):
2064 return self.enabled
2066 def put(self, s, bg=None, fg=None):
2067 self.textWidget.insert('end', s)
2069 def setFocus(self):
2070 pass
2072 def update(self):
2073 pass
2074 #@-others
2075#@+node:ekr.20031218072017.2233: ** class NullTree (LeoTree)
2076class NullTree(LeoTree):
2077 """A do-almost-nothing tree class."""
2078 #@+others
2079 #@+node:ekr.20031218072017.2234: *3* NullTree.__init__
2080 def __init__(self, frame):
2081 """Ctor for NullTree class."""
2082 super().__init__(frame)
2083 assert self.frame
2084 self.c = frame.c
2085 self.editWidgetsDict = {} # Keys are vnodes, values are StringTextWidgets.
2086 self.font = None
2087 self.fontName = None
2088 self.canvas = None
2089 self.treeWidget = g.NullObject()
2090 self.redrawCount = 0
2091 self.updateCount = 0
2092 #@+node:ekr.20070228163350.2: *3* NullTree.edit_widget
2093 def edit_widget(self, p):
2094 d = self.editWidgetsDict
2095 if not p or not p.v:
2096 return None
2097 w = d.get(p.v)
2098 if not w:
2099 d[p.v] = w = StringTextWrapper(
2100 c=self.c,
2101 name=f"head-{1 + len(list(d.keys())):d}")
2102 w.setAllText(p.h)
2103 return w
2104 #@+node:ekr.20070228164730: *3* NullTree.editLabel
2105 def editLabel(self, p, selectAll=False, selection=None):
2106 """Start editing p's headline."""
2107 self.endEditLabel()
2108 if p:
2109 wrapper = StringTextWrapper(c=self.c, name='head-wrapper')
2110 e = None
2111 return e, wrapper
2112 return None, None
2113 #@+node:ekr.20070228173611: *3* NullTree.printWidgets
2114 def printWidgets(self):
2115 d = self.editWidgetsDict
2116 for key in d:
2117 # keys are vnodes, values are StringTextWidgets.
2118 w = d.get(key)
2119 g.pr('w', w, 'v.h:', key.headString, 's:', repr(w.s))
2120 #@+node:ekr.20070228163350.1: *3* NullTree.Drawing & scrolling
2121 def drawIcon(self, p):
2122 pass
2124 def redraw(self, p=None):
2125 self.redrawCount += 1
2126 return p
2127 # Support for #503: Use string/null gui for unit tests
2129 redraw_now = redraw
2131 def redraw_after_contract(self, p):
2132 self.redraw()
2134 def redraw_after_expand(self, p):
2135 self.redraw()
2137 def redraw_after_head_changed(self):
2138 self.redraw()
2140 def redraw_after_icons_changed(self):
2141 self.redraw()
2143 def redraw_after_select(self, p=None):
2144 self.redraw()
2146 def scrollTo(self, p):
2147 pass
2149 def updateAllIcons(self, p):
2150 pass
2152 def updateIcon(self, p):
2153 pass
2154 #@+node:ekr.20070228160345: *3* NullTree.setHeadline
2155 def setHeadline(self, p, s):
2156 """Set the actual text of the headline widget.
2158 This is called from the undo/redo logic to change the text before redrawing."""
2159 w = self.edit_widget(p)
2160 if w:
2161 w.delete(0, 'end')
2162 if s.endswith('\n') or s.endswith('\r'):
2163 s = s[:-1]
2164 w.insert(0, s)
2165 else:
2166 g.trace('-' * 20, 'oops')
2167 #@-others
2168#@+node:ekr.20070228074228.1: ** class StringTextWrapper
2169class StringTextWrapper:
2170 """A class that represents text as a Python string."""
2171 #@+others
2172 #@+node:ekr.20070228074228.2: *3* stw.ctor
2173 def __init__(self, c, name):
2174 """Ctor for the StringTextWrapper class."""
2175 self.c = c
2176 self.name = name
2177 self.ins = 0
2178 self.sel = 0, 0
2179 self.s = ''
2180 self.supportsHighLevelInterface = True
2181 self.virtualInsertPoint = 0
2182 self.widget = None # This ivar must exist, and be None.
2184 def __repr__(self):
2185 return f"<StringTextWrapper: {id(self)} {self.name}>"
2187 def getName(self):
2188 """StringTextWrapper."""
2189 return self.name # Essential.
2190 #@+node:ekr.20140903172510.18578: *3* stw.Clipboard
2191 def clipboard_clear(self):
2192 g.app.gui.replaceClipboardWith('')
2194 def clipboard_append(self, s):
2195 s1 = g.app.gui.getTextFromClipboard()
2196 g.app.gui.replaceClipboardWith(s1 + s)
2197 #@+node:ekr.20140903172510.18579: *3* stw.Do-nothings
2198 # For StringTextWrapper.
2200 def flashCharacter(self, i, bg='white', fg='red', flashes=3, delay=75):
2201 pass
2203 def getXScrollPosition(self):
2204 return 0
2206 def getYScrollPosition(self):
2207 return 0
2209 def see(self, i):
2210 pass
2212 def seeInsertPoint(self):
2213 pass
2215 def setFocus(self):
2216 pass
2218 def setStyleClass(self, name):
2219 pass
2221 def setXScrollPosition(self, i):
2222 pass
2224 def setYScrollPosition(self, i):
2225 pass
2227 def tag_configure(self, colorName, **keys):
2228 pass
2229 #@+node:ekr.20140903172510.18591: *3* stw.Text
2230 #@+node:ekr.20140903172510.18592: *4* stw.appendText
2231 def appendText(self, s):
2232 """StringTextWrapper."""
2233 self.s = self.s + g.toUnicode(s) # defensive
2234 self.ins = len(self.s)
2235 self.sel = self.ins, self.ins
2236 #@+node:ekr.20140903172510.18593: *4* stw.delete
2237 def delete(self, i, j=None):
2238 """StringTextWrapper."""
2239 i = self.toPythonIndex(i)
2240 if j is None:
2241 j = i + 1
2242 j = self.toPythonIndex(j)
2243 # This allows subclasses to use this base class method.
2244 if i > j:
2245 i, j = j, i
2246 s = self.getAllText()
2247 self.setAllText(s[:i] + s[j:])
2248 # Bug fix: 2011/11/13: Significant in external tests.
2249 self.setSelectionRange(i, i, insert=i)
2250 #@+node:ekr.20140903172510.18594: *4* stw.deleteTextSelection
2251 def deleteTextSelection(self):
2252 """StringTextWrapper."""
2253 i, j = self.getSelectionRange()
2254 self.delete(i, j)
2255 #@+node:ekr.20140903172510.18595: *4* stw.get
2256 def get(self, i, j=None):
2257 """StringTextWrapper."""
2258 i = self.toPythonIndex(i)
2259 if j is None:
2260 j = i + 1
2261 j = self.toPythonIndex(j)
2262 s = self.s[i:j]
2263 return g.toUnicode(s)
2264 #@+node:ekr.20140903172510.18596: *4* stw.getAllText
2265 def getAllText(self):
2266 """StringTextWrapper."""
2267 s = self.s
2268 return g.checkUnicode(s)
2269 #@+node:ekr.20140903172510.18584: *4* stw.getInsertPoint
2270 def getInsertPoint(self):
2271 """StringTextWrapper."""
2272 i = self.ins
2273 if i is None:
2274 if self.virtualInsertPoint is None:
2275 i = 0
2276 else:
2277 i = self.virtualInsertPoint
2278 self.virtualInsertPoint = i
2279 return i
2280 #@+node:ekr.20140903172510.18597: *4* stw.getSelectedText
2281 def getSelectedText(self):
2282 """StringTextWrapper."""
2283 i, j = self.sel
2284 s = self.s[i:j]
2285 return g.checkUnicode(s)
2286 #@+node:ekr.20140903172510.18585: *4* stw.getSelectionRange
2287 def getSelectionRange(self, sort=True):
2288 """Return the selected range of the widget."""
2289 sel = self.sel
2290 if len(sel) == 2 and sel[0] >= 0 and sel[1] >= 0:
2291 i, j = sel
2292 if sort and i > j:
2293 sel = j, i # Bug fix: 10/5/07
2294 return sel
2295 i = self.ins
2296 return i, i
2297 #@+node:ekr.20140903172510.18586: *4* stw.hasSelection
2298 def hasSelection(self):
2299 """StringTextWrapper."""
2300 i, j = self.getSelectionRange()
2301 return i != j
2302 #@+node:ekr.20140903172510.18598: *4* stw.insert
2303 def insert(self, i, s):
2304 """StringTextWrapper."""
2305 i = self.toPythonIndex(i)
2306 s1 = s
2307 self.s = self.s[:i] + s1 + self.s[i:]
2308 i += len(s1)
2309 self.ins = i
2310 self.sel = i, i
2311 #@+node:ekr.20140903172510.18589: *4* stw.selectAllText
2312 def selectAllText(self, insert=None):
2313 """StringTextWrapper."""
2314 self.setSelectionRange(0, 'end', insert=insert)
2315 #@+node:ekr.20140903172510.18600: *4* stw.setAllText
2316 def setAllText(self, s):
2317 """StringTextWrapper."""
2318 self.s = s
2319 i = len(self.s)
2320 self.ins = i
2321 self.sel = i, i
2322 #@+node:ekr.20140903172510.18587: *4* stw.setInsertPoint
2323 def setInsertPoint(self, pos, s=None):
2324 """StringTextWrapper."""
2325 i = self.toPythonIndex(pos)
2326 self.virtualInsertPoint = i
2327 self.ins = i
2328 self.sel = i, i
2329 #@+node:ekr.20070228111853: *4* stw.setSelectionRange
2330 def setSelectionRange(self, i, j, insert=None):
2331 """StringTextWrapper."""
2332 i, j = self.toPythonIndex(i), self.toPythonIndex(j)
2333 self.sel = i, j
2334 self.ins = j if insert is None else self.toPythonIndex(insert)
2335 #@+node:ekr.20140903172510.18581: *4* stw.toPythonIndex
2336 def toPythonIndex(self, index):
2337 """
2338 StringTextWrapper.toPythonIndex.
2340 Convert indices of the form 'end' or 'n1.n2' to integer indices into self.s.
2342 Unit tests *do* use non-integer indices, so removing this method would be tricky.
2343 """
2344 return g.toPythonIndex(self.s, index)
2345 #@+node:ekr.20140903172510.18582: *4* stw.toPythonIndexRowCol
2346 def toPythonIndexRowCol(self, index):
2347 """StringTextWrapper."""
2348 s = self.getAllText()
2349 i = self.toPythonIndex(index)
2350 row, col = g.convertPythonIndexToRowCol(s, i)
2351 return i, row, col
2352 #@-others
2353#@-others
2354#@@language python
2355#@@tabwidth -4
2356#@@pagewidth 70
2357#@-leo