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