# @+leo-ver=5-thin
# @+node:ekr.20170302123956.1: * @file ../doc/leoAttic.txt
# This is Leo's final resting place for dead code.
# New in Leo 6.7.5. The attic will contain only code retired in the present release.

# @@language python
# @@killbeautify
# @+all
# @+node:ekr.20240617085704.1: ** Permanent attic
# @+node:ekr.20230913144248.1: *3* g.SherlockTracer
# I am going to leave this class in the attic indefinitely.
# It might be useful as the base for other classes.
# @+node:ekr.20121128031949.12605: *4* class g.SherlockTracer
class SherlockTracer:
    """
    A stand-alone tracer class with many of Sherlock's features.

    This class should work in any environment containing the re, os and sys modules.

    The arguments in the pattern lists determine which functions get traced
    or which stats get printed. Each pattern starts with "+", "-", "+:" or
    "-:", followed by a regular expression::

    "+x"  Enables tracing (or stats) for all functions/methods whose name
          matches the regular expression x.
    "-x"  Disables tracing for functions/methods.
    "+:x" Enables tracing for all functions in the **file** whose name matches x.
    "-:x" Disables tracing for an entire file.

    Enabling and disabling depends on the order of arguments in the pattern
    list. Consider the arguments for the Rope trace::

    patterns=['+.*','+:.*',
        '-:.*\\lib\\.*','+:.*rope.*','-:.*leoGlobals.py',
        '-:.*worder.py','-:.*prefs.py','-:.*resources.py',])

    This enables tracing for everything, then disables tracing for all
    library modules, except for all rope modules. Finally, it disables the
    tracing for Rope's worder, prefs and resources modules.

    Being able to zero in on the code of interest can be a big help in
    studying other people's code. This is a non-invasive method: no tracing
    code needs to be inserted anywhere.

    Usage:

    g.SherlockTracer(patterns).run()
    """
    @others
# @+node:ekr.20121128031949.12602: *5* sherlock.__init__
def __init__(
    self,
    patterns: list[Any],
    indent: bool = True,
    show_args: bool = True,
    show_return: bool = True,
    verbose: bool = True,
) -> None:
    """SherlockTracer ctor."""
    self.bad_patterns: list[str] = []  # List of bad patterns.
    self.indent = indent  # True: indent calls and returns.
    self.contents_d: dict[str, list] = {}  # Keys are file names, values are file lines.
    self.n = 0  # The frame level on entry to run.
    self.stats: dict[str, dict] = {}  # Keys are full file names, values are dicts.
    self.patterns: list[Any] = None  # A list of regex patterns to match.
    self.pattern_stack: list[str] = []
    self.show_args = show_args  # True: show args for each function call.
    self.show_return = show_return  # True: show returns from each function.
    self.trace_lines = True  # True: trace lines in enabled functions.
    self.verbose = verbose  # True: print filename:func
    self.set_patterns(patterns)
    try:  # Don't assume g.app exists.
        from leo.core.leoQt import QtCore
        if QtCore:
            # pylint: disable=no-member
            QtCore.pyqtRemoveInputHook()
    except Exception:
        pass
# @+node:ekr.20140326100337.16844: *5* sherlock.__call__
def __call__(self, frame: Any, event: Any, arg: Any) -> Any:
    """Exists so that self.dispatch can return self."""
    return self.dispatch(frame, event, arg)
# @+node:ekr.20140326100337.16846: *5* sherlock.bad_pattern
def bad_pattern(self, pattern: Any) -> None:
    """Report a bad Sherlock pattern."""
    if pattern not in self.bad_patterns:
        self.bad_patterns.append(pattern)
        print(f"\nignoring bad pattern: {pattern}\n")
# @+node:ekr.20140326100337.16847: *5* sherlock.check_pattern
def check_pattern(self, pattern: str) -> bool:
    """Give an error and return False for an invalid pattern."""
    try:
        for prefix in ('+:', '-:', '+', '-'):
            if pattern.startswith(prefix):
                re.match(pattern[len(prefix) :], 'xyzzy')
                return True
        self.bad_pattern(pattern)
        return False
    except Exception:
        self.bad_pattern(pattern)
        return False
# @+node:ekr.20121128031949.12609: *5* sherlock.dispatch
def dispatch(self, frame: Any, event: Any, arg: Any) -> Any:
    """The dispatch method."""
    if event == 'call':
        self.do_call(frame, arg)
    elif event == 'return' and self.show_return:
        self.do_return(frame, arg)
    elif event == 'line' and self.trace_lines:
        self.do_line(frame, arg)
    # Queue the SherlockTracer instance again.
    return self
# @+node:ekr.20121128031949.12603: *5* sherlock.do_call & helper
def do_call(self, frame: Any, unused_arg: Any) -> None:
    """Trace through a function call."""
    frame1 = frame
    code = frame.f_code
    file_name = code.co_filename
    locals_ = frame.f_locals
    function_name = code.co_name
    try:
        full_name = self.get_full_name(locals_, function_name)
    except Exception:
        full_name = function_name
    if not self.is_enabled(file_name, full_name, self.patterns):
        # 2020/09/09: Don't touch, for example, __ methods.
        return
    n = 0  # The number of callers of this def.
    while frame:
        frame = frame.f_back
        n += 1
    indent = ' ' * max(0, n - self.n) if self.indent else ''
    path = f"{os.path.basename(file_name):>20}" if self.verbose else ''
    leadin = '+' if self.show_return else ''
    args_list = self.get_args(frame1)
    if self.show_args and args_list:
        args_s = ','.join(args_list)
        args_s2 = f"({args_s})"
        if len(args_s2) > 100:
            print(f"{path}:{indent}{leadin}{full_name}")
            g.printObj(args_list, indent=len(indent) + 22)
        else:
            print(f"{path}:{indent}{leadin}{full_name}{args_s2}")
    else:
        print(f"{path}:{indent}{leadin}{full_name}")
    # Always update stats.
    d = self.stats.get(file_name, {})
    d[full_name] = 1 + d.get(full_name, 0)
    self.stats[file_name] = d
# @+node:ekr.20130111185820.10194: *6* sherlock.get_args
def get_args(self, frame: Any) -> list[str]:
    """Return a list of string "name=val" for each arg in the function call."""
    code = frame.f_code
    locals_ = frame.f_locals
    name = code.co_name
    n = code.co_argcount
    if code.co_flags & 4:
        n = n + 1
    if code.co_flags & 8:
        n = n + 1
    result = []
    for i in range(n):
        name = code.co_varnames[i]
        if name != 'self':
            arg = locals_.get(name, '*undefined*')
            if arg:
                if isinstance(arg, (list, tuple)):
                    val_s = ','.join([self.show(z) for z in arg if self.show(z)])
                    val = f"[{val_s}]"
                elif isinstance(arg, str):
                    val = arg
                else:
                    val = self.show(arg)
                if val:
                    result.append(f"{name}={val}")
    return result
# @+node:ekr.20140402060647.16845: *5* sherlock.do_line (not used)
bad_fns: list[str] = []

def do_line(self, frame: Any, arg: Any) -> None:
    """print each line of enabled functions."""
    if 1:
        return
    code = frame.f_code
    file_name = code.co_filename
    locals_ = frame.f_locals
    name = code.co_name
    full_name = self.get_full_name(locals_, name)
    if not self.is_enabled(file_name, full_name, self.patterns):
        return
    n = frame.f_lineno - 1  # Apparently, the first line is line 1.
    d = self.contents_d
    lines = d.get(file_name)
    if not lines:
        print(file_name)
        try:
            with open(file_name) as f:
                s = f.read()
        except Exception:
            if file_name not in self.bad_fns:
                self.bad_fns.append(file_name)
                print(f"open({file_name}) failed")
            return
        lines = g.splitLines(s)
        d[file_name] = lines
    line = lines[n].rstrip() if n < len(lines) else '<EOF>'
    if 0:
        print(f"{name:3} {line}")
    else:
        print(f"{g.shortFileName(file_name)} {n} {full_name} {line}")
# @+node:ekr.20130109154743.10172: *5* sherlock.do_return & helper
def do_return(self, frame: Any, arg: Any) -> None:  # Arg *is* used below.
    """Trace a return statement."""
    code = frame.f_code
    fn = code.co_filename
    locals_ = frame.f_locals
    name = code.co_name
    self.full_name = self.get_full_name(locals_, name)
    if not self.is_enabled(fn, self.full_name, self.patterns):
        return
    n = 0
    while frame:
        frame = frame.f_back
        n += 1
    path = f"{os.path.basename(fn):>20}" if self.verbose else ''
    if name and name == '__init__':
        try:
            ret1 = locals_ and locals_.get('self', None)
            self.put_ret(ret1, n, path)
        except NameError:
            self.put_ret(f"<{ret1.__class__.__name__}>", n, path)
    else:
        self.put_ret(arg, n, path)
# @+node:ekr.20220605141445.1: *6* sherlock.put_ret
def put_ret(self, arg: Any, n: int, path: str) -> None:
    """Print arg, the value returned by a "return" statement."""
    indent = ' ' * max(0, n - self.n + 1) if self.indent else ''
    try:
        if isinstance(arg, types.GeneratorType):
            ret = '<generator>'
        elif isinstance(arg, (tuple, list)):
            ret_s = ','.join([self.show(z) for z in arg])
            if len(ret_s) > 40:
                g.printObj(arg, indent=len(indent))
                ret = ''
            else:
                ret = f"[{ret_s}]"
        elif arg:
            ret = self.show(arg)
            if len(ret) > 100:
                ret = f"\n    {ret}"
        else:
            ret = '' if arg is None else repr(arg)
        print(f"{path}:{indent}-{self.full_name} -> {ret}")
    except Exception:
        exctype, value = sys.exc_info()[:2]
        try:  # Be extra careful.
            arg_s = f"arg: {arg!r}"
        except Exception:
            arg_s = ''  # arg.__class__.__name__
        print(
            f"{path}:{indent}-{self.full_name} -> "
            f"{exctype.__name__}, {value} {arg_s}"
        )
# @+node:ekr.20121128111829.12185: *5* sherlock.fn_is_enabled
def fn_is_enabled(self, func: Any, patterns: list[str]) -> bool:
    """Return True if tracing for the given function is enabled."""
    if func in self.ignored_functions:
        return False

    def ignore_function() -> None:
        if func not in self.ignored_functions:
            self.ignored_functions.append(func)
            print(f"Ignore function: {func}")
    #
    # New in Leo 6.3. Never trace dangerous functions.
    table = (
        '_deepcopy.*',
        # Unicode primitives.
        'encode\b', 'decode\b',
        # System functions
        '.*__next\b',
        '<frozen>', '<genexpr>', '<listcomp>',
        # '<decorator-gen-.*>',
        'get\b',
        # String primitives.
        'append\b', 'split\b', 'join\b',
        # File primitives...
        'access_check\b', 'expanduser\b', 'exists\b', 'find_spec\b',
        'abspath\b', 'normcase\b', 'normpath\b', 'splitdrive\b',
    )
    g.trace('=====', func)
    for z in table:
        if re.match(z, func):
            ignore_function()
            return False
    #
    # Legacy code.
    try:
        enabled, pattern = False, None
        for pattern in patterns:
            if pattern.startswith('+:'):
                if re.match(pattern[2:], func):
                    enabled = True
            elif pattern.startswith('-:'):
                if re.match(pattern[2:], func):
                    enabled = False
        return enabled
    except Exception:
        self.bad_pattern(pattern)
        return False
# @+node:ekr.20130112093655.10195: *5* sherlock.get_full_name
def get_full_name(self, locals_: Any, name: str) -> str:
    """Return class_name::name if possible."""
    full_name = name
    try:
        user_self = locals_ and locals_.get('self', None)
        if user_self:
            full_name = user_self.__class__.__name__ + '::' + name
    except Exception:
        pass
    return full_name
# @+node:ekr.20121128111829.12183: *5* sherlock.is_enabled
ignored_files: list[str] = []  # List of files.
ignored_functions: list[str] = []  # List of files.

def is_enabled(
    self,
    file_name: str,
    function_name: str,
    patterns: list[str] = None,
) -> bool:
    """Return True if tracing for function_name in the given file is enabled."""
    #
    # New in Leo 6.3. Never trace through some files.
    if not os:
        return False  # Shutting down.
    base_name = os.path.basename(file_name)
    if base_name in self.ignored_files:
        return False

    def ignore_file() -> None:
        if base_name not in self.ignored_files:
            self.ignored_files.append(base_name)

    def ignore_function() -> None:
        if function_name not in self.ignored_functions:
            self.ignored_functions.append(function_name)

    if f"{os.sep}lib{os.sep}" in file_name:
        ignore_file()
        return False
    if base_name.startswith('<') and base_name.endswith('>'):
        ignore_file()
        return False
    #
    # New in Leo 6.3. Never trace dangerous functions.
    table = (
        '_deepcopy.*',
        # Unicode primitives.
        'encode\b', 'decode\b',
        # System functions
        '.*__next\b',
        '<frozen>', '<genexpr>', '<listcomp>',
        # '<decorator-gen-.*>',
        'get\b',
        # String primitives.
        'append\b', 'split\b', 'join\b',
        # File primitives...
        'access_check\b', 'expanduser\b', 'exists\b', 'find_spec\b',
        'abspath\b', 'normcase\b', 'normpath\b', 'splitdrive\b',
    )
    for z in table:
        if re.match(z, function_name):
            ignore_function()
            return False
    #
    # Legacy code.
    enabled = False
    if patterns is None:
        patterns = self.patterns
    for pattern in patterns:
        try:
            if pattern.startswith('+:'):
                if re.match(pattern[2:], file_name):
                    enabled = True
            elif pattern.startswith('-:'):
                if re.match(pattern[2:], file_name):
                    enabled = False
            elif pattern.startswith('+'):
                if re.match(pattern[1:], function_name):
                    enabled = True
            elif pattern.startswith('-'):
                if re.match(pattern[1:], function_name):
                    enabled = False
            else:
                self.bad_pattern(pattern)
        except Exception:
            self.bad_pattern(pattern)
    return enabled
# @+node:ekr.20121128111829.12182: *5* sherlock.print_stats
def print_stats(self, patterns: list[str] = None) -> None:
    """Print all accumulated statisitics."""
    print('\nSherlock statistics...')
    if not patterns:
        patterns = ['+.*', '+:.*',]
    for fn in sorted(self.stats.keys()):
        d = self.stats.get(fn)
        if self.fn_is_enabled(fn, patterns):
            result = sorted(d.keys())  # type:ignore
        else:
            result = [key for key in sorted(d.keys())  # type:ignore
                if self.is_enabled(fn, key, patterns)]
        if result:
            print('')
            fn = fn.replace('\\', '/')
            parts = fn.split('/')
            print('/'.join(parts[-2:]))
            for key in result:
                print(f"{d.get(key):4} {key}")
# @+node:ekr.20121128031949.12614: *5* sherlock.run
# Modified from pdb.Pdb.set_trace.

def run(self, frame: Any = None) -> None:
    """Trace from the given frame or the caller's frame."""
    print("SherlockTracer.run:patterns:\n%s" % '\n'.join(self.patterns))
    if frame is None:
        frame = sys._getframe().f_back
    # Compute self.n, the number of frames to ignore.
    self.n = 0
    while frame:
        frame = frame.f_back
        self.n += 1
    # Pass self to sys.settrace to give easy access to all methods.
    sys.settrace(self)
# @+node:ekr.20140322090829.16834: *5* sherlock.push & pop
def push(self, patterns: list[str]) -> None:
    """Push the old patterns and set the new."""
    self.pattern_stack.append(self.patterns)  # type:ignore
    self.set_patterns(patterns)
    print(f"SherlockTracer.push: {self.patterns}")

def pop(self) -> None:
    """Restore the pushed patterns."""
    if self.pattern_stack:
        self.patterns = self.pattern_stack.pop()  # type:ignore
        print(f"SherlockTracer.pop: {self.patterns}")
    else:
        print('SherlockTracer.pop: pattern stack underflow')
# @+node:ekr.20140326100337.16845: *5* sherlock.set_patterns
def set_patterns(self, patterns: list[str]) -> None:
    """Set the patterns in effect."""
    self.patterns = [z for z in patterns if self.check_pattern(z)]
# @+node:ekr.20140322090829.16831: *5* sherlock.show
def show(self, item: Any) -> str:
    """return the best representation of item."""
    if not item:
        return repr(item)
    if isinstance(item, dict):
        return 'dict'
    if isinstance(item, str):
        s = repr(item)
        if len(s) <= 20:
            return s
        return s[:17] + '...'
    s = repr(item)
    # A Hack for mypy:
    if s.startswith("<object object"):
        s = "_dummy"
    return s
# @+node:ekr.20121128093229.12616: *5* sherlock.stop
def stop(self) -> None:
    """Stop all tracing."""
    sys.settrace(None)
# @+node:ekr.20181202062518.1: *3* leoFastRedraw.py
"""
Gui-independent fast-redraw code.

For an explanation, see this thread:
https://groups.google.com/forum/#!topic/leo-editor/hpHyHU2sWtM
"""
import difflib
import re
import time
from leo.core import leoGlobals as g

class FastRedraw:
    @others

@language python
@tabwidth -4
@pagewidth 70
# @+node:ekr.20181202060924.4: *4* LeoGui.dump_diff_op_codes
def dump_diff_op_codes(self, a, b, op_codes):
    """Dump the opcodes returned by difflib.SequenceMatcher."""

    def summarize(aList):
        pat = re.compile(r'.*:.*:(.*)')
        return ', '.join([pat.match(z).group(1) for z in aList])

    for tag, i1, i2, j1, j2 in op_codes:
        if tag == 'equal':
            print(f"{tag:7} at {i1}:{i2} (both) ==> {summarize(b[j1:j2])!r}")
        elif tag == 'insert':
            print(f"{tag:7} at {i1}:{i2} (b)    ==> {summarize(b[j1:j2])!r}")
        elif tag == 'delete':
            print(f"{tag:7} at {i1}:{i2} (a)    ==> {summarize(a[i1:i2])!r}")
        elif tag == 'replace':
            print(f"{tag:7} at {i1}:{i2} (a)    ==> {summarize(a[i1:i2])!r}")
            print(f"{tag:7} at {i1}:{i2} (b)    ==> {summarize(b[j1:j2])!r}")
        else:
            print('unknown tag')
# @+node:ekr.20181202060924.5: *4* LeoGui.dump_opcodes
def dump_opcodes(self, opcodes):
    """Dump the opcodes returned by app.peep_hole and app.make_redraw_list."""
    for z in opcodes:
        kind = z[0]
        if kind == 'replace':
            kind, i1, gnxs1, gnxs2 = z
            print(kind, i1)
            print("  a: [%s]" % ',\n    '.join(gnxs1))
            print("  b: [%s]" % ',\n    '.join(gnxs2))
        elif kind in ('delete', 'insert'):
            kind, i1, gnxs = z
            print(kind, i1)
            print("  [%s]" % ',\n    '.join(gnxs))
        else:
            print(z)
# @+node:ekr.20181202060924.2: *4* LeoGui.flatten_outline
def flatten_outline(self, c):
    """Return a flat list of strings "level:gnx" for all *visible* positions."""
    trace = False and not g.unitTesting
    t1 = time.process_time()
    aList: list[str] = []
    for p in c.rootPosition().self_and_siblings():
        self.extend_flattened_outline(aList, p)
    if trace:
        t2 = time.process_time()
        print(f"app.flatten_outline: {len(aList)} entries {t2 - t1:6.4f} sec.")
    return aList

def extend_flattened_outline(self, aList, p):
    """Add p and all p's visible descendants to aList."""
    # Padding the fields causes problems later.
    aList.append(f"{p.level()}:{p.gnx}:{p.h}\n")
    if p.isExpanded():
        for child in p.children():
            self.extend_flattened_outline(aList, child)
# @+node:ekr.20181202060924.3: *4* LeoGui.make_redraw_list
def make_redraw_list(self, a, b):
    """
    Diff the a (old) and b (new) outline lists.
    Then optimize the diffs to create a redraw instruction list.
    """
    trace = False and not g.unitTesting
    if a == b:
        return []

    def gnxs(aList):
        """Return the gnx list. Do not try to remove this!"""
        return [z.strip() for z in aList]
    @others # Define local helpers
    d = difflib.SequenceMatcher(None, a, b)
    op_codes = list(d.get_opcodes())
    # dump_diff_op_codes(a, b, op_codes)
    #
    # Generate the instruction list, and verify the result.
    opcodes, result = [], []
    for tag, i1, i2, j1, j2 in op_codes:
        if tag == 'insert':
            opcodes.append(['insert', i1, gnxs(b[j1:j2])])
        elif tag == 'delete':
            opcodes.append(['delete', i1, gnxs(a[i1:i2])])
        elif tag == 'replace':
            opcodes.append(['replace', i1, gnxs(a[i1:i2]), gnxs(b[j1:j2])])
        result.extend(b[j1:j2])
    assert b == result, (a, b)
    #
    # Run the peephole.
    opcodes = self.peep_hole(opcodes)
    if trace:
        print('app.make_redraw_list: opcodes after peephole...')
        self.dump_opcodes(opcodes)
    return opcodes
# @+node:ekr.20181202060924.6: *4* LeoGui.peep_hole
def peep_hole(self, opcodes):
    """Scan the list of opcodes, merging adjacent op-codes."""
    i, result = 0, []
    while i < len(opcodes):
        op0 = opcodes[i]
        if i == len(opcodes) - 1:
            result.append(op0)
            break
        op1 = opcodes[i + 1]
        kind0, kind1 = op0[0], op1[0]
        # Merge adjacent insert/delete opcodes with the same gnx.
        if (
            kind0 == 'insert' and kind1 == 'delete' or
            kind0 == 'delete' and kind1 == 'insert'
        ):
            kind0, index0, gnxs0 = op0
            kind1, index1, gnxs1 = op1
            if gnxs0[0] == gnxs1[0]:
                result.append(['move', index0, index1, gnxs0, gnxs1])
                i += 2  # Don't scan either op again!
                break
        # The default is to retain the opcode.
        result.append(op0)
        i += 1
    return result
# @+node:ekr.20260323083821.41: *3* Theory of operation: c.deletePositionsInList
@language rest

*** Note: As of Leo 6.8.8, Leo uses a much simpler version of c.deletePositionsInList.
*** Happily, this documentation is no longer needed.

The positions passed to c.deletePositionsInList only *specify* the desired
changes; the only way to *make* those changes is to operate on vnodes!

Consider this outline, containing no clones::

    + ROOT
      - A
      - B

The fundamental problem is this. If we delete node A, the index of
node B in ROOT.children will change. This problem has (almost) nothing
to do with clones or positions.

**Proof that c.deletePositionsInList is correct**

Clearly, deleting a positions ultimately means changing vnodes,
specifically, v.parents and v.children for v in some set of vnodes. We must
show that the particular set of changed vnodes in the new code is the
correct set of vnodes, and that the changes made to that set are correct.
This is far from obvious at first glance.

**The new code**

Here is Vitalije's version of c.deletePositionsInList, without the docstring:

.. code-block:: python

    def deletePositionsInList(self, aList):
        c = self

        # Ensure all positions are valid.
        aList = [p for p in aList if c.positionExists(p)]
        if not aList:
            return

        def p2link(p):
            parent_v = p.stack[-1][0] if p.stack else c.hiddenRootNode
            return p._childIndex, parent_v

        links_to_be_cut = sorted(set(map(p2link, aList)), key=lambda x:-x[0])

        # The main loop.
        for i, v in links_to_be_cut:
            child_v = v.children.pop(i)
            child_v.parents.remove(v)

To begin the proof, note that the main loop has the expected "form". That
is, when we delete a node, we will:

1. Delete child_v = v.children[i] for some v and i.

2. Delete child_v from its parents array.

v.parents is an unordered array, so child_v.parents.remove(v) is all that
is needed. This line might crash if one of the positions is invalid. It
will also crash if v is not present in child_v.parents. I'll discuss this
later.

To complete the proof, we must show that the main loop deletes all and only
those vnodes implied by the positions in aList. We can tentatively assume
that the main loop will work properly if that data in links_to_be_cut is
correct, but there are some subtleties lurking here which I'll discuss later.

The following code creates links_to_be_cut:

.. code-block:: python

    def p2link(p):
        parent_v = p.stack[-1][0] if p.stack else c.hiddenRootNode
        return p._childIndex, parent_v

    links_to_be_cut = sorted(set(map(p2link, aList)), key=lambda x:-x[0])

This is elegant (compressed) code. Let's examine it carefully...

p2link produces tuples (childIndex, parent_v). These become bound to i, v
in the main loop:

.. code-block:: python

    # The main loop.
    for i, v in links_to_be_cut:
        child_v = v.children.pop(i)
        child_v.parents.remove(v)

To make this work, parent_v must be the vnode whose i'th child should be
deleted:

.. code-block:: python

    parent_v = p.stack[-1][0] if p.stack else c.hiddenRootNode

- p.stack entries have the form (v, childIndex), so p.stack[-1] is the
  position p's immediate parent vnode, if p has a parent. Otherwise,
  c.hiddenRootNode is the proper parent vnode.

- p._childIndex is also the correct value for i.

We have just proved the following::

    Deleting position p means executing the main loop for (i, v) returned by p2link(p)

**Sorting and filtering**

The following line filters and sorts the results produced by p2link:

.. code-block:: python

    links_to_be_cut = sorted(set(map(p2link, aList)), key=lambda x:-x[0]

Let's break this into parts, from "inside" to "outside"

- ``map(p2link, aList)`` applies ``p2link`` to every position in ``aList``.
  The result is a list of tuples ``(i, v)``. This list may contain
  duplicates.

- ``set(map(p2link, aList)`` removes those duplicates.
  Let **theSet** be ``set(map(p2link, aList)``.

- Finally, ``sorted(theSet, key=lambda x:-x[0])`` creates a generator
  equivalent to a sorted list.

The generator will deliver the tuples in descending order of ``i``, when
several tuples have the same vnode ``v``. The generator does not order
tuples on ``v``, and there is no need to do so.

It's easy to understand the intent of this sort order. The main loop
contains this line:

.. code-block:: python

    child_v = v.children.pop(i)

The sort order means that ``pop(i)`` happens before ``pop(j)`` if ``i >
j``. This condition ensures that the precomputed values of ``i`` won't
change in the main loop.

**Completing the proof**

The algorithm appears sound. The tuples ``(i, v)`` computed from
``p2link(p)`` correctly describe the work to be done. Filtering ensures
that the work is done once. Sorting ensures that child indices ``i`` will
not change during the main loop.

The tuples are sorted on ``i``, but not ``v``. ``aList`` can contain positions
in any order, so we know only that for any particular vnode ``v`` the main loop
will handle tuples ``(i1, v), (i2, v), ...`` in the correct order.

There are two final questions to consider:

First, does the order in which the main loop handles vnodes matter?

No. The vnode data are independent. The main loop works regardless of
whether a vnode has already been unlinked.

Second, could the following lines ever fail?

.. code-block:: python

    child_v = v.children.pop(i)
    child_v.parents.remove(v)

No. Sorting and filtering ensure that tuples are unique. ``v`` is guaranteed to
be in ``child_v.parents`` because ``v.children[i]`` must exist.

This concludes the proof.

**Discussion**

The algorithm is sound because the tuples ``(i, v)`` contain no duplicates.
For any particular ``v``, the main loop will process the tuples in descending
order of ``i``. This resolves the question of whether deleting already deleted
positions is valid.
# @+node:ekr.20181103094900.1: *3* ../plugins/leoflexx.py

# Disable mypy checking.
# type:ignore

@language python
@tabwidth -4
# pylint: disable=logging-not-lazy,unused-private-member,wrong-import-position

<< leoflexx: docstring >>
<< leoflexx: imports >>
<< leoflexx: assets >>

# flexx can't handle generators, so we *must* use comprehensions instead.
# pylint: disable=use-a-generator

@others
# @+node:ekr.20181122215342.1: *4* << leoflexx: docstring >>
@language md
@wrap
"""
flexx.py: LeoWapp (Leo as a web browser), implemented using flexx:
https://flexx.readthedocs.io/en/stable/

Start Leo using the --gui=browser option. Like this::

    leo --gui=browser
    leo --gui=browser-firefox

This file is the main line of the LeoWapp project.
https://github.com/leo-editor/leo-editor/issues/1005.

See #1005 for a status report and list of to-do's.

# Prerequisites

Install flexx: https://flexx.readthedocs.io/en/stable/start.html

# What you should see

You should see the flexx (Tornado) server start up in the console.
Something that looks like Leo should then appear in the browser. Everything
you see is real, and most of it is "live".
"""
# @+node:ekr.20181113041314.1: *4* << leoflexx: imports >>
import os
import re
import sys
import time
from typing import Optional

path = os.getcwd()
if path not in sys.path:
    sys.path.insert(0, path)
del path

# This is what Leo typically does.
# JS code can *not* use g.trace, g.callers or g.pdb.
from leo.core import leoGlobals as g
from leo.core import leoFrame
from leo.core import leoGui
from leo.core import leoMenu
from leo.core import leoNodes
from leo.core.leoAPI import StringTextWrapper
# Third-party imports.
try:
    from flexx import flx
    from pscript import RawJS
except Exception:
    flx = None
# @+node:ekr.20181111074958.1: *4* << leoflexx: assets >>
# Assets for ace JS editor.
base_url = 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/'
flx.assets.associate_asset(__name__, base_url + 'ace.js')
flx.assets.associate_asset(__name__, base_url + 'mode-python.js')
flx.assets.associate_asset(__name__, base_url + 'theme-solarized_dark.js')
# @+node:ekr.20181121040901.1: *4*  top-level functions
# @+node:ekr.20181121091633.1: *5* dump_event
def dump_event(ev):
    """Print a description of the event."""
    id_ = ev.source.title or ev.source.text
    kind = '' if ev.new_value else 'un-'
    s = kind + ev.type
    message = '%s: %s' % (s.rjust(15), id_)
    print('dump_event: ' + message)
# @+node:ekr.20181121040857.1: *5* get_root
def get_root():
    """
    Return the LeoBrowserApp instance.

    This is the same as self.root for any flx.Component.
    """
    # Only called at startup, so this will never be None.
    root = flx.loop.get_active_component().root
    assert isinstance(root, LeoBrowserApp), repr(root)
    return root
# @+node:ekr.20181112165240.1: *5* info (deprecated)
def info(s):
    """Send the string s to the flex logger, at level info."""
    if not isinstance(s, str):
        s = repr(s)
    # A hack: automatically add the "Leo" prefix so
    # the top-level suppression logic will not delete this message.
    flx.logger.info('Leo: ' + s)
# @+node:ekr.20181103151350.1: *5* init
def init():
    return flx
# @+node:ekr.20181203151314.1: *5* make_editor_function
def make_editor_function(name, node):
    """
    Instantiate the ace editor.

    Making this a top-level function avoids the need to create a common
    base class that only defines this as a method.
    """
    global window  # pylint doesn't know about the window global.
    ace = window.ace.edit(node, 'editor')  # pylint: disable=undefined-variable
    ace.navigateFileEnd()  # otherwise all lines highlighted
    ace.setTheme("ace/theme/solarized_dark")
    if name == 'body':
        ace.getSession().setMode("ace/mode/python")  # This sets soft tabs.
    if name == 'minibuffer':
        pass  # To do: Disable line numbers.
    return ace
# @+node:ekr.20181113041410.1: *5* suppress_unwanted_log_messages (not used)
def suppress_unwanted_log_messages():
    """
    Suppress the "Automatically scrolling cursor into view" messages by
    *allowing* only important messages.
    """
    allowed = r'(Traceback|Critical|Error|Leo|Session|Starting|Stopping|Warning)'
    pattern = re.compile(allowed, re.IGNORECASE)
    flx.set_log_level('INFO', pattern)
# @+node:ekr.20181115071559.1: *4* Py side: App & wrapper classes
# @+node:ekr.20181127151027.1: *5* class API_Wrapper (StringTextWrapper)
class API_Wrapper(StringTextWrapper):
    """
    A wrapper class that implements the high-level interface.
    """

    def __init__(self, c, name):
        assert name in ('body', 'log', 'minibuffer'), repr(name)
        super().__init__(c, name)
        assert self.c == c
        assert self.name == name
        self.tag = 'py.%s.wrap' % name
        self.root = get_root()

    def flx_wrapper(self):
        if self.root.inited:
            w = self.root.main_window
            return getattr(w, self.name)
        return g.NullObject()

    def setFocus(self):
        # print(self.tag, 'setFocus')
        self.flx_wrapper().set_focus()

    # No need to override getters.
    @others
# @+node:ekr.20181128101421.1: *6* API_Wrapper.Selection Setters
def finish_set_insert(self, tag):
    """Common helper for selection setters."""
    if 'select' in g.app.debug:
        tag = '%s.%s' % (self.tag, 'finish_set_insert')
        print('%30s: %s %r' % (tag, self.ins, self.sel))
    self.flx_wrapper().set_insert_point(self.ins, self.sel)

def seeInsertPoint(self):
    self.flx_wrapper().see_insert_point()

def selectAllText(self, insert=None):
    super().selectAllText(insert)
    self.finish_set_insert('selectAllText')

def setInsertPoint(self, pos, s=None):
    super().setInsertPoint(pos, s)
    self.finish_set_insert('setInsertPoint')

def setSelectionRange(self, i: int, j: int, insert: Optional[int] = None):
    super().setSelectionRange(i, j, insert)
    self.finish_set_insert('setSelectionRange')
# @+node:ekr.20181127121642.1: *6* API_Wrapper.Text Setters
@language rest
@wrap
@
These methods implement Leo's high-level api for the body pane.

Consider Leo's sort-lines command. sort-lines knows nothing about which gui
is in effect. It updates the body pane using *only* the high-level api.

These methods must do two things:

1. Call the corresponding super() method to update self.s, self.i and self.ins.
2. Call the corresponding flx_body methods to update the flx_body widget,
   except while unit testing.
@c
@language python

def finish_setter(self, tag):
    """The common setter code."""
    c = self.c
    tag = 'py.body.setter'
    if self.name == 'body':
        # print('%s: %s' % (tag, len(self.s)))
        # p.b = self.s will cause an unbounded recursion.
        c.p.v.setBodyString(self.s)
    if 0:
        print('%s: %s:  len(self.s): %s ins: %s sel: %r' % (
            self.tag, tag, len(self.s), self.ins, self.sel))
    if not g.unitTesting:
        self.flx_wrapper().set_text(self.s)
        self.flx_wrapper().set_insert_point(self.ins)

def appendText(self, s):
    super().appendText(s)
    self.finish_setter('appendText')

def delete(self, i: int, j: Optional[int] = None):
    super().delete(i, j)
    self.finish_setter('delete')

def deleteTextSelection(self):
    super().deleteTextSelection()
    self.finish_setter('deleteTextSelection')

def insert(self, i: int, s):
    # Called from doPlainChar, insertNewlineHelper, etc. on every keystroke.
    super().insert(i, s)
    self.finish_setter('insert')

def setAllText(self, s):
    # Called by set_body_text_after_select.
    super().setAllText(s)
    self.finish_setter('insert')
# @+node:ekr.20181206153831.1: *5* class DummyFrame
class DummyFrame(leoFrame.NullFrame):
    """
    A frame to keep Leo's core happy until we can call app.finish_create.
    """

    def __repr__(self):
        return 'DummyFrame: %r' % self.c.shortFileName()

    __str__ = __repr__

# @+node:ekr.20181107052522.1: *5* class LeoBrowserApp
# pscript never converts flx.PyComponents to JS.

class LeoBrowserApp(flx.PyComponent):
    """
    The browser component of Leo in the browser.

    This is self.root for all flexx components.

    This is *not* g.app.
    """

    main_window = flx.ComponentProp(settable=True)

    @others
# @+node:ekr.20181126103604.1: *6*  app.Initing
# @+node:ekr.20181114015356.1: *7* app.create_all_data
def create_gnx_to_vnode(self):
    t1 = time.process_time()
    # This is likely the only data that ever will be needed.
    self.gnx_to_vnode = {v.gnx: v for v in self.c.all_unique_nodes()}
    if 0:
        print('app.create_all_data: %5.3f sec. %s entries' % (
            (time.process_time() - t1), len(list(self.gnx_to_vnode.keys()))))
    self.test_round_trip_positions()
# @+node:ekr.20181124133513.1: *7* app.finish_create (leoflexx.py) (patches c.request_focus)
@flx.action
def finish_create(self):
    """
    Initialize all ivars and widgets.

    Called after all flx.Widgets have been fully inited!
    """
    w = self.main_window
    self.c = c = g.app.log.c
    assert c
    # Init all redraw ivars
    self.create_gnx_to_vnode()
    # Select the proper position.
    c.selectPosition(c.p or c.rootPosition())
    c.contractAllHeadlines()
    c.redraw()  # 2380.
    # Monkey-patch the FindTabManager
    c.findCommands.minibuffer_mode = True
    c.findCommands.ftm = g.NullObject()
    # Monkey-patch c.request_focus
    self.old_request_focus = c.request_focus
    c.request_focus = self.request_focus
    # #1143: monkey-patch Leo's save commands.
    self.old_save_file = c.fileCommands.save
    c.fileCommands.save = self.save_file
    self.old_save_file_as = c.fileCommands.saveAs
    c.fileCommands.saveAs = self.save_file_as
    self.old_save_file_to = c.fileCommands.saveTo
    c.fileCommands.saveTo = self.save_file_to
    # Init the log, body, status line and tree.
    g.app.gui.writeWaitingLog2()
    self.set_body_text()
    self.set_status()
    # Init the focus. It's debatable...
    if 0:
        self.gui.set_focus(c, c.frame.tree)
        w.tree.set_focus()
    else:  # This definitely shows focus in the editor.
        self.gui.set_focus(c, c.frame.body)
        w.body.set_focus()
    # Set the inited flag *last*.
    self.inited = True
# @+node:ekr.20181216042806.1: *7* app.init (leoflexx.py)
def init(self):
    # Set the ivars.
    global g  # Always use the imported g.
    self.inited = False  # Set in finish_create
    self.tag = 'py.app.wrap'
    # Open or get the first file.
    if not g.app:
        print('app.init: no g.app. g:', repr(g))
        return
    if not isinstance(g.app.gui, LeoBrowserGui):
        print('app.init: wrong g.app.gui:', repr(g.app.gui))
        return
    if not isinstance(g.app.log, leoFrame.NullLog):
        # This can happen in strange situations.
        # print('app.init: ===== Ctrl-H ===== ?')
        return
    # We are running with --gui=browser.
    c = g.app.log.c
    # self.gui must be a synonym for g.app.gui.
    self.c = c
    self.gui = gui = g.app.gui
    # Make sure everything is as expected.
    assert self.c and c == self.c
    assert g.app.gui.guiName() == 'browser'
    # When running from Leo's core, we must wait until now to set LeoBrowserGui.root.
    gui.root = get_root()
    # Check g.app.ivars.
    assert g.app.windowList
    for frame in g.app.windowList:
        assert isinstance(frame, DummyFrame), repr(frame)
    # Instantiate all wrappers here, not in app.finish_create.
    title = c.computeWindowTitle()
    c.frame = gui.lastFrame = LeoBrowserFrame(c, title, gui)
    # The main window will be created (much) later.
    main_window = LeoFlexxMainWindow()
    self._mutate('main_window', main_window)
# @+node:ekr.20190507110902.1: *6* app.action.cls
@flx.action
def cls(self):
    """Clear the console"""
    g.cls()
# @+node:ekr.20181117163223.1: *6* app.action.do_key
# https://flexx.readthedocs.io/en/stable/ui/widget.html#flexx.ui.Widget.key_down
# See Widget._create_key_event in flexx/ui/_widget.py:

@flx.action
def do_key(self, ev, ivar):
    """
    LeoBrowserApp.do_key: The central key handler.

    Will be called *in addition* to any inner key handlers,
    unless the inner key handler calls e.preventDefault()
    """
    if 'keys' in g.app.debug:
        g.trace(ev, ivar)
    c = self.c
    # Essential: there is no way to pass the actual wrapper.
    browser_wrapper = getattr(c.frame, ivar)
    << check browser_wrapper >>
    # ev is a dict, keys are type, source, key, modifiers
    # mods in ('Alt', 'Shift', 'Ctrl', 'Meta')
    key, mods = ev['key'], ev['modifiers']
    # Special case Ctrl-H and Ctrl-F.
    if mods == ['Ctrl'] and key in 'fh':
        command = 'find' if key == 'f' else 'head'
        self.do_command(command, key, mods)
        return
    << set char to the translated key name >>
    binding = '%s%s' % (''.join(['%s+' % (z) for z in mods]), char)
    << create key_event >>
    c.k.masterKeyHandler(key_event)
# @+node:ekr.20181129073812.1: *7* << check browser_wrapper >>
assert isinstance(browser_wrapper, (
    LeoBrowserBody,
    LeoBrowserLog,
    LeoBrowserMinibuffer,
    LeoBrowserStatusLine,
    LeoBrowserTree,
)), repr(browser_wrapper)
# @+node:ekr.20181129073905.1: *7* << set char to the translated key name >>
d = {
    'ArrowDown': 'Down',
    'ArrowLeft': 'Left',
    'ArrowRight': 'Right',
    'ArrowUp': 'Up',
    'Enter': '\n',  # For k.fullCommand, etc.
    'PageDown': 'Next',
    'PageUp': 'Prior',
}
char = d.get(key, key)
if 'Ctrl' in mods:
    mods.remove('Ctrl')
    mods.append('Control')
# @+node:ekr.20181129073734.1: *7* << create key_event >>
# create the key event, but don't bother tracing it.
old_debug = g.app.debug
try:
    g.app.debug = []
    key_event = leoGui.LeoKeyEvent(c,
        char=char,
        binding=binding,
        event={'c': c},
        w=browser_wrapper,
    )
finally:
    g.app.debug = old_debug
# @+node:ekr.20181124054536.1: *6* app.action.dump_dict
@flx.action
def dump_dict(self, obj, tag):
    # Note: flexx has problems with keyword args.
    g.printObj(obj, tag=tag)

# def message(self, s):
    # """For testing."""
    # print('app.message: %s' % s)
# @+node:ekr.20181207080933.1: *6* app.action.set_body_text & set_status
# These must be separate because they are called from the tree logic.

@flx.action
def set_body_text(self):
    c = self.c
    # Using the wrapper sets the text *and* the insert point and selection range.
    c.frame.body.wrapper.setAllText(c.p.b)
    # print('app.set_body_text', len(c.p.b), c.p.h)

@flx.action
def set_status(self):
    c, w = self.c, self.main_window
    lt, rt = c.frame.statusLine.update(c.p.b, 0)
    w.status_line.update(lt, rt)
# @+node:ekr.20181122132345.1: *6* app.Drawing...
# @+node:ekr.20181113042549.1: *7* app.action.redraw
@flx.action
def redraw(self, p):
    """
    Send a **redraw list** to the tree.

    This is a recusive list lists of items (ap, gnx, headline) describing
    all and *only* the presently visible nodes in the tree.

    As a side effect, app.make_redraw_dict updates all internal dicts.
    """
    trace = 'drawing' in g.app.debug
    tag = 'py.app.redraw'
    c = self.c
    p = p or c.p
    # #1142.
    c.selectPosition(p)
    w = self.main_window
    # Be careful: c.frame.redraw can be called before app.finish_create.
    if not w or not w.tree:
        if trace:
            print(tag, 'no w.tree')
        return
    # Do a full redraw.
    t1 = time.process_time()
    ap = self.p_to_ap(p)
    w.tree.select_ap(ap)
    redraw_dict = self.make_redraw_dict(p)
    w.tree.redraw_with_dict(redraw_dict)
    # Do not call c.setChanged() here.
    if trace:
        print('app.redraw: %5.3f sec.' % (time.process_time() - t1))
# @+node:ekr.20181111095640.1: *7* app.action.send_children_to_tree
@flx.action
def send_children_to_tree(self, parent_ap):
    """
    Call w.tree.receive_children(d), where d is compatible with make_redraw_dict:
        {
            'parent_ap': parent_ap,
            'items': [
                self.make_dict_for_position(p)
                    for p in for p in p.children()
            ],
        }
    """
    p = self.ap_to_p(parent_ap)
    assert p, repr(parent_ap)
    if 0:
        # There is a similar trace in flx.tree.receive_children.
        print('send_children_to_tree: %s children' % len(list(p.children())))
        if 0:  # Corresponds to the trace in flx_tree.populate_children.
            for child in p.children():
                print('  ' + child.h)
    #
    # Always respond, even if there are no children.
    # This allows the tree widget to reset state properly.
    w = self.main_window
    # assert parent_ap == self.p_to_ap(p), '\n%r\n%r' % (parent_ap, self.p_to_ap(p))
        # This assert can fail because the expansion bits don't match.
    w.tree.receive_children({
        'parent_ap': parent_ap,
        'items': [
            self.make_dict_for_position(p)
                for p in p.children()
            # For compatibility with flx.tree.create_item_with_parent.
        ],
    })
# @+node:ekr.20181111203114.1: *7* app.ap_to_p
def ap_to_p(self, ap):
    """Convert an archived position to a true Leo position."""
    childIndex = ap['childIndex']
    v = self.gnx_to_vnode[ap['gnx']]
    stack = [
        (self.gnx_to_vnode[d['gnx']], d['childIndex'])
            for d in ap['stack']
    ]
    return leoNodes.Position(v, childIndex, stack)
# @+node:ekr.20181124071215.1: *7* app.dump_top_level
def dump_top_level(self):
    """Dump the top-level nodes."""
    if 0:
        c = self.c
        print('\napp.dump_top_level...')
        # print('root:', c.rootPosition().h)
        # print(' c.p:', c.p.h)
        # print('')
        # print('Top-level nodes...')
        for p in c.rootPosition().self_and_siblings():
            print('  %5s %s' % (p.isExpanded(), p.h))
        print('')
# @+node:ekr.20181113043539.1: *7* app.make_redraw_dict & helper
def make_redraw_dict(self, p=None):
    """
    Return a **recursive**, archivable, list of lists describing all the
    visible nodes of the tree.

    As a side effect, recreate gnx_to_vnode.
    """
    c = self.c
    p = p or c.p
    t1 = time.process_time()
    c.expandAllAncestors(c.p)  # Ensure that c.p will be shown.
    d = {
        'c.p': self.p_to_ap(p),
        'items': [
            self.make_dict_for_position(p2)
                for p2 in c.rootPosition().self_and_siblings()
        ],
    }
    t2 = time.process_time()
    if 0:
        print('app.make_redraw_dict: %s direct children %5.3f sec.' % (
            len(list(c.rootPosition().self_and_siblings())), (t2 - t1)))
    return d
# @+node:ekr.20181113044701.1: *8* app.make_dict_for_position
def make_dict_for_position(self, p):
    """
    Recursively add a sublist for p and all its visible nodes.
    It is already known that p itself is visible.
    """
    assert p.v
    self.gnx_to_vnode[p.v.gnx] = p.v
    if 0:
        # A superb trace. There are similar traces in:
        # - flx_tree.redraw_with_dict  and its helper, flx_tree.create_item_with_parent.
        # - flx_tree.populate_children and its helper, flx_tree.create_item_for_ap
        print('make_dict_for_position: %s%s' % ('  ' * p.level(), p.v.h))
    if p.isExpanded():  # Do not use p.v.isExpanded().
        children = [
            self.make_dict_for_position(child)
                for child in p.children()
        ]
    else:
        children = []
    return {
        'ap': self.p_to_ap(p),
        # 'body': p.b, # This would transfer much too much data.
        'children': children,
        'gnx': p.v.gnx,
        'headline': p.v.h,
    }
# @+node:ekr.20190511091601.1: *6* app.Editing
# @+node:ekr.20181129122147.1: *7* app.edit_headline & helper
def edit_headline(self):
    """Simulate editing the headline in the minibuffer."""
    w = self.root.main_window
    mb = w.minibuffer
    mb.set_text('Enter Headline: ')
    mb.set_focus()

def edit_headline_completer(self, headline):
    print('app.edit_headline_completer')
# @+node:ekr.20190512081356.1: *6* app.request_focus
def request_focus(self, w):
    """Monkey-patched c.request_focus."""
    tag = 'py.app.request_focus'
    trace = 'focus' in g.app.debug
    if not w:
        print('%30s: NO W: %s' % (tag, g.callers()))
        return
    #
    # Schedule the change in focus.
    mw = self.main_window
    table = (
        ('body', mw.body),
        ('log', mw.log),
        ('mini', mw.minibuffer),
        ('tree', mw.tree),
    )
    for w_name, flx_w in table:
        if w_name in repr(w):
            if trace:
                print('%30s: %s' % (tag, flx_w.__class__.__name__))
            flx_w.set_focus()
            return
    if trace:
        print('%30s: FAIL %r' % (tag, w))
# @+node:ekr.20190511091236.1: *6* app.Minibuffer
# @+node:ekr.20181127070903.1: *7* app.execute_minibuffer_command
def execute_minibuffer_command(self, commandName, char, mods):
    """Start the execution of a minibuffer command."""
    # Called by app.do_command.
    c = self.c
    # New code: execute directly.
    self.complete_minibuffer_command({
        'char': char,
        'commandName': commandName,
        'headline': c.p.h,  # Debugging.
        'mods': mods,
    })
# @+node:ekr.20190510133737.1: *7* app.action.complete_minibuffer_command
@flx.action
def complete_minibuffer_command(self, d):
    """Complete the minibuffer command using d."""
    c = self.c
    k, w = c.k, c.frame.body.wrapper
    #
    # Do the minibuffer command: like k.callAltXFunction.
    commandName, char, mods = d['commandName'], d['char'], d['mods']
    # Same as in app.do_key.
    binding = '%s%s' % (''.join(['%s+' % (z) for z in mods]), char)
    event = g.Bunch(
        c=c,
        char=char,
        stroke=binding,
        widget=w,  # Use the body pane by default.
    )
    # Another hack.
    k.functionTail = None
    if commandName and commandName.isdigit():
        # The line number Easter Egg.
        def func(event=None):
            c.gotoCommands.find_file_line(n=int(commandName))
    else:
        func = c.commandsDict.get(commandName)
    if func:
        # These must be done *after* getting the command.
        k.clearState()
        k.resetLabel()
        if commandName != 'repeat-complex-command':
            k.mb_history.insert(0, commandName)
        w = event and event.widget
        if hasattr(w, 'permanent') and not w.permanent:
            # In a headline that is being edited.
            # g.es('Can not execute commands from headlines')
            c.endEditing()
            c.bodyWantsFocusNow()
            # Change the event widget so we don't refer to the to-be-deleted headline widget.
            event.w = event.widget = c.frame.body.wrapper.widget
        else:
            # Important, so cut-text works, e.g.
            c.widgetWantsFocusNow(event and event.widget)
        try:
            func(event)
        except Exception:
            g.es_exception()
        return True
    if 0:
        # Not ready yet
        # Show possible completions if the command does not exist.
        if 1:  # Useful.
            k.doTabCompletion(list(c.commandsDict.keys()))
        else:  # Annoying.
            k.keyboardQuit()
            k.setStatusLabel('Command does not exist: %s' % commandName)
            c.bodyWantsFocus()
    return False
# @+node:ekr.20190511102058.1: *6* app.Save commands
# These all monkey-patch the corresponding c.fileCommands methods.
# @+node:ekr.20190511100908.1: *7* app.save_file
def save_file(self, fileName):
    """
    Monkey-patched override of c.fileCommands.save.

    Sync p.b before calling the original method.
    """
    if not self.root.inited:
        return
    if 'select' in g.app.debug:
        tag = 'py.app.save_file'
        print('%30s: %s' % (tag, fileName))
    c = self.c
    w = self.root.main_window
    w.body.sync_body_before_save_file({
        'ap': self.root.p_to_ap(c.p),
        'fn': fileName,
    })

@flx.action
def complete_save_file(self, d):
    self.update_body_from_dict_helper(d)  # Use the helper, to skip checks.
    fn = d['fn']
    if 'select' in g.app.debug:
        tag = 'py.app.complete_save_file'
        print('%30s: %s' % (tag, fn))
    self.old_save_file(fn)
# @+node:ekr.20190511102119.1: *7* app.save_file_as
def save_file_as(self, fileName):
    """
    Monkey-patched override of c.fileCommands.saveAs.

    Sync p.b before calling the original method.
    """
    if not self.root.inited:
        return
    if 'select' in g.app.debug:
        tag = 'py.app.save_file_as'
        print('%30s: %s' % (tag, fileName))
    c = self.c
    w = self.root.main_window
    w.body.sync_body_before_save_file_as({
        'ap': self.root.p_to_ap(c.p),
        'fn': fileName,
    })

@flx.action
def complete_save_file_as(self, d):
    self.update_body_from_dict_helper(d)  # Use the helper, to skip checks.
    fn = d['fn']
    if 'select' in g.app.debug:
        tag = 'py.app.complete_save_file'
        print('%30s: %s' % (tag, fn))
    self.old_save_file_as(fn)
# @+node:ekr.20190511102120.1: *7* app.save_file_to
def save_file_to(self, fileName):
    """
    Monkey-patched override of c.fileCommands.saveTo.

    Sync p.b before calling the original method.
    """
    if not self.root.inited:
        return
    if 'select' in g.app.debug:
        tag = 'py.app.save_file_to'
        print('%30s: %s' % (tag, fileName))
    c = self.c
    w = self.root.main_window
    w.body.sync_body_before_save_file_to({
        'ap': self.root.p_to_ap(c.p),
        'fn': fileName,
    })

@flx.action
def complete_save_file_to(self, d):
    self.update_body_from_dict_helper(d)  # Use the helper, to skip checks.
    fn = d['fn']
    if 'select' in g.app.debug:
        tag = 'py.app.complete_save_file'
        print('%30s: %s' % (tag, fn))
    self.old_save_file_to(fn)
# @+node:ekr.20181124095316.1: *6* app.Selecting...
# @+node:ekr.20181216051109.1: *7* app.action.complete_select
@flx.action
def complete_select(self, d):
    """Complete the selection of the d['new_ap']"""
    self.update_body_from_dict(d)
    # tree.complete_select has direct ivars to tree ivars.
    self.c.frame.tree.complete_select(d)
# @+node:ekr.20181111202747.1: *7* app.action.select_ap
@flx.action
def select_ap(self, ap):
    """
    Select the position in Leo's core corresponding to the archived position.

    Nothing in the flx.tree needs to be updated.
    """
    assert ap, g.callers()
    c, w = self.c, self.main_window
    p = self.ap_to_p(ap)
    assert p, (repr(ap), g.callers())
    lt, rt = c.frame.statusLine.update()
    w.status_line.update(lt, rt)
    # print('app.select_ap', repr(ap))
    w.tree.select_ap(ap)
    # call LeoTree.select, but not self.select_p.
    c.frame.tree.super_select(p)
# @+node:ekr.20190506100026.1: *7* app.action.select_minibuffer
@flx.action
def select_minibuffer(self):
    """Select the minibuffer in response to user click."""
    c = self.c
    event = g.app.gui.create_key_event(c, w=c.frame.body.wrapper)
    c.k.fullCommand(event)
# @+node:ekr.20181118061020.1: *7* app.action.select_p
@flx.action
def select_p(self, p):
    """
    Select the position in the tree.

    Called from LeoBrowserTree.select, so do *not* call c.frame.tree.select.
    """
    c = self.c
    w = self.main_window
    ap = self.p_to_ap(p)
    # Be careful during startup.
    if not (w and w.tree):
        return
    if 'select' in g.app.debug:
        tag = 'py.app.select_p'
        print('%30s: %4s %s %s' % (tag, len(p.b), p.gnx, p.h))
    w.tree.set_ap(ap)
    # #1142: This code was not the problem.
    body = c.frame.body.wrapper
    w.body.set_text(body.s)
    w.body.set_insert_point(body.ins, body.sel)
# @+node:ekr.20190510053112.1: *7* app.action.select_tree_using_ap
@flx.action
def select_tree_using_ap(self, ap):
    """A helper action, called from flx_tree.on_selected_event."""
    # tag = 'py.app.select_tree_using_ap'
    # print(tag, ap ['headline'])
    p = self.ap_to_p(ap)
    self.c.frame.tree.select(p)
# @+node:ekr.20181111204659.1: *7* app.p_to_ap (updates dict)
def p_to_ap(self, p):
    """
    Convert a true Leo position to a serializable archived position.
    """
    if not p.v:
        print('app.p_to_ap: no p.v: %r %s' % (p, g.callers()))
        assert False, g.callers()
    p_gnx = p.v.gnx
    if p_gnx not in self.gnx_to_vnode:
        # print('=== update gnx_to_vnode', p_gnx, p.h)
            # len(list(self.gnx_to_vnode.keys())
        self.gnx_to_vnode[p_gnx] = p.v
    return {
        'childIndex': p._childIndex,
        'cloned': p.isCloned(),
        'expanded': p.isExpanded(),
        'gnx': p.v.gnx,
        'level': p.level(),
        'headline': p.h,
        'marked': p.isMarked(),
        'stack': [{
            'gnx': stack_v.gnx,
            'childIndex': stack_childIndex,
            'headline': stack_v.h,
        } for (stack_v, stack_childIndex) in p.stack],
    }
# @+node:ekr.20181215154640.1: *7* app.update_body_from_dict & helper
def update_body_from_dict(self, d):
    """
    Update the *old* p.b from d.

    Add 'ins_row/col', 'sel_row/col/1/2' keys.
    """
    tag = 'py.app.update_body_from_dict'
    p, v = self.c.p, self.c.p.v
    assert v, g.callers()
    #
    # Check d[old_ap], if it exists.
    old_p = self.ap_to_p(d['old_ap'])
    if p != old_p:
        print('%30s: MISMATCH: %s ==> %s' % (tag, old_p.h, p.h))
        return
    if 'select' in g.app.debug:
        tag = 'py.app.update_body_from_dict'
        d_s = d['s']
        print('%30s: %4s %s %s' % (tag, ' ', p.gnx, p.h))
        print('%30s: p.b: %s d.s: %s' % (tag, len(p.b), len(d_s)))
        # self.dump_dict(d, tag)
    self.update_body_from_dict_helper(d)
# @+node:ekr.20190511094352.1: *8* app.update_body_from_dict_helper
def update_body_from_dict_helper(self, d):
    """Update the body dict, without checks."""
    c, v = self.c, self.c.p.v
    d_s = d['s']
    #
    # Compute the insert point and selection range.
    col, row = d['ins_col'], d['ins_row']
    ins = g.convertRowColToPythonIndex(d_s, row, col)
    col1, row1 = d['sel_col1'], d['sel_row1']
    col2, row2 = d['sel_col2'], d['sel_row2']
    sel1 = g.convertRowColToPythonIndex(d_s, row1, col1)
    sel2 = g.convertRowColToPythonIndex(d_s, row2, col2)
    sel = (sel1, sel2)
    #
    # Update v's ivars
    v.setBodyString(d_s)
    v.insertSpot = ins
    # #2020: The old code updated a non-existent attribute.
    v.selectionStart = ins
    v.selectionLength = abs(sel1 - sel2)
    #
    # Update the body wrapper's ivars (for minibuffer commands).
    if 0:
        # These don't seem to work properly.
        # Besides, we are about to change nodes.
        w = c.frame.body.wrapper
        w.ins, w.sel, w.s = ins, sel, d_s
# @+node:ekr.20181122132009.1: *6* app.Testing...
# @+node:ekr.20181111142921.1: *7* app.action: do_command & helpers
@flx.action
def do_command(self, command, key, mods):
    c = self.c
    w = self.main_window
    #
    # Intercept in-progress commands.
    h1 = 'Set headline: '
    if command.startswith(h1):
        self.end_set_headline(command[len(h1) :])
        return
    h2 = 'Find: '
    if command.startswith(h2):
        self.end_find(command[len(h2) :])
        return
    func = getattr(self, 'do_' + command, None)
    if func:
        func()
        return
    #
    # Pass all other commands to Leo.
    self.execute_minibuffer_command(command, key, mods)
    c.k.keyboardQuit()
    w.body.set_focus()
# @+node:ekr.20181210054910.1: *8* app.do_cls
def do_cls(self):
    c = self.c
    w = self.root.main_window
    g.cls()
    c.k.keyboardQuit()
    w.body.set_focus()
# @+node:ekr.20181210055704.1: *8* app.do_find & helpers
def do_find(self):
    c = self.c
    event = leoGui.LeoKeyEvent(c,
        binding='',
        char='',
        event={'c': c},
        w=c.frame.miniBufferWidget,
    )
    c.interactive(
        callback=self.terminate_do_find,
        event=event,
        prompts=['Find: ',],
    )

def terminate_do_find(self):
    """Never called."""
# @+node:ekr.20181210092900.1: *9* app.end_find
def end_find(self, pattern):
    c = self.c
    fc = c.findCommands

    class DummyFTM:
        def get_find_text(self):
            return pattern
        def get_change_text(self):
            return ''
        def get_settings(self):
            return g.Bunch(
                # Find/change strings...
                find_text=pattern,
                change_text='',
                # Find options...
                file_only=False,
                ignore_case=True,
                mark_changes=False,
                mark_finds=False,
                node_only=False,
                pattern_match=False,
                search_body=True,
                search_headline=True,
                suboutline_only=False,
                whole_word=True,
            )

    # Init the search.
    fc.ftm = DummyFTM()
    # Do the search.
    fc.find_next()
    if 1:  # Testing only?
        w = self.root.main_window
        c.k.keyboardQuit()
        c.redraw()
        w.body.set_focus()
# @+node:ekr.20181119103144.1: *8* app.do_focus
def do_focus(self):
    c = self.c
    old_debug = g.app.debug
    try:
        g.app.debug = ['focus', 'keys',]
        print('\ncalling c.set_focus(c.frame.miniBufferWidget.widget')
        c.set_focus(c.frame.miniBufferWidget.widget)
    finally:
        g.app.debug = old_debug



# @+node:ekr.20181210055648.1: *8* app.do_head & helpers
def do_head(self):
    c = self.c
    event = leoGui.LeoKeyEvent(c,
        binding='',
        char='',
        event={'c': c},
        w=c.frame.miniBufferWidget,
    )
    c.interactive(
        callback=self.terminate_do_head,
        event=event,
        prompts=['Set headline: ',],
    )

def terminate_do_head(self, args, c, event):
    """never actually called."""
# @+node:ekr.20181210092817.1: *9* app.end_set_headline (leoflexx.py)
def end_set_headline(self, h):
    c, k, p, u = self.c, self.c.k, self.c.p, self.c.undoer
    w = self.root.main_window
    # Undoably set the head. Like leoTree.onHeadChanged.
    p.v.setHeadString(h)
    undoType = 'set-headline'
    undoData = u.beforeChangeNodeContents(p)
    if not c.changed:
        c.setChanged()
    p.setDirty()
    u.afterChangeNodeContents(p, undoType, undoData)
    k.keyboardQuit()
    c.redraw()
    w.body.set_focus()
# @+node:ekr.20181210054631.1: *8* app.do_redraw
def do_redraw(self):
    print('testing redraw...')
    self.redraw(None)
# @+node:ekr.20181210054631.2: *8* app.do_select
def do_select(self):
    print('testing select...')
    c = self.c
    h = 'Active Unit Tests'
    p = g.findTopLevelNode(c, h, exact=True)
    if p:
        c.frame.tree.select(p)
        # LeoBrowserTree.select.
    else:
        print('app.do_select: not found: %s' % h)
# @+node:ekr.20181210054516.1: *8* app.do_test
def do_test(self):
    c = self.c
    w = self.root.main_window
    print('testing positions...')
    self.test_round_trip_positions()
    self.do_select()
    c.k.keyboardQuit()
    c.redraw()
    w.body.set_focus()

# @+node:ekr.20181126104843.1: *7* app.test_full_outline
def test_full_outline(self, p):
    """Exercise the new diff-based redraw code on a fully-expanded outline."""
    c = self.c
    p = p.copy()
    # Don't call c.expandAllHeadlines: it calls c.redraw.
    for p2 in c.all_positions(copy=False):
        p2.expand()
    # Test the code.
    self.make_redraw_dict(p)  # Call this only for timing stats.
    # Restore the tree.
    for p2 in c.all_positions(copy=False):
        p2.contract()
    c.expandAllAncestors(p)  # Does not do a redraw.
# @+node:ekr.20181113180246.1: *7* app.test_round_trip_positions
def test_round_trip_positions(self):
    """Test the round tripping of p_to_ap and ap_to_p."""
    c = self.c
    # Bug fix: p_to_ap updates app.gnx_to_vnode. Save and restore it.
    old_d = self.gnx_to_vnode.copy()
    old_len = len(list(self.gnx_to_vnode.keys()))
    # t1 = time.process_time()
    for p in c.all_positions():
        ap = self.p_to_ap(p)
        p2 = self.ap_to_p(ap)
        assert p == p2, (repr(p), repr(p2), repr(ap))
    self.gnx_to_vnode = old_d
    new_len = len(list(self.gnx_to_vnode.keys()))
    assert old_len == new_len, (old_len, new_len)
    # print('app.test_round_trip_positions: %5.3f sec' % (time.process_time()-t1))
# @+node:ekr.20181115092337.3: *5* class LeoBrowserBody
class LeoBrowserBody(leoFrame.NullBody):

    def __init__(self, frame):
        super().__init__(frame, parentFrame=None)
        self.c = c = frame.c
        self.root = get_root()
        self.tag = 'py.body.wrap'
        # Replace the StringTextWrapper with the ace wrapper.
        assert isinstance(self.wrapper, StringTextWrapper)
        self.wrapper = API_Wrapper(c, 'body')

    # The Ace Wrapper does almost everything.
    def __getattr__(self, attr):
        if self.root.inited:
            return getattr(self.wrapper, attr)
        raise AttributeError('app not inited')

    def getName(self):
        return 'body'  # Required for proper pane bindings.

# @+node:ekr.20181115092337.6: *5* class LeoBrowserFrame
class LeoBrowserFrame(leoFrame.NullFrame):

    def __init__(self, c, title, gui):
        """Ctor for the LeoBrowserFrame class."""
        super().__init__(c, title, gui)
        assert self.c == c
        frame = self
        self.root = get_root()
        self.tag = 'py.frame.wrap'
        #
        # Instantiate the other wrappers.
        self.body = LeoBrowserBody(frame)
        self.tree = LeoBrowserTree(frame)
        self.log = LeoBrowserLog(frame)
        self.menu = LeoBrowserMenu(frame)
        self.miniBufferWidget = LeoBrowserMinibuffer(c, frame)
        self.iconBar = LeoBrowserIconBar(c, frame)
        # NullFrame does this in createStatusLine.
        self.statusLine = LeoBrowserStatusLine(c, frame)
        # This is the DynamicWindow object.
        # There is no need to implement its methods now.
        self.top = g.NullObject()

    def finishCreate(self):
        """Override NullFrame.finishCreate."""
        # Do not call self.createFirstTreeNode.

    @others
# @+node:ekr.20181113041113.1: *5* class LeoBrowserGui
class LeoBrowserGui(leoGui.NullGui):

    def __init__(self, gui_name='browser'):
        # leoTest.doTest special-cases the name "browser".
        super().__init__(guiName='browser')
        self.gui_name = gui_name  # May specify the actual browser.
        assert gui_name.startswith('browser')
        self.logWaiting = []
        self.root = None  # Will be set later.
        self.silentMode = True  # Don't print a single signon line.
        self.tag = 'py.gui'
        self.specific_browser = gui_name.lstrip('browser').lstrip(':').lstrip('-').strip()
        if not self.specific_browser:
            self.specific_browser = 'browser'
        self.consoleOnly = False  # Console is separate from the log.
        #
        # Monkey-patch g.app.writeWaitingLog to be a do-nothing.
        # This allows us to write the log much later.
        g.app.writeWaitingLog = self.writeWaitingLog1

    def insertKeyEvent(self, event, i):
        """Insert the key given by event in location i of widget event.w."""
        # Mysterious...
        assert False, g.callers()

    @others
# @+node:ekr.20181206153033.1: *6* gui.createLeoFrame
def createLeoFrame(self, c, title):
    """
    Override NullGui.createLeoFrame.

    We can't create a real flx.Frame until much later.

    We create a placeholder in g.app.windowList, for app.finish_create.
    """
    gui = self
    self.lastFrame = DummyFrame(c, title, gui)
    # A buglet in Leo's core: this is necessary.
    # LM.doPostPluginsInit tests g.app.windowList, maybe when it shouldn't.
    g.app.windowList.append(self.lastFrame)
    return self.lastFrame
# @+node:ekr.20181119141542.1: *6* gui.isTextWrapper
def isTextWrapper(self, w):
    """Return True if w is supposedly a text widget."""
    # isinstance is much more pythonic than using getName.
    if isinstance(w, (
        LeoBrowserBody,
        LeoBrowserLog,
        LeoBrowserMinibuffer,
        # LeoFlexxTreeItem,
            # At present, Leo's core can not edit tree items.
        LeoBrowserStatusLine,
    )):
        return True
    if g.unitTesting and isinstance(w, StringTextWrapper):
        return True
    return False
# @+node:ekr.20181119153936.1: *6* gui.focus...
def get_focus(self, *args, **kwargs):
    w = self.focusWidget
    # print('%s: get_focus: %r' % (self.tag, w.tag))
    return w

def set_focus(self, commander, widget):
    # Be careful during startup.
    if g.unitTesting:
        return
    if not self.root:
        return
    c = self.root.c
    if isinstance(widget, (
        LeoBrowserBody,
        LeoBrowserFrame,
        LeoBrowserLog,
        LeoBrowserMinibuffer,
        LeoBrowserTree,
    )):
        if 'focus' in g.app.debug:
            tag = 'gui.set_focus'
            print('%30s: %s' % (tag, widget.tag))
        widget.setFocus()
        self.focusWidget = widget
    elif isinstance(widget, API_Wrapper):
        # This does sometimes get executed.
        # print('gui.set_focus: redirect AceWrapper to LeoBrowserBody')
        assert isinstance(c.frame.body, LeoBrowserBody), repr(c.frame.body)
        assert widget.name == 'body', repr(widget.name)
        c.frame.body.wrapper.setFocus()
    else:
        print('gui.set_focus: unknown widget', repr(widget), g.callers(6))
# @+node:ekr.20181206090210.1: *6* gui.writeWaitingLog1/2
def writeWaitingLog1(self, c=None):
    """Monkey-patched do-nothing version of g.app.writeWaitingLog."""

def writeWaitingLog2(self, c=None):
    """Called from app.finish_create."""
    w = self.root.main_window
    #
    # Print the signon.
    table = [
        ('Leo Log Window', 'red'),
        ('flexx version: %s' % (flx.__version__), None),
        (g.app.signon, None),
        (g.app.signon1, None),
        (g.app.signon2, None),
    ]
    for message, color in table:
        if message.strip():
            w.log.put(message.rstrip())
    #
    # Write all the queued log entries.
        # c.setLog()
    g.app.logInited = True  # Prevent recursive call.
    for msg in g.app.logWaiting:
        s, color, newline = msg[:3]  # May have 4 elements.
        w.log.put(s.rstrip())
    g.app.logWaiting = []
    g.app.setLog(None)  # Essential when opening multiple files...
# @+node:ekr.20181202083305.1: *6* gui.runMainLoop
def runMainLoop(self):
    """Run the main loop from within Leo's core."""
    runtime = self.specific_browser or 'webruntime'
    flx.launch(LeoBrowserApp, runtime)
    flx.set_log_level('ERROR')  #  'INFO'
    flx.run()
# @+node:ekr.20181115092337.21: *5* class LeoBrowserIconBar
class LeoBrowserIconBar(leoFrame.NullIconBarClass):

    def __init__(self, c, parentFrame):
        super().__init__(c, parentFrame)
        assert self.c == c
        self.root = get_root()
        self.tag = 'py.icon bar.wrap'

    @others
# @+node:ekr.20181115092337.22: *5* class LeoBrowserLog
class LeoBrowserLog(leoFrame.NullLog):

    def __init__(self, frame, parentFrame=None):
        super().__init__(frame, parentFrame)
        self.root = get_root()
        self.tag = 'py.log.wrap'
        assert isinstance(self.widget, StringTextWrapper)
        self.logCtrl = self.widget
        self.logWidget = self.widget
        self.wrapper = self.widget
        #
        # Monkey-patch self.wrapper, a StringTextWrapper.
        assert self.wrapper.getName().startswith('log')
        self.wrapper.setFocus = self.setFocus

    def __repr__(self):
        return 'LeoBrowserLog'

    __str__ = __repr__

    def getName(self):
        return 'log'  # Required for proper pane bindings.

    @others
# @+node:ekr.20181120063043.1: *6* log_wrapper.setFocus
def setFocus(self):
    w = self.root.main_window
    w.log.set_focus()
# @+node:ekr.20181120063111.1: *6* log_wrapper.put & putnl
def put(self, s, color=None, tabName='Log', from_redirect=False, nodeLink=None):
    self.root.main_window.log.put(s)

def putnl(self, tabName='Log'):
    self.root.main_window.log.put('')
# @+node:ekr.20181115092337.31: *5* class LeoBrowserMenu
class LeoBrowserMenu(leoMenu.NullMenu):
    """Browser wrapper for menus."""

    # def __init__(self, frame):
        # super().__init__(frame)
        # self.root = get_root()
        # self.tag = '(py.menu.wrap)'

    # @verbatim
    # @others
# @+node:ekr.20181115120317.1: *5* class LeoBrowserMinibuffer (StringTextWrapper)
# Leo's core doesn't define a NullMinibuffer class.

class LeoBrowserMinibuffer(StringTextWrapper):
    """Browser wrapper for minibuffer."""

    def __init__(self, c, frame):
        # Name must be minibuffer, for gui.isTextWrapper().
        super().__init__(c, name='minibuffer')
        assert self.c == c, repr(self.c)
        # assert c.frame == frame, (repr(c.frame), repr(frame))
            # c.frame is a NullFrame.  frame is a LeoBrowserFrame.
        assert self.widget is None, repr(self.widget)
        assert self.getName() == 'minibuffer'
        self.frame = frame
        self.root = get_root()
        self.tag = 'py.mini.wrap'
        self.widget = self
        self.wrapper = self
        # Hook this class up to the key handler.
        c.k.w = self
        c.miniBufferWidget = self  # #1146.
        frame.minibufferWidget = self

    # Overrides.
    def setFocus(self):
        self.root.main_window.minibuffer.set_focus()

    @others
# @+node:ekr.20181208062449.1: *6* mini.called by k.minibuffer
# Override the methods called by k.minibuffer:

last_ins = None
last_sel = None
last_s = None

def update(self, tag):
    w = self.root.main_window
    i, j = self.sel
    # Do nothing if there will be no changes.
    if self.s == self.last_s and self.last_ins == self.ins and self.sel == self.last_sel:
        return
    # Remember the present values.
    self.last_ins, self.last_s, self.last_sel = self.ins, self.s, self.sel
    if 'select' in g.app.debug:
        tag = 'py.mini.update'
        print('%30s: sel: %3s %3s ins: %3s len(s): %s' % (
            tag, i, j, self.ins, len(self.s)))
    w.minibuffer.set_text(self.s)
    w.minibuffer.set_selection(i, j)
    w.minibuffer.set_insert(self.ins)

def delete(self, i: int, j: Optional[int] = None):
    super().delete(i, j)
    self.update('delete')

def getAllText(self):
    return self.s

def insert(self, i: int, s: str) -> None:
    super().insert(i, s)
    self.update('insert')

def setAllText(self, s):
    super().setAllText(s)
    self.update('setAllText')

def setSelectionRange(self, i: int, j: int, insert: Optional[int] = None):
    super().setSelectionRange(i, j, insert)
    self.update('setSelectionRange')

def setStyleClass(self, name):
    w = self.root.main_window
    if not w:
        g.trace('NO MAIN WINDOW')
        return
    # if 'focus' in g.app.debug:
        # tag = 'py.mini.setStyleClass'
        # print('%30s: %r' % (tag, name))
    w.minibuffer.set_style(name)
    self.update('setStyleClass:%r' % name)
# @+node:ekr.20181115092337.32: *5* class LeoBrowserStatusLine
class LeoBrowserStatusLine(leoFrame.NullStatusLineClass):

    def __init__(self, c, parentFrame):
        super().__init__(c, parentFrame)
        self.root = get_root()
        self.tag = 'py.status.wrap'
        self.w = self  # Required.

    def getName(self):
        return 'status line'  # Value not important.

    # Pretend that this widget supports the high-level interface.
    def __getattr__(self, attr):
        return g.NullObject()

    @others
# @+node:ekr.20181119045430.1: *6* status_line_wrapper.clear & get
def clear(self):
    pass

def get(self):
    print('status_line.get: NOT READY')
    return ''
# @+node:ekr.20181119045343.1: *6* status_line_wrapper.put and put1
def put(self, s, bg=None, fg=None):
    w = self.root.main_window
    # Be careful during startup.
    if w and w.status_line:
        w.status_line.put(s, bg, fg)

def put1(self, s, bg=None, fg=None):
    w = self.root.main_window
    # Be careful during startup.
    if w and w.status_line:
        w.status_line.put2(s, bg, fg)
# @+node:ekr.20181119154422.1: *6* status_line_wrapper.setFocus
def setFocus(self):
    self.root.status_line.set_focus()
# @+node:ekr.20181119042937.1: *6* status_line_wrapper.update
def update(self, body_text='', insert_point=0):
    """
    Update the status line, based on the contents of the body.

    Called from LeoTree.select.

    Returns (lt_part, rt_part) for LeoBrowserApp.init.
    """
    # pylint: disable=arguments-differ
    if g.app.killed:
        return None
    c, p = self.c, self.c.p
    if not p:
        return None
    # Calculate lt_part
    row, col = g.convertPythonIndexToRowCol(body_text, insert_point)
    fcol = c.gotoCommands.find_node_start(p)
    lt_part = "line: %2d col: %2d fcol: %s" % (row, col, fcol or '')
    # Calculate rt_part.
    headlines = [v.h for (v, childIndex) in p.stack] + [p.h]
    rt_part = '%s#%s' % (g.shortFileName(c.fileName()), '->'.join(headlines))
    # Update the status area.
    self.put(lt_part)
    self.put1(rt_part)
    return lt_part, rt_part
# @+node:ekr.20181115092337.57: *5* class LeoBrowserTree
class LeoBrowserTree(leoFrame.NullTree):

    def __init__(self, frame):
        super().__init__(frame)
        self.root = get_root()
        self.select_lockout = False
        self.tag = 'py.tree.wrap'
        self.widget = self
        self.wrapper = self

    def getName(self):
        return 'canvas(tree)'  # Required for proper pane bindings.

    @others
# @+node:ekr.20181116081421.1: *6* tree.selection...


# @+node:ekr.20190508121417.1: *7* tree.complete_select
def complete_select(self, d):
    """Complete the selection of the tree."""
    trace = 'select' in g.app.debug
    tag = 'py.tree.complete_select'
    if not self.new_p:
        self.select_lockout = False
        print('%30s: can not happen: no new_p')
        return
    p = self.root.ap_to_p(d['new_ap'])
    if p != self.new_p:
        print('%30s: expected: %s, got: %s' % (tag, self.new_p.h, p.h))
        return
    if trace:
        print('%30s: %4s %s %s' % (tag, len(p.b), p.gnx, p.h))
    #
    # Allow a new select.
    self.select_lockout = False
    #
    # Make everything official in Leo's core.
    super().select(p)  # Call LeoTree.select.
    self.root.select_p(p)  # Call app.select_position.
# @+node:ekr.20190508121510.1: *7* tree.endEditLabel
def endEditLabel(self):
    """
    End editing.

    This must be a do-nothing, because app.end_set_headline takes its place.
    """
    # print(flx.tree.endEditLabel')
# @+node:ekr.20190508121414.1: *7*  tree.select
# The lockout ensures that old_p never changes during the selection process.
select_lockout = False
old_p = None
new_p = None

def select(self, p):
    """
    Override LeoTree.select.

    Operations across the Python/JS divide do not happen immediately. As a result,
    the selection must happen in phases:

    Phase 1a: PY side:
    - Remember the old and new p.
    - Schedule sync_body_before_select to update of the old p.b on the JS side.
    - Set the lockout, so only one JS update is ever done.

    Phase 1b: PY side:
    - If the lockout is set, just update the *new* p.

    Phase 2: JS side:
    - Update the *old* p.b and related vnode ivars from the body pane.
      Important: The lockout never changes the *old* p.
    - Call flx.tree.complete_select to update the widget.
    - Schedule app.complete_select.

    Phase 3: PY side:
    """
    trace = 'select' in g.app.debug
    tag = 'py.tree.select'
    w = self.root.main_window
    if self.select_lockout:
        self.new_p = p.copy()
        if trace:
            print('%30s: %s %s' % (tag, ' Lockout', p.h))
        return
    if not self.root.inited:
        # Don't sync the body pane during startup.
        super().select(p)  # Call LeoTree.select
        self.root.select_p(p)  # Call app.select_position.
        return
    #
    # Begin the selection.
    self.select_lockout = True
    self.new_p = p.copy()
    old_p = self.root.c.p
    if trace:
        print('\n%30s: %4s %s ==> %s %s ' % (
            tag, len(old_p.v._bodyString), old_p.h, len(p.v._bodyString), p.h))
    #
    # Schdule JS side to:
    # 1. Update *old* p.b from flx.body.
    #    The lockout ensures that the old p never changes.
    # 2. Schedule self.complete_select.
    w.body.sync_body_before_select({
        'old_ap': self.root.p_to_ap(old_p),
        'new_ap': self.root.p_to_ap(p),
    })
# @+node:ekr.20190508121417.2: *7* tree.select_ap
def select_ap(self, ap):
    trace = 'select' in g.app.debug
    p = self.root.ap_to_p(ap)
    if trace:
        tag = 'py.tree.select_ap'
        print('%30s: %s %s' % (tag, p.v.fileIndex, p.v._headString))
    self.select(p)
# @+node:ekr.20190508121417.3: *7* tree.super_select
def super_select(self, p):
    """Call only LeoTree.select."""
    trace = 'select' in g.app.debug
    if trace:
        tag = 'py.tree.super_select'
        print('%30s %4s %s %s' % (tag, len(p.b), p.gnx, p.h))
    super().select(p)

# @+node:ekr.20181118052203.1: *6* tree.redraw
def redraw(self, p=None):
    """This is c.frame.tree.redraw!"""
    self.root.redraw(p)
# @+node:ekr.20181120063844.1: *6* tree.setFocus
def setFocus(self):
    w = self.root.main_window
    w.tree.set_focus()
# @+node:ekr.20181119094122.1: *5* class TracingNullObject (leoflexx.py)
@nobeautify

class TracingNullObject:
    """A tracing version of g.NullObject."""
    def __init__(self, *args, **keys):
        pass

    def __call__(self, *args, **keys):
        return self

    def __repr__(self):
        return "NullObject"

    def __str__(self):
        return "NullObject"

    def __bool__(self):
        return False

    def __delattr__(self, attr):
        print('NullObject.__delattr__ %r %s' % (attr, g.callers()))
        return self

    def __getattr__(self, attr):
        print('NullObject.__getattr__ %r %s' % (attr, g.callers()))
        return self

    def __setattr__(self, attr, val):
        print('NullObject.__setattr__ %r %s' % (attr, g.callers()))
        return self
# @+node:ekr.20181107052700.1: *4* Js side: flx.Widgets
# @+node:ekr.20181201125953.1: *5* class JS_Editor (flx.Widget)
class JS_Editor(flx.Widget):
    """
    The base class for the body and log panes.
    """

    def init(self, name, flex=1):
        # pylint: disable=arguments-differ
        assert name in ('body', 'log', 'minibuffer'), repr(name)
        self.name = name
        self.tag = 'flx.%s' % name
        self.editor = None  # Now done in subclasses.

    @flx.reaction('size')
    def __on_size(self, *events):
        self.editor.resize()

    @others
# @+node:ekr.20181121072246.1: *6* jse.Keys
# @+node:ekr.20181215083729.1: *7* jse.key_press & on_key_press
last_down = None
ignore_up = False

@flx.emitter  # New
def key_down(self, e):
    trace = False and 'keys' in g.app.debug
    self.ignore_up = False
    ev = self._create_key_event(e)
    down = '%s %r' % (self.name, ev)
    if down != self.last_down:
        self.last_down = down
        if trace:
            print('     jse.key_down:', down)
    if self.should_be_leo_key(ev):
        e.preventDefault()
    return ev

@flx.emitter  # New
def key_up(self, e):
    trace = False and 'keys' in g.app.debug
    self.last_down = None  # Enable key downs.
    ev = self._create_key_event(e)
    if self.ignore_up:
        if trace:
            print('IGNORE jse.key_up: %s %r' % (self.name, ev))
        e.preventDefault()
        return ev
    self.ignore_up = True  # Ignore all further key ups, until the next key down.
    should_be_leo = bool(self.should_be_leo_key(ev))
    if trace:
        print('       jse.key_up: %s %r Leo: %s' % (self.name, ev, should_be_leo))
    if should_be_leo:
        e.preventDefault()
        ivar = 'minibufferWidget' if self.name == 'minibuffer' else self.name
        self.root.do_key(ev, ivar)
    return ev

@flx.emitter
def key_press(self, e):
    trace = 'keys' in g.app.debug
    ev = self._create_key_event(e)
    # Init the key_down state.
    self.last_down = None
    # Ignore all key ups until the next key down.
    self.ignore_up = True
    if trace:
        print('    jse.key_press: %s %r' % (self.name, ev))
    if self.should_be_leo_key(ev):
        e.preventDefault()
    return ev

@flx.reaction('key_press')
def on_key_press(self, *events):
    # The JS editor has already** handled the key!
    for ev in events:
        if 'keys' in g.app.debug:
            print(' jse.on_key_press: %s %r' % (self.name, ev))
        if self.should_be_leo_key(ev):
            ivar = 'minibufferWidget' if self.name == 'minibuffer' else self.name
            self.root.do_key(ev, ivar)
# @+node:ekr.20181201081444.1: *7* jse.should_be_leo_key
def should_be_leo_key(self, ev):
    """
    Return True if Leo should handle the key.
    Leo handles only modified keys, not F-keys or plain keys.
    """
    trace = False and 'keys' in g.app.debug
    tag = 'JSE.should_be_leo_key'
    key, mods = ev['key'], ev['modifiers']
    #
    # The widget handles F-keys.
    if not mods and key.startswith('F'):
        if trace:
            print('%s: %r %r return: false' % (tag, mods, key))
        return False
    mods2 = mods
    if 'Shift' in mods2:
        mods2.remove('Shift')
    #
    # Send only Ctrl-Arrow keys to body.
    if mods2 == ['Ctrl'] and key in ('RtArrow', 'LtArrow', 'UpArrow', 'DownArrow'):
        # This never fires: Neither editor widget emis Ctrl/Arrow keys or Alt-Arrow keys.
        if trace:
            print(tag, 'Arrow key: send to to body', repr(mods), repr(key))
        return False
    #
    # Leo should handle all other modified keys.
    if trace:
        print('%s: %r %r return: %s' % (tag, mods, key, bool(mods2)))
    return mods2
# @+node:ekr.20181215083642.1: *6* jse.focus
@flx.action
def see_insert_point(self):
    if 'focus' in g.app.debug:
        tag = '%s.%s' % (self.tag, 'jse.see_insert_point')
        print('%30s:' % tag)

@flx.action
def set_focus(self):
    if 'focus' in g.app.debug:
        tag = '%s.%s' % (self.tag, 'jse.set_focus')
        print('%30s:' % tag)
    self.editor.focus()
# @+node:ekr.20181215061810.1: *6* jse.text getters
def get_ins(self):
    d = self.editor.selection.getCursor()
    row, col = d['row'], d['column']
    # print('%s: get_ins: row: %s col: %s' % (self.tag, row, col))
    return row, col

def get_sel(self):
    selections = self.editor.selection.getAllRanges()
    if selections:
        sel = selections[0]
        d1, d2 = sel['start'], sel['end']
        row1, col1 = d1['row'], d1['column']
        row2, col2 = d2['row'], d2['column']
    else:
        print('%s: get_sel: NO SELECTION' % self.tag)
        i = self.get_ins()
        row1 = col1 = row2 = col2 = i
    # print('%s: get_sel: (%s, %s) to (%s, %s)' % (self.tag, row1, col1, row2, col2))
    return row1, col1, row2, col2

def get_text(self):
    editor = self.editor
    s = editor.getValue()
    # print('%s: get_text: %s' % (self.tag, len(s)))
    return s
# @+node:ekr.20181128061524.1: *6* jse.text setters
@flx.action
def insert(self, s):
    # print(self.tag, 'insert', repr(s))
    self.editor.insert(s)

@flx.action
def select_all_text(self):
    if 'select' in g.app.debug:
        print(self.tag, 'select_all_text')

@flx.action
def set_insert_point(self, insert, sel):
    s = self.editor.getValue()
    row, col = g.convertPythonIndexToRowCol(s, insert)
    row = max(0, row - 1)
    if 'select' in g.app.debug:
        tag = '%s.%s' % (self.tag, 'set_insert_point')
        print('%30s: i: %s = (%s, %s)' % (
            tag, str(insert).ljust(3), row, col))
    self.editor.moveCursorTo(row, col)

@flx.action
def set_selection_range(self, i, j):
    print(self.tag, 'set_selection_range: NOT READY', i, j)

@flx.action
def set_text(self, s):
    """Set the entire text"""
    # print('%s.set_text: len(s): %s' % (self.tag, len(s)))
    self.editor.setValue(s)
# @+node:ekr.20181104082144.1: *5* class LeoFlexxBody (JS_Editor)
class LeoFlexxBody(JS_Editor):
    """A CodeEditor widget based on Ace."""

    << body css >>

    def init(self):
        # pylint: disable=arguments-differ
        super().init('body')
        self.editor = make_editor_function(self.name, self.node)

    @others
# @+node:ekr.20181120055046.1: *6* << body css >>
CSS = """
.flx-CodeEditor > .ace {
    width: 100%;
    height: 100%;
}
"""
# @+node:ekr.20190512094614.1: *6* flx_body.action.set_focus
@flx.action
def set_focus(self):
    if 'focus' in g.app.debug:
        tag = 'flx.body.set_focus'
        print('%30s:' % tag)
    self.editor.focus()
# @+node:ekr.20181215061402.1: *6* flx_body.action.sync*


# @+node:ekr.20190511092226.1: *7* flx.body.action.sync_body_before_save_file
@flx.action
def sync_body_before_save_file(self, d):
    """Update p.b, etc. before executing calling c.fileCommands.save."""
    self.update_body_dict(d)
    if 'select' in g.app.debug:
        tag = 'flx.body.sync_body_before_save_file'
        print('%30s: %r' % (tag, d['s']))
    self.root.complete_save_file(d)
# @+node:ekr.20190511102428.1: *7* flx.body.action.sync_body_before_save_file_as
@flx.action
def sync_body_before_save_file_as(self, d):
    """Update p.b, etc. before executing calling c.fileCommands.saveAs."""
    self.update_body_dict(d)
    if 'select' in g.app.debug:
        tag = 'flx.body.sync_body_before_save_file_as'
        print('%30s: %r' % (tag, d['s']))
    self.root.complete_save_file_as(d)
# @+node:ekr.20190511102429.1: *7* flx.body.action.sync_body_before_save_file_to
@flx.action
def sync_body_before_save_file_to(self, d):
    """Update p.b, etc. before executing calling c.fileCommands.saveTo."""
    self.update_body_dict(d)
    if 'select' in g.app.debug:
        tag = 'flx.body.sync_body_before_save_file_to'
        print('%30s: %r' % (tag, d['s']))
    self.root.complete_save_file_to(d)
# @+node:ekr.20190510070009.1: *7* flx.body.action.sync_body_before_select
@flx.action
def sync_body_before_select(self, d):
    """
    Called by app.tree.select to update the *old* p.b, etc. before selecting a new node.

    d['old_ap']: The AP for the old node.
    d['new_ap']: The AP for the new node.
    """
    if 'select' in g.app.debug:
        tag = 'flx.body.sync_body_before_select'
        print('')
        print('%30s: %s ==> %s' % (
            tag, d['old_ap']['headline'], d['old_ap']['headline']))
        # self.root.dump_dict(d, tag)
    # Sets d.s, etc., describing insert point & selection range.
    self.update_body_dict(d)
    self.root.complete_select(d)

# @+node:ekr.20190510070010.1: *6* flx.body.update_body_dict
def update_body_dict(self, d):
    """
    Add keys to d describing flx.body.

    This sets d.s & other keys from the *old* position.
    """
    #
    # Remember the body text.
    d['s'] = self.get_text()
    d['ins_row'], d['ins_col'] = self.get_ins()
    #
    # Remember the selection ranges.
    row1, col1, row2, col2 = self.get_sel()
    d['sel_row1'], d['sel_col1'] = row1, col1
    d['sel_row2'], d['sel_col2'] = row2, col2
    if 'select' in g.app.debug:
        d_s = d['s']
        tag = 'flx.body.update_body_dict'
        print('%30s: len(d_s): %4s' % (tag, len(d_s)))
        if 0:
            print('')
            for z in d_s.split('\n'):
                print(repr(z))
            print('')
# @+node:ekr.20181104082149.1: *5* class LeoFlexxLog (JS_Editor)
class LeoFlexxLog(JS_Editor):

    << log css >>

    def init(self):
        # pylint: disable=arguments-differ
        super().init('log')
        self.editor = make_editor_function(self.name, self.node)

    @others
# @+node:ekr.20181120060336.1: *6* << log css >>
CSS = """
.flx-CodeEditor > .ace {
    width: 100%;
    height: 100%;
}
"""
# @+node:ekr.20181120060348.1: *6* flx.log.put & set_focus
@flx.action
def put(self, s):
    prev = self.editor.getValue()
    if prev:
        self.editor.setValue(prev + '\n' + s)
    else:
        self.editor.setValue(s)

@flx.action
def set_focus(self):
    if 'focus' in g.app.debug:
        tag = 'flx.log.set_focus'
        print('%30s:' % tag)
    self.editor.focus()
# @+node:ekr.20181104082130.1: *5* class LeoFlexxMainWindow
class LeoFlexxMainWindow(flx.Widget):

    """
    Leo's main window, that is, root.main_window.

    Each property is accessible as root.main_window.x.
    """
    body = flx.ComponentProp(settable=True)
    log = flx.ComponentProp(settable=True)
    minibuffer = flx.ComponentProp(settable=True)
    status_line = flx.ComponentProp(settable=True)
    tree = flx.ComponentProp(settable=True)

    def init(self):
        self.tag = 'flx.main window'
        with flx.VSplit():
            with flx.HSplit(flex=1):
                tree = LeoFlexxTree(flex=1)
                log = LeoFlexxLog(flex=1)
            body = LeoFlexxBody(flex=1)
            minibuffer = LeoFlexxMiniBuffer()
            status_line = LeoFlexxStatusLine()
        << define unload action >>
        self._mutate('body', body)
        self._mutate('log', log)
        self._mutate('minibuffer', minibuffer)
        self._mutate('status_line', status_line)
        self._mutate('tree', tree)
        self.emit('do_init')

    @flx.reaction('!do_init')
    def after_init(self):
        self.root.finish_create()
# @+node:ekr.20181206044554.1: *6* << define unload action >>
RawJS("""\
// Called from Mozilla, but not webruntime.
window.onbeforeunload = function(){
    // console.log("window.onbeforeunload")
    return "Are you sure?"
};
""")
# @+node:ekr.20181104082154.1: *5* class LeoFlexxMiniBuffer (JS_Editor)
class MinibufferEditor(flx.Widget):

    def init(self):
        # Unlike in components, this call happens immediately.
        self.editor = make_editor_function('minibuffer', self.node)

class LeoFlexxMiniBuffer(JS_Editor):

    def init(self):
        # pylint: disable=arguments-differ
        super().init('minibuffer')
        with flx.HBox():
            flx.Label(text='Minibuffer')
            w = MinibufferEditor(flex=1)
            self.editor = w.editor

    @flx.reaction('pointer_down')
    def on_select(self):
        self.root.select_minibuffer()

    @others
# @+node:ekr.20181127060810.1: *6* flx_minibuffer.high-level interface
# The high-level interface methods, called from LeoBrowserMinibuffer.

@flx.action
def set_focus(self):
    if 'focus' in g.app.debug:
        tag = 'flx.mini.set_focus'
        print('%30s:' % tag)
    self.editor.focus()

@flx.action
def set_insert(self, i):
    if False and 'select' in g.app.debug:
        print('flx.mini.set_insert', i)
    # Where is call?

@flx.action
def set_selection(self, i, j):
    if False and 'select' in g.app.debug:
        print('flx.mini.set_selection', i, j)
    # Where is the call?

@flx.action
def set_style(self, style):
    pass

@flx.action
def set_text(self, s):
    if 'focus' in g.app.debug:
        tag = 'flx.mini.set_text'
        print('%30s: %r' % (tag, s))
    self.editor.setValue(s)
# @+node:ekr.20181203150409.1: *6* flx_minibuffer.Key handling
@flx.emitter
def key_press(self, e):
    """Pass *all* keys except Enter and F12 to Leo's core."""
    # Backspace is not emitted.
    ev = self._create_key_event(e)
    key, mods = ev['key'], ev['modifiers']
    if 'keys' in g.app.debug:
        print('mini.key_press: %r %r' % (mods, key))
    if mods:
        e.preventDefault()
        return ev
    if key == 'F12':
        return ev
    if key == 'Enter':
        self.do_enter_key(key, mods)
        e.preventDefault()
        return None
    # k.masterKeyHandler will handle everything.
    e.preventDefault()
    return ev

@flx.reaction('key_press')
def on_key_press(self, *events):
    """Pass *all* keys Leo's core."""
    for ev in events:
        # print('mini.on_key_press: %r %r' % (ev ['modifiers'], ev['key']))
        self.root.do_key(ev, 'minibufferWidget')
# @+node:ekr.20181129174405.1: *6* flx_minibuffer.do_enter_key
def do_enter_key(self, key, mods):
    """
    Handle the enter key in the minibuffer.
    This will only be called if the user has entered the minibuffer via a click.
    """
    command = self.editor.getValue()
    if 'keys' in g.app.debug:
        print('mini.do_enter_key', repr(command))
    if command.strip():
        if command.startswith('full-command:'):
            command = command[len('full-command:') :].strip()
            self.root.do_command(command, 'Enter', [])
        elif command.startswith('Enter Headline:'):
            headline = command[len('Enter Headline:') :].strip()
            self.root.edit_headline_completer(headline)
        else:
            self.root.do_command(command, key, mods)
# @+node:ekr.20181104082201.1: *5* class LeoFlexxStatusLine
class LeoFlexxStatusLine(flx.Widget):

    def init(self):
        self.tag = 'flx.status'
        with flx.HBox():
            flx.Label(text='Status Line')
            self.widget = flx.LineEdit(flex=1)
            self.widget2 = flx.LineEdit(flex=1)
        self.widget.apply_style('background: green')
        self.widget2.apply_style('background: green')

    @others
# @+node:ekr.20181123043015.1: *6* flx.status_line.action.update
@flx.action
def update(self, lt, rt):
    self.put(lt)
    self.put2(rt)
# @+node:ekr.20181120060957.1: *6* flx_status_line.action.put & put2
@flx.action
def put(self, s, bg=None, fg=None):
    self.widget.set_text(s)

@flx.action
def put2(self, s, bg=None, fg=None):
    self.widget2.set_text(s)
# @+node:ekr.20181120060950.1: *6* flx_status_line.Key handling
@flx.emitter
def key_press(self, e):
    """Allow only F-keys, Ctrl-C and Ctrl-S."""
    ev = self._create_key_event(e)
    key, mods = ev['key'], ev['modifiers']
    print('flx.status_line:', repr(mods), repr(key))
    if not mods and key.startswith('F'):
        return ev
    if mods == ['Ctrl'] and key in 'cs':
        # We don't always get Ctrl-S from the browser.
        return ev
    # Prevent everything else.
    e.preventDefault()
    return ev

@flx.reaction('key_press')
def on_key_press(self, *events):
    """Pass Ctrl-S to Leo."""
    for ev in events:
        key, mods = ev['key'], ev['modifiers']
        if mods == ['Ctrl'] and key == 's':
            self.root.do_key(ev, 'statusLine')
        return ev
# @+node:ekr.20181104082138.1: *5* class LeoFlexxTree
class LeoFlexxTree(flx.Widget):

    << tree css >>

    def init(self):
        self.widget = self
        self.wrapper = self
        self.tag = 'flx.tree'
        # Init local ivars...
        self.populated_items_dict = {}  # Keys are ap **keys**, values are True.
        self.populating_tree_item = None  # The LeoTreeItem whose children are to be populated.
        self.selected_ap = {}  # The ap of the presently selected node.
        self.tree_items_dict = {}  # Keys are ap's. Values are LeoTreeItems.
        # Init the widget. The max_selected property does not seem to work.
        self.tree = flx.TreeWidget(flex=1, max_selected=1)

    def assert_exists(self, obj):
        # pylint: disable=undefined-variable
            # undefined
        global undefined
        assert obj not in (undefined, None, {}), repr(obj)

    @others
# @+node:ekr.20181120061118.1: *6*  << tree css >>

CSS = '''
.flx-TreeWidget {
    background: #000;
    color: white;
    /* background: #ffffec; */
    /* Leo Yellow */
    /* color: #afa; */
}
'''
# @+node:ekr.20181121073246.1: *6* flx_tree.Drawing...
# @+node:ekr.20181112163252.1: *7* flx_tree.action.clear_tree
@flx.action
def clear_tree(self):
    """
    Completely clear the tree, preparing to recreate it.

    Important: we do *not* clear self.tree itself!
    """
    trace = 'drawing' in g.app.debug
    tag = 'flx.tree.clear_tree'
    # pylint: disable=access-member-before-definition
    items = list(self.tree_items_dict.values())
    if trace:
        print('')
        print('%s: %s items' % (tag, len(items)))
    #
    # Clear *all* references first.
    self.tree_items_dict = {}
    self.populated_items_dict = {}
    self.populating_tree_item = None
    #
    # Clear all tree items.
    for item in items:
        if trace:
            print('  %r %s' % (item, item.text))
        item.dispose()
# @+node:ekr.20181113043004.1: *7* flx_tree.action.redraw_with_dict & helper
@flx.action
def redraw_with_dict(self, redraw_dict):
    """
    Clear the present tree and redraw using the **recursive** redraw_list.
    d has the form:
        {
            'c.p': self.p_to_ap(p),
            'items': [
                self.make_dict_for_position(p)
                    for p in c.rootPosition().self_and_siblings()
            ],
        }
    """
    # This is called only from app.action.redraw.
    trace = 'drawing' in g.app.debug
    tag = 'redraw_with_dict'
    assert redraw_dict
    self.clear_tree()
    items = redraw_dict['items']
    if trace:
        print('')
        print('%s: %s direct children' % (tag, len(items)))
    for item in items:
        tree_item = self.create_item_with_parent(item, self.tree)
        if trace:
            print('  %r %s' % (tree_item, item['headline']))
    #
    # Select c.p.
    self.select_ap(redraw_dict['c.p'])
    redraw_dict = {}  # #1127: Remove references to deleted items.
# @+node:ekr.20181124194248.1: *8* tree.create_item_with_parent
def create_item_with_parent(self, item, parent):
    """Create a tree item for item and all its visible children."""
    # pylint: disable=no-member
    # set_collapsed is in the base class.
    trace = 'drawing' in g.app.debug
    tag = 'create_item_with_parent'
    ap = item['ap']
    #
    # Create the node.
    with parent:
        tree_item = LeoFlexxTreeItem(ap, text=ap['headline'], checked=None, collapsed=True)
    tree_item.set_collapsed(not ap['expanded'])
    #
    # Set the data.
    key = self.ap_to_key(ap)
    self.tree_items_dict[key] = tree_item
    #
    # Children are *not* necessarily sent, so set the populated 'bit' only if they are.
    if item['children']:
        if trace:
            print(tag, '**populated**', ap['headline'])
        self.populated_items_dict[key] = True
    if hasattr(parent, 'leo_children'):
        # print(tag, parent.leo_ap['headline'], ap['headline'])
        parent.leo_children.append(tree_item)
    #
    # Create the children.
    for child in item['children']:
        self.create_item_with_parent(child, tree_item)
    return tree_item  # Debugging
# @+node:ekr.20181114072307.1: *7* flx_tree.ap_to_key
def ap_to_key(self, ap):
    """Produce a key for the given ap."""
    self.assert_exists(ap)
    childIndex = ap['childIndex']
    gnx = ap['gnx']
    headline = ap['headline']
    stack = ap['stack']
    stack_s = '::'.join([
        'childIndex: %s, gnx: %s' % (z['childIndex'], z['gnx'])
            for z in stack
    ])
    key = 'Tree key<childIndex: %s, gnx: %s, %s <stack: %s>>' % (
        childIndex, gnx, headline, stack_s or '[]')
    return key
# @+node:ekr.20181113085722.1: *7* flx_tree.dump_ap
def dump_ap(self, ap, padding, tag):
    """Print an archived position fully."""
    stack = ap['stack']
    if not padding:
        padding = ''
    padding = padding + ' ' * 4
    if stack:
        print('%s%s:...' % (padding, tag or 'ap'))
        padding = padding + ' ' * 4
        print('%schildIndex: %s v: %s %s stack: [' % (
            padding,
            str(ap['childIndex']),
            ap['gnx'],
            ap['headline'],
        ))
        padding = padding + ' ' * 4
        for d in ap['stack']:
            print('%s%s %s %s' % (
                padding,
                str(d['childIndex']).ljust(3),
                d['gnx'],
                d['headline'],
            ))
        padding = padding[:-4]
        print(']')
    else:
        print('%s%s: childIndex: %s v: %s stack: [] %s' % (
            padding, tag or 'ap',
            str(ap['childIndex']).ljust(3),
            ap['gnx'],
            ap['headline'],
        ))
# @+node:ekr.20181104080854.3: *7* flx_tree.reaction: on_tree_event
# actions: set_checked, set_collapsed, set_parent, set_selected, set_text, set_visible
@flx.reaction(
    'tree.children**.checked',
    'tree.children**.collapsed',
    'tree.children**.visible',  # Never seems to fire.
)
def on_tree_event(self, *events):
    for ev in events:
        expand = not ev.new_value
        if expand:
            # Don't redraw if the LeoTreeItem has children.
            tree_item = ev.source
            ap = tree_item.leo_ap
            if ap['expanded']:
                if 0:
                    print('flx.tree.on_tree_event: already expanded', ap['headline'])
            else:
                ap['expanded'] = True
                # Populate children, if necessary.
                self.start_populating_children(ap, tree_item)
# @+node:ekr.20181120063735.1: *6* flx_tree.Focus
@flx.action
def set_focus(self):
    if 'focus' in g.app.debug:
        tag = 'flx.tree.set_focus'
        print('%30s:' % tag)
    self.node.focus()
# @+node:ekr.20181123165819.1: *6* flx_tree.Incremental Drawing...
# This are not used, at present, but they may come back.
# @+node:ekr.20181125051244.1: *7* flx_tree.populate_children
def populate_children(self, children, parent_ap):
    """
    Populate the children of the given parent.

    self.populating_tree_item is the LeoFlexxTreeItem to be populated.

    children is a list of ap's.
    """
    parent = self.populating_tree_item
    assert parent
    # The expansion bit may have changed?
    assert parent_ap == parent.leo_ap
    # print('flx.tree.populate_children: parent: %r %s children' % (parent, len(children)))
    for child_ap in children:
        self.create_item_with_parent(child_ap, parent)
    self.populating_tree_item = False
# @+node:ekr.20181111011928.1: *7* flx_tree.start_populating_children
def start_populating_children(self, parent_ap, parent_tree_item):
    """
    Populate the parent tree item with the children if necessary.

    app.send_children_to_tree should send an empty list
    """
    self.assert_exists(parent_ap)
    self.assert_exists(parent_tree_item)
    key = self.ap_to_key(parent_ap)
    if key in self.populated_items_dict:
        # print('%s: already populated: %s' % (tag, headline))
        return
    assert isinstance(parent_tree_item, LeoFlexxTreeItem)
    # Remember the parent_tree_item.
    self.populating_tree_item = parent_tree_item
    #
    # Ask for the items.
    self.root.send_children_to_tree(parent_ap)
# @+node:ekr.20181110175222.1: *7* flx_tree.action.receive_children
@flx.action
def receive_children(self, d):
    """
    Populate the direct descendants of ap. d is compatible with make_redraw_dict:
        {
            'parent_ap': parent_ap,
            'items': [
                self.make_dict_for_position(p)
                    for p in for p in p.children()
            ],
        }
    """
    parent_ap = d['parent_ap']
    children = d['items']
    # print('flx.tree.receive_children: %s children' % (len(children)))
    self.populate_children(children, parent_ap)
# @+node:ekr.20181120061140.1: *6* flx_tree.Key handling
@flx.emitter
def key_press(self, e):
    ev = self._create_key_event(e)
    mods, key = ev['modifiers'], ev['key']
    # f_key = not mods and key.startswith('F')
    # print('flx.tree.key_press: %r preventDefault: %s', (ev, not f_key))
    # Use default action for F-Keys.
    if not mods and key.startswith('F'):
        return ev
    # Don't ignore Return
    # if not mods and key == 'Enter':
    #     return ev
    #
    # Prevent default action for all other keys.
    e.preventDefault()
    return ev

@flx.reaction('tree.key_press')
def on_key_press(self, *events):
    # print('flx.tree.on_key_press')
    for ev in events:
        self.root.do_key(ev, 'tree')
# @+node:ekr.20181121195235.1: *6* flx_tree.Selecting...
# @+node:ekr.20181123171958.1: *7* flx_tree.action.set_ap
@flx.action
def set_ap(self, ap):
    """self.selected_ap. Called from app.select_ap."""
    assert ap
    self.selected_ap = ap
    self.select_ap(self.selected_ap)
# @+node:ekr.20181116083916.1: *7* flx_tree.select_ap
@flx.action
def select_ap(self, ap):
    """
    Select the tree item corresponding to the given ap.

    Called from the mutator, and also on_selected_event.
    """
    # print('flx.tree.select_ap', repr(ap), ap ['headline'])
    key = self.ap_to_key(ap)
    item = self.tree_items_dict.get(key)
    if item:
        item.set_selected(True)
        self.selected_ap = ap  # Set the item's selected property.
    else:
        pass  # We may be in the middle of a redraw.
# @+node:ekr.20181109083659.1: *7* flx_tree.reaction.on_selected_event
@flx.reaction('tree.children**.selected')  # don't use mode="greedy" here!
def on_selected_event(self, *events):
    """
    Update c.p and c.p.b when the user selects a new tree node.

    This also gets fired on *unselection* events, which causes problems.
    """
    #
    # Reselect the present ap if there are no selection events.
    # This ensures that clicking a headline twice has no effect.
    if not any([ev.new_value for ev in events]):
            # Must use a comprehension above. flexx can't handle generator expressions.
        ev = events[0]
        self.assert_exists(ev)
        ap = ev.source.leo_ap
        self.assert_exists(ap)
        self.select_ap(ap)
        return
    #
    # handle selection events.
    for ev in events:
        if ev.new_value:  # A selection event.
            ap = ev.source.leo_ap
            # print('on_selected_event: select:', ap['headline'])
            self.select_ap(ap)  # Selects the corresponding LeoTreeItem.
            self.set_ap(ap)  # Sets self.selected_ap.
            # A helper action, calling tree.select.
            self.root.select_tree_using_ap(ap)
# @+node:ekr.20181108233657.1: *5* class LeoFlexxTreeItem
class LeoFlexxTreeItem(flx.TreeItem):

    def init(self, leo_ap):
        # pylint: disable=arguments-differ
        self.leo_ap = leo_ap  # Immutable: Gives access to cloned, marked, expanded fields.
        self.leo_children = []

    def getName(self):
        return 'head'  # Required, for proper pane bindings.

    @others
# @+node:ekr.20150107090324.1: *3* ../plugins/cursesGui.py
"""A minimal text-oriented gui."""

@
Things not found in the GUI 'interface' classes (in leoFrame.py, leoGui.py, etc)
are labeled: # undoc: where the AttributeError comes from other implementations
of method.
@c
# pylint: disable=arguments-differ
<< cursesGui.py: imports & annotations >>
<< TODO >>
@others
@language python
@tabwidth -4
# @+node:ekr.20150107090324.2: *4* << cursesGui.py: imports & annotations >>
from collections.abc import Callable
import os
from typing import Any
from leo.core import leoGlobals as g
from leo.core import leoChapters
from leo.core import leoGui
from leo.core import leoKeys
from leo.core import leoFrame
from leo.core import leoMenu
from leo.core import leoNodes
from leo.core.leoAPI import StringTextWrapper

get_input = input


# @+node:ekr.20150107090324.3: *4* << TODO >>
@nocolor-node
@
Body text:
Is the "signature" of the typing event right?
What does the InsertPoint do when text is inserted and deleted?
What does the SelectionRange do, period?
What about mouse input? What does createBindings() do?
What does set_focus() do?
What does the GUI need to do for Leo's undo features?
What about that minibuffer thing? (I've never used it.)
When should runMainLoop return?
What kind of newlines does the body text control get? How should it treat them?
Headline editing?
Body text selection.
(Strip trailing whitespace from this file. :P)
< < Random cruft >>
Pay attention to being direct and code-terse.
Not at all user-friendly.
Comments in the body reflect current status only.
Ideally, comments in the body go away as the "leoGUI interface" improves.
Written on a hundred-column terminal. :S
# @+node:ekr.20150107090324.4: *4* init
def init():
    ok = not g.app.gui and not g.unitTesting  # Not Ok for unit testing!
    if ok:
        g.app.gui = textGui()
        g.app.root = g.app.gui.createRootWindow()
        g.app.gui.finishCreate()
        g.plugin_signon(__name__)
    elif g.app.gui and not g.unitTesting:
        s = "Can't install text gui: previous gui installed"
        g.es_print(s, color="red")
    return ok


# @+node:ekr.20150107090324.5: *4* underline
def underline(s, idx):
    if idx < 0 or idx > len(s) - 1:
        return s
    return s[:idx] + '&' + s[idx:]


# @+node:ekr.20150107090324.6: *4* class textGui: cursesGui.py
class textGui(leoGui.LeoGui):
    @others


# @+node:ekr.20150107090324.7: *5* __init__
def __init__(self):
    super().__init__("text")
    self.frames = []
    self.killed = False
    # TODO leoTkinterFrame finishCreate g.app.windowList.append(f) - use that?

# @+node:ekr.20150107090324.8: *5* createKeyHandlerClass
def createKeyHandlerClass(self, c):
    return leoKeys.KeyHandlerClass(c)

# @+node:ekr.20150107090324.9: *5* createLeoFrame
def createLeoFrame(self, c, title=None) -> Any:
    gui = self
    ret = TextFrame(c, gui)
    self.frames.append(ret)
    return ret

# @+node:ekr.20150107090324.10: *5* createRootWindow
def createRootWindow(self):
    pass  # N/A

# @+node:ekr.20150107090324.11: *5* destroySelf
def destroySelf(self):
    self.killed = True

# @+node:ekr.20150107090324.12: *5* finishCreate (cursesGui.py)
def finishCreate(self):
    pass

# @+node:ekr.20150107090324.17: *5* get/set_focus
def get_focus(self, c):
    pass

def set_focus(self, c, w):
    pass

# @+node:ekr.20150107090324.13: *5* isTextWidget (cursesGui.py)
def isTextWidget(self, w):
    """Return True if w is a Text widget suitable for text-oriented commands."""
    return w and isinstance(w, StringTextWrapper)

# @+node:ekr.20150107090324.69: *5* runAskYesNoDialog (cursesGui.py)
def runAskYesNoDialog(self, c, title, message=None, yes_all=False, no_all=False):
    return 'yes'

# @+node:ekr.20150107090324.15: *5* runMainLoop
def runMainLoop(self):
    self.text_run()

# @+node:ekr.20150107090324.16: *5* runOpenFileDialog (cursesGui2)
def runOpenFileDialog(
    self,
    c,
    title,
    *,
    filetypes: list[tuple[str, str]],
    defaultextension='',  # Not used.
    startpath=None,
) -> str:
    initialdir = g.app.globalOpenDir or g.os_path_abspath(os.getcwd())
    ret = get_input("Open which %s file (from %s?) > " % (repr(filetypes), initialdir))
    return ret

# @+node:ekr.20150107090324.18: *5* text_run & helper
def text_run(self):
    frame_idx = 0
    while not self.killed:
        # Frames can come and go.
        if frame_idx > len(self.frames) - 1:
            frame_idx = 0
        f = self.frames[frame_idx]
        g.pr(f.getTitle())
        s = get_input('Do what? (menu,key,body,frames,tree,quit) > ')
        try:
            self.doChoice(f, s)
        except Exception:
            g.es_exception()

# @+node:ekr.20150107090324.19: *6* doChoice
def doChoice(self, f, s):
    if s in ('m', 'menu'):
        f.menu.show_menu()
    elif s in ('k', 'key'):
        f.text_key()
    elif s in ('b', 'body'):
        f.body.text_show()
    elif s in ('f', 'frames'):
        for i, f in enumerate(self.frames):
            g.pr(i, ')', f.getTitle())
        s = get_input('Operate on which frame? > ')
        try:
            s = int(s)
        except ValueError:
            s = -1
        # if s >= 0 and s <= len(self.frames) - 1:
        #    frame_idx = s
    elif s in ('t', 'tree'):
        f.tree.text_draw_tree()
    elif s in ('q', 'quit'):
        self.killed = True

# @+node:ekr.20150107090324.20: *5* widget_name (cursesGui.py)
def widget_name(self, w):
    if isinstance(w, textBodyCtrl):
        return 'body'
    return leoGui.LeoGui.widget_name(self, w)

# @+node:ekr.20150107090324.21: *4* class TextFrame: cursesGui.py
class TextFrame(leoFrame.LeoFrame):
    @others


# @+node:ekr.20150107090324.22: *5* __init__
def __init__(self, c, gui):
    super().__init__(c, gui)
    assert self.c == c
    self.top = None

# @+node:ekr.20150107090324.23: *5* createFirstTreeNode (cursesGui.py)
def createFirstTreeNode(self):
    c = self.c
    #
    # #1631: Initialize here, not in p._linkAsRoot.
    c.hiddenRootNode.children = []
    #
    # #1817: Clear the gnxDict.
    c.fileCommands.gnxDict = {}
    #
    v = leoNodes.vnode(context=c)
    p = leoNodes.Position(v)
    v.initHeadString("newHeadline")
    # New in Leo 4.5: p.moveToRoot would be wrong:
    # the node hasn't been linked yet.
    p._linkAsRoot()
    # c.setRootPosition(p) # New in 4.4.2.

# @+node:ekr.20150107090324.24: *5* deiconify
def deiconify(self):
    pass  # N/A

def lift(self):
    pass  # N/A

# @+node:ekr.20150107090324.25: *5* destroySelf
def destroySelf(self):
    pass

# @+node:ekr.20150107090324.26: *5* finishCreate (cursesGui.py)
def finishCreate(self):
    c, f = self.c, self
    f.tree = textTree(self)
    f.body = textBody(frame=self)
    f.log = textLog(frame=self)
    f.menu = textLeoMenu(self)
    if f.body.use_chapters:
        c.chapterController = leoChapters.ChapterController(c)
    f.createFirstTreeNode()
    # (*after* setting self.log)
    c.setLog()  # writeWaitingLog hangs without this(!)
    # So updateRecentFiles will update our menus.
    g.app.windowList.append(f)

# @+node:ekr.20161118195504.1: *5* getFocus
def getFocus(self):
    return None

# @+node:ekr.20150107090324.27: *5* setInitialWindowGeometry
def setInitialWindowGeometry(self):
    pass  # N/A

# @+node:ekr.20150107090324.28: *5* setMinibufferBindings
def setMinibufferBindings(self):
    pass

def setTopGeometry(self, w, h, x, y):
    pass  # N/A

# @+node:ekr.20150107090324.29: *5* text_key
def text_key(self):
    c = self.c
    k = c.k
    w = self.body.bodyCtrl
    key = get_input('Keystroke > ')
    if not key:
        return

    class leoTypingEvent:
        def __init__(self, c, w, char, keysym):
            self.c = c
            self.char = char
            self.keysym = keysym
            self.leoWidget = w
            self.widget = w

    char = key
    stroke = c.k.shortcutFromSetting(char)
    g.trace('char', repr(char), 'stroke', repr(stroke))
    e = leoTypingEvent(c, w, char, stroke)
    k.masterKeyHandler(event=e)

# @+node:ekr.20150107090324.30: *5* update
def update(self):
    pass

def resizePanesToRatio(self, ratio: float, ratio2: float) -> None:
    pass  # N/A

# @+node:ekr.20150107090324.31: *4* class textBody: cursesGui.py
class textBody(leoFrame.LeoBody):
    @others


# @+node:ekr.20150107090324.32: *5* textBody.__init__
def __init__(self, frame):
    super().__init__(frame)
    c = frame.c
    name = 'body'
    self.bodyCtrl = textBodyCtrl(c, name)
    self.colorizer = leoFrame.NullColorizer(self.c)

# @+node:ekr.20150107090324.33: *5* bind
# undoc: newLeoCommanderAndFrame -> c.finishCreate -> k.finishCreate ->
# k.completeAllBindings -> k.makeMasterGuiBinding -> 2156 w.bind ; nullBody

def bind(self, bindStroke, callback):
    pass

# @+node:ekr.20150107090324.34: *5* setEditorColors
def setEditorColors(self, bg, fg):
    pass  # N/A

def createBindings(self, w=None):
    pass

# @+node:ekr.20150107090324.35: *5* text_show
def text_show(self):
    w = self.bodyCtrl
    g.pr('--- body ---')
    g.pr('ins', w.ins, 'sel', w.sel)
    g.pr(w.s)

# @+node:ekr.20150107090324.36: *4* class textBodyCtrl (StringTextWrapper) cursesGui.py
class textBodyCtrl(StringTextWrapper):
    pass


# @+node:ekr.20150107090324.37: *4* class textMenuCascade: cursesGui.py
class textMenuCascade:
    def __init__(self, menu, label, underline):
        self.menu = menu
        self.label = label
        self.underline = underline

    @others


# @+node:ekr.20150107090324.39: *5* display
def display(self):
    ret = underline(self.label, self.underline)
    if not self.menu.entries:
        ret += ' [Submenu with no entries]'
    return ret

# @+node:ekr.20150107090324.40: *4* class textMenuEntry: cursesGui.py
class textMenuEntry:
    @others


# @+node:ekr.20150107090324.41: *5* __init__
def __init__(self, label, underline, accel, callback):
    self.label = label
    self.underline = underline
    self.accel = accel
    self.callback = callback

# @+node:ekr.20150107090324.42: *5* display
def display(self):
    return "%s %s" % (
        underline(self.label, self.underline),
        self.accel,
    )

# @+node:ekr.20150107090324.43: *4* class textMenuSep: cursesGui.py
class textMenuSep:
    @others


# @+node:ekr.20150107090324.44: *5* display
def display(self):
    return '-' * 5

# @+node:ekr.20150107090324.45: *4* class textLeoMenu (LeoMenu) cursesGui.py
class textLeoMenu(leoMenu.LeoMenu):
    @others


# @+node:ekr.20150107090324.46: *5* ctor (textLeoMenu)
def __init__(self, frame):
    self.entries = []
    self.c = frame.c
    super().__init__(frame)

# @+node:ekr.20150107090324.47: *5* createMenuBar
def createMenuBar(self, frame):
    g.trace(frame.c)
    self._top_menu = textLeoMenu(frame)
    self.createMenusFromTables()

# @+node:ekr.20150107090324.48: *5* new_menu
def new_menu(self, parent: Any, tearoff: int = 0, labe: str = ''):
    if tearoff:
        raise NotImplementedError(repr(tearoff))
    menu = textLeoMenu(parent or self.frame)
    menu.entries = []
    return menu

# @+node:ekr.20150107090324.49: *5* add_cascade (cursesGui.py)
def add_cascade(self, parent: Any, label: str, menu: Any, underline: int):
    if parent is None:
        parent = self._top_menu
    parent.entries.append(
        textMenuCascade(
            menu,
            label,
            underline,
        )
    )

# @+node:ekr.20150107090324.50: *5* add_command (cursesGui.py)
def add_command(
    self,
    menu: Any,
    accelerator: str = '',
    command: Callable = None,
    commandName: str = None,
    label: str = None,
    underline: int = 0,
) -> None:
    # ?
    # underline - Offset into label. For those who memorised Alt, F, X rather than Alt+F4.
    # accelerator - For display only; these are implemented by Leo's key handling.
    menu = self

    def doNothingCallback():
        pass

    if not command:
        command = doNothingCallback

    # label = keys.get('label') or 'no label'
    # underline = keys.get('underline') or 0
    # accelerator = keys.get('accelerator') or ''
    # command = keys.get('command') or doNothingCallback
    entry = textMenuEntry(label, underline, accelerator, command)
    menu.entries.append(entry)

# @+node:ekr.20150107090324.51: *5* add_separator
def add_separator(self, menu):
    menu.entries.append(textMenuSep())

# @+node:ekr.20150107090324.52: *5* delete_range
def delete_range(self, *args, **keys):
    pass

# @+node:ekr.20150107090324.53: *5* show_menu
def show_menu(self):
    last_menu = self._top_menu
    while True:
        entries = last_menu.entries
        for i, entry in enumerate(entries):
            g.pr(i, ')', entry.display())
        g.pr(len(last_menu.entries), ')', '[Prev]')
        which = get_input('Which menu entry? > ')
        which = which.strip()
        if not which:
            continue
        try:
            n = int(which)
        except ValueError:
            # Look for accelerator character.
            ch = which[0].lower()
            for n, z in enumerate(entries):
                if hasattr(z, 'underline') and ch == z.label[z.underline].lower():
                    break
            else:
                continue
        if n == len(entries):
            return
        if n < 0 or n > len(entries) - 1:
            continue
        menu = entries[n]
        if isinstance(menu, textMenuEntry):
            menu.callback()
            return
        if isinstance(menu, textMenuCascade):
            last_menu = menu.menu
        else:
            pass

# @+node:ekr.20150107090324.54: *4* class textLog: cursesGui.py
class textLog(leoFrame.LeoLog):
    @others


# @+node:ekr.20150107090324.58: *5* createControl
# < < HACK Quiet, oops. >>

def createControl(self, parentFrame):
    pass

def setFontFromConfig(self):
    pass  # N/A

# @+node:ekr.20150107090324.68: *5* finishCreate (cursesGui.py)
def finishCreate(self):
    pass

# @+node:ekr.20150107090324.56: *5* put
def put(self, s, color=None, tabName='Log', from_redirect=False):
    g.pr(s, newline=False)

# @+node:ekr.20150107090324.57: *5* putnl
def putnl(self, tabName='log'):
    g.pr('')

# @+node:ekr.20150107090324.59: *5* setColorFromConfig
def setColorFromConfig(self):
    pass

# @+node:ekr.20150107090324.55: *5* setTabBindings
def setTabBindings(self, tabName):
    pass

# @+node:ekr.20150107090324.60: *4* class textTree: cursesGui.py
class textTree(leoFrame.LeoTree):
    @others


# @+node:ekr.20150107090324.61: *5* setBindings
def setBindings(self):
    pass

# @+node:ekr.20150107090324.62: *5* begin/endUpdate & redraw/now
def redraw(self, p=None):
    self.text_draw_tree()

redraw_now = redraw

# @+node:ekr.20150107090324.63: *5* endUpdate
# @+node:ekr.20150107090324.64: *5* textTree.__init__
def __init__(self, frame):
    # undoc: openWithFileName -> treeWantsFocus -> c.frame.tree.canvas
    self.c = frame.c
    super().__init__(frame)

# @+node:ekr.20150107090324.65: *5* select
def select(self, p, scroll=True):
    # TODO Much more here: there's four hooks and all sorts of other things called in the TK version.
    c = self.c
    w = c.frame.body.bodyCtrl
    c.setCurrentPosition(p)
    # This is also where the body-text control is given the text of the selected node...
    # Always do this.    Otherwise there can be problems with trailing hewlines.
    w.setAllText(p.b)
    # and something to do with undo?

# @+node:ekr.20150107090324.66: *5* editLabel & headline_wrapper (cursesGui)
def editLabel(self, v, selectAll: bool = False, selection: tuple = None) -> tuple[None, None]:
    return None, None

def headline_wrapper(self, p):
    return None

# @+node:ekr.20150107090324.67: *5* text_draw_tree & helper
def text_draw_tree(self):
    g.pr('--- tree ---')
    self.draw_tree_helper(self.c.rootPosition(), indent=0)

def draw_tree_helper(self, p, indent):
    for p in p.self_and_siblings():
        if p.hasChildren():
            box = '+' if p.isExpanded() else '-'
        else:
            box = ' '
        icons = '%s%s%s%s' % (
            'b' if p.b else ' ',
            'm' if p.isMarked() else ' ',
            '@' if p.isCloned() else ' ',
            '*' if p.isDirty() else ' ',
        )
        g.pr(" " * indent * 2, icons, box, p.h)
        if p.isExpanded() and p.hasChildren():
            self.draw_tree_helper(p.firstChild(), indent + 1)

# @+node:ekr.20170419093747.1: *3* app.createTextGui (was createCursesGui)
def createTextGui(self, fileName: str = '', verbose: bool = False) -> None:
    app = self
    app.pluginsController.loadOnePlugin('leo.plugins.cursesGui', verbose=verbose)

# @+node:ekr.20260323083734.1: ** Temporary Attic
# @+node:ekr.20260407033920.1: *3* Deleted by PR #4576
https://github.com/leo-editor/leo-editor/pull/4576
# @+node:ekr.20160427062131.1: *4* c.is_unusual_focus
def is_unusual_focus(self, w: Wrapper) -> bool:
    """Return True if w is not in an expected place."""
    #
    # #270: Leo's keyboard events doesn't work after "Insert"
    #       on headline and Alt+Tab, Alt+Tab
    #
    # #276: Focus lost...in Nav text input
    from leo.plugins import qt_frame

    return isinstance(w, qt_frame.QtTabBarWrapper)

# @+node:ekr.20150403063658.1: *4* c.trace_idle_focus
def trace_idle_focus(self, w: Wrapper) -> None:
    """Trace the focus for w, minimizing chatter."""
    from leo.core.leoQt import QtWidgets
    from leo.plugins import qt_frame

    trace = 'focus' in g.app.debug
    trace_known = False
    c = self
    table = (
        QtWidgets.QWidget,
        qt_frame.LeoQTreeWidget,
    )
    count = c.idle_focus_count
    if w:
        w_class = w and w.__class__.__name__
        if self.is_unusual_focus(w):
            if trace:
                g.trace(f"{count} unusual focus: {w_class}")
        else:
            c.last_unusual_focus = None
            if isinstance(w, table):
                if trace and trace_known:
                    g.trace(f"{count} known focus: {w_class}")
            elif trace:
                g.trace(f"{count} unknown focus: {w_class}")
    else:
        if trace:
            g.trace(f"{count:3} no focus")

# @+node:ekr.20260423033738.1: *4* class BaseTextAPI
class BaseTextAPI:
    """
    A class specifying the interface to various text widgets,
    including Leo's headline, body pane and other widgets.
    """

    def __init__(self, c: Cmdr) -> None:
        pass

    def appendText(self, s: str) -> None:
        pass

    def clipboard_append(self, s: str) -> None:
        pass

    def clipboard_clear(self) -> None:
        pass

    def delete(self, i: int, j: int = None) -> None:
        pass

    def deleteTextSelection(self) -> None:
        pass

    def disable(self) -> None:
        pass

    def enable(self, enabled: bool = True) -> None:
        pass

    def flashCharacter(
        self,
        i: int,
        bg: str = 'white',
        fg: str = 'red',
        flashes: int = 3,
        delay: int = 75,
    ) -> None:
        pass

    def get(self, i: int, j: int) -> str:
        return ''

    def getAllText(self) -> str:
        return ''

    def getInsertPoint(self) -> int:
        return 0

    def getSelectedText(self) -> str:
        return ''

    def getSelectionRange(self) -> tuple[int, int]:
        return (0, 0)

    def getXScrollPosition(self) -> int:
        return 0

    def getYScrollPosition(self) -> int:
        return 0

    def hasSelection(self) -> bool:
        return False

    def insert(self, i: int, s: str) -> None:
        pass

    def see(self, i: int) -> None:
        pass

    def seeInsertPoint(self) -> None:
        pass

    def selectAllText(self, insert: str = None) -> None:
        pass

    def setAllText(self, s: str) -> None:
        pass

    def setFocus(self) -> None:
        pass  # Required: sets the focus to wrapper.widget.

    def setInsertPoint(self, pos: str, s: str = None) -> None:
        pass

    def setSelectionRange(self, i: int, j: int, insert: int = None) -> None:
        pass

    def setXScrollPosition(self, i: int) -> None:
        pass

    def setYScrollPosition(self, i: int) -> None:
        pass


# @+node:ekr.20070317073627: *4* class LeoTreeTab
class LeoTreeTab:
    """A class representing a tabbed outline pane."""

    @others


# @+node:ekr.20070317073627.1: *5* LeoTreeTab.ctor (LeoTreeTab)
def __init__(self, c: Cmdr, chapterController: ChapterController, parentFrame: Widget) -> None:
    self.c = c
    self.cc: ChapterController
    self.nb: NbController = None  # Created in createControl.
    self.parentFrame: Widget = parentFrame

# @+node:ekr.20070317073755: *5* LeoTreeTab: Must be defined in subclasses
def createControl(self) -> TextAPI:
    raise NotImplementedError

def createTab(
    self,
    tabName: str,
    createText: bool = True,
    widget: Widget = None,
    select: bool = True,
) -> None:
    raise NotImplementedError

def destroyTab(self, tabName: str) -> None:
    raise NotImplementedError

def selectTab(self, tabName: str, wrap: str = 'none') -> None:
    raise NotImplementedError

def setTabLabel(self, tabName: str) -> None:
    raise NotImplementedError

# @+node:ekr.20140901141402.18700: *4* class PlainTextWrapper(QTextMixin)
class PlainTextWrapper(QTextMixin):
    """A Qt class for use by the find code."""

    def __init__(self, widget: QWidget) -> None:
        """Ctor for the PlainTextWrapper class."""
        super().__init__()
        self.widget = widget


# @+node:ekr.20170613095422.1: *4* class StringGui (LeoGui)
class StringGui(LeoGui):
    """
    A class representing all on-screen objects using subclasses of the
    leoAPI.StringTextWrapper class.
    """

    @others


# @+node:ekr.20170613114120.1: *5* StringGui.runMainLoop
def runMainLoop(self) -> None:
    raise NotImplementedError

# @+node:ekr.20260401143657.1: *4* g.printClassDict
def printClassDict() -> None:
    """Print g.check_class_dict"""

    # Print the values that *have* been found.
    d = g.check_class_dict
    all_seen_classes = set()
    print('')
    print('Seen objects...')
    for key, value in sorted(d.items()):
        print(f"{key}:")
        classes = [z.strip() for z in sorted(value)]
        print(f"    {', '.join(classes)}")
        for z in classes:
            all_seen_classes.add(z)

    # Compute all known classes.
    d2 = g.all_class_dict
    all_known_classes = set()
    for key, value in sorted(d2.items()):
        for z in value:
            all_known_classes.add(z)

    # Print all known classes.
    if 0:
        print('')
        print('All known classes...')
        for z in sorted(all_seen_classes):
            print(f"    {z}")

    # Print all known classes that *haven't* been found.
    print('')
    print('Unseen classes...')
    for z in all_known_classes:
        if z not in all_seen_classes:
            print(f"    {z}")


# @+node:ekr.20110605121601.18275: *4* LeoQtFrame.configureBar
def configureBar(self, bar: QMenuBar, verticalFlag: bool) -> None:  # pylint: disable=disallowed-name
    c = self.c
    # Get configuration settings.
    w = c.config.getInt("split-bar-width")
    if not w or w < 1:
        w = 7
    relief = c.config.get("split_bar_relief", "relief")
    if not relief:
        relief = "flat"
    color = c.config.getColor("split-bar-color")
    if not color:
        color = "LightSteelBlue2"
    try:
        if verticalFlag:
            # Panes arranged vertically; horizontal splitter bar
            bar.configure(relief=relief, height=w, bg=color, cursor="sb_v_double_arrow")
        else:
            # Panes arranged horizontally; vertical splitter bar
            bar.configure(relief=relief, width=w, bg=color, cursor="sb_h_double_arrow")
    except Exception:
        # Could be a user error. Use all defaults
        g.es("exception in user configuration for splitbar")
        g.es_exception()
        if verticalFlag:
            # Panes arranged vertically; horizontal splitter bar
            bar.configure(height=7, cursor="sb_v_double_arrow")
        else:
            # Panes arranged horizontally; vertical splitter bar
            bar.configure(width=7, cursor="sb_h_double_arrow")

# @+node:ekr.20110605121601.18276: *4* LeoQtFrame.configureBarsFromConfig
def configureBarsFromConfig(self) -> None:
    c = self.c
    w = c.config.getInt("split-bar-width")
    if not w or w < 1:
        w = 7
    relief = c.config.get("split_bar_relief", "relief")
    if not relief or relief == "":
        relief = "flat"
    color = c.config.getColor("split-bar-color")
    if not color or color == "":
        color = "LightSteelBlue2"
    if self.splitVerticalFlag:
        bar1, bar2 = self.bar1, self.bar2
    else:
        bar1, bar2 = self.bar2, self.bar1
    try:
        bar1.configure(relief=relief, height=w, bg=color)
        bar2.configure(relief=relief, width=w, bg=color)
    except Exception:
        # Could be a user error.
        g.es("exception in user configuration for splitbar")
        g.es_exception()

# @+node:ekr.20110605121601.18277: *4* LeoQtFrame.reconfigureFromConfig
def reconfigureFromConfig(self) -> None:
    """Init the configuration of the Qt frame from settings."""
    c, frame = self.c, self
    frame.configureBarsFromConfig()
    frame.setTabWidth(c.tab_width)
    c.redraw()

# @+node:ekr.20260404050449.1: *4* script: Virtual Qt User
g.cls()
for command_or_key in [
    'full-command',  # Activate minibuffer.
    's',
    'h',
    '\t',  # Tab completion!
    'keyboard-quit',  # Deactivate minibuffer.
]:
    stroke = c.k.getStrokeForCommandName(command_or_key)
    if stroke:  # The name of a command.
        print(f"{stroke.s!r}: {command_or_key}")
        g.app.gui.event_generate(c, char=None, shortcut=stroke.s, w=None)
    else:  # A single character.
        char = binding = command_or_key
        event = g.app.gui.create_key_event(c, char=char, binding=binding)
        print(f"{event.char!r}")
        c.k.masterKeyHandler(event)
        g.app.gui.qtApp.processEvents()
        g.sleep(1.0)  # Testing only.
# @+node:ekr.20260420174001.1: *3* Deleted by PR #4624
# @+node:ekr.20260420154627.8: *4* dw.addNewEditor
def addNewEditor(self, name: str) -> tuple[QWidget, QTextMixin]:
    """Create a new body editor."""
    c, p = self.leo_c, self.leo_c.p
    body = c.frame.body
    assert isinstance(body, LeoQtBody), repr(body)
    # Step 1: create the editor.
    parent_frame = c.frame.top.leo_body_inner_frame
    widget = qt_text.LeoQTextBrowser(parent_frame, c, self)
    widget.setObjectName('richTextEdit')  # Will be changed later.
    wrapper = qt_text.QTextEditWrapper(widget, name='body', c=c)
    self.packLabel(widget)
    # Step 2: inject ivars, set bindings, etc.
    inner_frame = c.frame.top.leo_body_inner_frame  # Inject ivars *here*.
    body.injectIvars(inner_frame, name, p, wrapper)
    body.updateInjectedIvars(widget, p)
    wrapper.setAllText(p.b)
    wrapper.see(0)
    c.k.completeAllBindingsForWidget(wrapper)
    if isinstance(widget, QtWidgets.QTextEdit):
        colorizer = leoColorizer.make_colorizer(c, widget)
        colorizer.highlighter.setDocument(widget.document())
    else:
        # Scintilla only.
        body.recolorWidget(p, wrapper)
    return parent_frame, wrapper

# @+node:ekr.20260425153444.1: *3* Deleted by PR #4631
# @+node:ekr.20260418104208.1: *4*  LeoQtGui.create_wrapper_for_widge
def create_wrapper_for_widget(self, c: Cmdr, w: Any) -> QTextMixin:
    if w is None:
        return None
    if isinstance(w, QTextMixin):
        return w
    if getattr(w, 'wrapper', None):
        return w.wrapper
    if getattr(w, 'leo_wrapper', None):
        return w.leo_wrapper
    if isinstance(w, LeoQtTree):
        # A strange special case: the class allocates its own wrappers.
        return None
    # Defensive code.
    w.leo_wrapper = (
             QLineEditWrapper(c=c, widget=w) if isinstance(w, QtWidgets.QLineEdit)
        else QTextEditWrapper(c=c, widget=w) if isinstance(w, QtWidgets.QTextEdit)
        else StringTextWrapper(c=c)
    )  # fmt: skip
    g.trace(f"Allocate {w.leo_wrapper.__class__.__name__} for {w.__class__.__name__}")
    return w.leo_wrapper

# @+node:ekr.20260425154059.8: *4* class PlainTextWrapper(QTextMixin)
class PlainTextWrapper(QTextMixin):
    """A Qt class for use by the find code."""

    def __init__(self, widget: QWidget) -> None:
        """Ctor for the PlainTextWrapper class."""
        super().__init__()
        self.widget = widget


# @+node:ekr.20131007055150.17608: *4* LeoQtGui.insertKeyEvent
def insertKeyEvent(self, event: QEvent, i: int) -> None:
    """Insert the key given by event in location i of widget event.w."""
    assert isinstance(event, leoGui.LeoKeyEvent)
    qw = getattr(event.w, 'widget', None)
    if qw and isinstance(qw, QtWidgets.QTextEdit):
        if 1:
            # Assume that qevent.text() *is* the desired text.
            # This means we don't have to hack eventFilter.
            qw.insertPlainText(qevent.text())  ### qevent no longer exists.
        else:
            # Make no such assumption.
            # We would like to use qevent to insert the character,
            # but this would invoke eventFilter again!
            # So set this flag for eventFilter, which will
            # return False, indicating that the widget must handle
            # qevent, which *presumably* is the best that can be done.
            g.app.gui.insert_char_flag = True

# @+node:ekr.20260621054801.1: *3* Deleted by PR #4745
# @+node:ekr.20250329033642.2: *4* class IconBarAPI
class IconBarAPI:
    """The required API for c.frame.iconBar."""

    def __init__(self, c: Cmdr, parentFrame: Widget) -> None:
        pass

    def add(self, *args: str, **keys: str) -> None:
        pass

    def addRow(self, height: str = None) -> None:
        pass

    def addRowIfNeeded(self) -> None:
        pass

    def addWidget(self, w: QTextMixin) -> None:
        pass

    def clear(self) -> None:
        pass

    def createChaptersIcon(self) -> None:
        pass

    def deleteButton(self, w: QTextMixin) -> None:
        pass

    def getNewFrame(self) -> None:
        pass

    def setCommandForButton(
        self,
        button: QTextMixin,
        command: str,
        command_p: Position,
        controller: ScriptingController,
        gnx: str,
        script: str,
    ) -> None:
        pass


# @+node:ekr.20250329033642.3: *4* class StatusLineAPI
class StatusLineAPI:
    """The required API for c.frame.statusLine."""

    def __init__(self, c: Cmdr, parentFrame: Widget) -> None:
        pass

    def clear(self) -> None:
        pass

    def disable(self, background: str = None) -> None:
        pass

    def enable(self, background: str = "white") -> None:
        pass

    def get(self) -> str:
        return ''

    def isEnabled(self) -> bool:
        return False

    def put(self, s: str, bg: str = None, fg: str = None) -> None:
        pass

    def setFocus(self) -> None:
        pass

    def update(self) -> None:
        pass


# @+node:ekr.20260621062838.21: *4* class TestAPIClasses(LeoUnitTest)
class TestAPIClasses(LeoUnitTest):
    """Tests that gui classes are compatible with the corresponding API class."""

    @others


# @+node:ekr.20260621062838.22: *5* test_icon_bar_api
def test_icon_bar_api(self):
    def get_methods(cls):
        return [z for z in dir(cls) if not z.startswith('__')]

    def get_missing(cls):
        return [z for z in get_methods(IconBarAPI) if z not in get_methods(cls)]

    classes = [NullIconBarClass]
    if Qt:
        classes.append(QtIconBarClass)
    for cls in classes:
        self.assertFalse(get_missing(cls), msg=f"Missing {cls.__class__.__name__} methods")

# @+node:ekr.20260621062838.23: *5* test_status_line_api
def test_status_line_api(self):
    def get_methods(cls):
        return [z for z in dir(cls) if not z.startswith('__')]

    def get_missing(cls):
        return [z for z in get_methods(StatusLineAPI) if z not in get_methods(cls)]

    classes = [NullStatusLineClass]
    if Qt:
        classes.append(QtStatusLineClass)
    for cls in classes:
        self.assertFalse(get_missing(cls), msg=f"Missing {cls.__class__.__name__} methods")

# @+node:ekr.20260621062838.24: *5* test_tree_api
def test_tree_api(self):
    def get_methods(cls):
        return [z for z in dir(cls) if not z.startswith('__')]

    def get_missing(cls):
        return [z for z in get_methods(TreeAPI) if z not in get_methods(cls)]

    classes = [NullTree]
    if Qt:
        classes.extend([LeoQtTree, LeoTree])
    for cls in classes:
        self.assertFalse(get_missing(cls), msg=f"Missing {cls.__class__.__name__} methods")

# @+node:ekr.20260621062838.25: *5* test_text_api
def test_text_api(self):
    def get_methods(cls):
        return [z for z in dir(cls) if not z.startswith('__')]

    def get_missing(cls):
        return [z for z in get_methods(QTextMixin) if z not in get_methods(cls)]

    classes = [StringTextWrapper]
    if Qt:
        classes.extend([QLineEditWrapper, QTextEditWrapper, QScintillaWrapper])
    for cls in classes:
        missing = get_missing(cls)
        self.assertFalse(missing, msg=f"Missing {cls} methods: {missing}")

# @+node:ekr.20250329033642.4: *4* class TreeAPI
class TreeAPI:
    """The required API for c.frame.tree."""

    def __init__(self, frame: Widget) -> None:
        pass

    # Must be defined in subclasses.

    def editLabel(
        self,
        v: VNode,
        selectAll: bool = False,
        selection: tuple = None,
    ) -> tuple[Widget, QTextMixin]:
        return None, None

    def headline_wrapper(self, p: Position) -> None:
        return None

    def redraw_tree(self, p: Position = None) -> Position:
        pass

    redraw_now = redraw_tree

    def scrollTo(self, p: Position) -> None:
        pass

    # May be defined in subclasses.

    def initAfterLoad(self) -> None:
        pass

    def onHeadChanged(self, p: Position, undoType: str = 'Typing') -> None:
        pass

    # Hints for optimization. The proper default is c.redraw()

    def redraw_after_head_changed(self) -> None:
        pass

    # Must be defined in the LeoTree class...
    # def OnIconDoubleClick (self,p):

    def OnIconCtrlClick(self, p: Position) -> None:
        pass

    def endEditLabel(self) -> None:
        pass

    def getEditTextDict(self, v: VNode) -> None:
        return None

    def onHeadlineKey(self, event: LeoKeyEvent) -> None:
        pass

    def select(self, p: Position) -> None:
        pass

    def updateHead(self, event: LeoKeyEvent, w: QTextMixin) -> None:
        pass


# @+node:ekr.20031218072017.3680: *4* LeoFrame: Must be defined in subclasses
def bringToFront(self) -> None:
    raise NotImplementedError

def cascade(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def contractBodyPane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def contractLogPane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def contractOutlinePane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def contractPane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def deiconify(self) -> None:
    raise NotImplementedError

def equalSizedPanes(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def expandBodyPane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def expandLogPane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def expandOutlinePane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def expandPane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def fullyExpandBodyPane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def fullyExpandLogPane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def fullyExpandOutlinePane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def fullyExpandPane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def get_window_info(self) -> tuple[int, int, int, int]:
    raise NotImplementedError

def hideBodyPane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def hideLogPane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def hideLogWindow(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def hideOutlinePane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def hidePane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def leoHelp(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def lift(self) -> None:
    raise NotImplementedError

def minimizeAll(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def resizePanesToRatio(self, ratio: float, secondary_ratio: float) -> None:
    raise NotImplementedError

def resizeToScreen(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def setInitialWindowGeometry(self) -> None:
    raise NotImplementedError

def setTopGeometry(self, w: int, h: int, x: int, y: int) -> None:
    raise NotImplementedError

def toggleActivePane(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

def toggleSplitDirection(self, event: LeoKeyEvent = None) -> None:
    raise NotImplementedError

# @+node:ekr.20061109212618: *4* LeoGui: Must be defined in subclasses
# @+node:ekr.20031218072017.3725: *5* LeoGui.destroySelf
def destroySelf(self) -> None:
    raise NotImplementedError

# @+node:ekr.20031218072017.3730: *5* LeoGui.dialogs
def runAboutLeoDialog(
    self,
    c: Cmdr,
    version: str,
    theCopyright: str,
    url: str,
    email: str,
) -> Any:  # Must be any, for compatibility with testing subclass.
    """Create and run Leo's About Leo dialog."""
    raise NotImplementedError

def runAskOkDialog(self, c: Cmdr, title: str, message: str = None, text: str = "Ok") -> Any:
    """Create and run an askOK dialog ."""
    raise NotImplementedError

def runAskOkCancelNumberDialog(
    self,
    c: Cmdr,
    title: str,
    message: str,
    cancelButtonText: str = None,
    okButtonText: str = None,
) -> Any:
    """Create and run askOkCancelNumber dialog ."""
    raise NotImplementedError

def runAskOkCancelStringDialog(
    self,
    c: Cmdr,
    title: str,
    message: str,
    cancelButtonText: str = None,
    okButtonText: str = None,
    default: str = "",
    wide: bool = False,
) -> Any:
    """Create and run askOkCancelString dialog ."""
    raise NotImplementedError

def runAskYesNoDialog(
    self,
    c: Cmdr,
    title: str,
    message: str = None,
    yes_all: bool = False,
    no_all: bool = False,
) -> Any:
    """Create and run an askYesNo dialog."""
    raise NotImplementedError

def runAskYesNoCancelDialog(
    self,
    c: Cmdr,
    title: str,
    message: str = None,
    yesMessage: str = "Yes",
    noMessage: str = "No",
    yesToAllMessage: str = None,
    defaultButton: str = "Yes",
    cancelMessage: str = None,
) -> Any:
    """Create and run an askYesNoCancel dialog ."""
    raise NotImplementedError

def runPropertiesDialog(
    self,
    title: str = 'Properties',
    data: Any = None,
    callback: Callable = None,
    buttons: list[str] = None,
) -> Any:
    """Display a modal TkPropertiesDialog"""
    raise NotImplementedError

# @+node:ekr.20031218072017.3731: *5* LeoGui.file dialogs
def runOpenFileDialog(
    self,
    c: Cmdr,
    title: str,
    *,
    filetypes: list[tuple[str, str]],
    defaultextension: str = '',  # Not used
    startpath: str = None,
) -> str:
    """Create and run an open file dialog ."""
    raise NotImplementedError

def runOpenFilesDialog(
    self,
    c: Cmdr,
    title: str,
    *,
    filetypes: list[tuple[str, str]],
    defaultextension: str = '',  # Not used
    startpath: str = None,
) -> list[str]:
    """Create and run an open files dialog ."""
    raise NotImplementedError

def runSaveFileDialog(
    self,
    c: Cmdr,
    title: str,
    *,
    filetypes: list[tuple[str, str]],
    defaultextension: str = '',  # Not used
) -> str:
    """Create and run a save file dialog ."""
    raise NotImplementedError

# @+node:ekr.20031218072017.3732: *5* LeoGui.panels
def createComparePanel(self, c: Cmdr) -> None:
    """Create Compare panel."""
    raise NotImplementedError

def createFindTab(self, c: Cmdr, parentFrame: Widget) -> None:
    """Create a find tab in the indicated frame."""
    raise NotImplementedError

def createFontPanel(self, c: Cmdr) -> None:
    """Create a hidden Font panel."""
    raise NotImplementedError

def createLeoFrame(self, c: Cmdr, title: str) -> Widget:
    """Create a new Leo frame."""
    raise NotImplementedError

# @+node:ekr.20031218072017.3729: *5* LeoGui.runMainLoop
def runMainLoop(self) -> None:
    """Run the gui's main loop."""
    raise NotImplementedError

# @+node:ekr.20031218072017.3733: *5* LeoGui.utils
@ Subclasses are expected to subclass all of the following methods.
These are all do-nothing methods: callers are expected to check for
None returns.
The type of commander passed to methods depends on the type of frame
or dialog being created. The commander may be a Commands instance or
one of its subcommanders.
# @+node:ekr.20031218072017.3734: *6* LeoGui.Clipboard
def replaceClipboardWith(self, s: str) -> None:
    raise NotImplementedError

def getTextFromClipboard(self) -> str:
    raise NotImplementedError

# @+node:ekr.20031218072017.3735: *6* LeoGui.Dialog utils
def attachLeoIcon(self, window: Widget) -> None:
    """Attach the Leo icon to a window."""
    raise NotImplementedError

def center_dialog(self, dialog: str) -> None:
    """Center a dialog."""
    raise NotImplementedError

def create_labeled_frame(
    self,
    parent: str,
    caption: str = None,
    relief: str = "groove",
    bd: int = 2,
    padx: int = 0,
    pady: int = 0,
) -> None:
    """Create a labeled frame."""
    raise NotImplementedError

def get_window_info(self, window: str) -> tuple[int, int, int, int]:
    """Return the window information."""
    raise NotImplementedError

# @+node:ekr.20031218072017.3736: *6* LeoGui.Font
def getFontFromParams(
    self,
    family: str,
    size: str,
    slant: str,
    weight: str,
    defaultSize: int = 12,
    tag: str = '',
) -> Any:
    raise NotImplementedError

# @+node:ekr.20070212145124: *6* LeoGui.getFullVersion
def getFullVersion(self, c: Cmdr = None) -> str:
    return 'LeoGui: dummy version'

# @+node:ekr.20070212070820: *6* LeoGui.makeScriptButton
def makeScriptButton(
    self,
    c: Cmdr,
    args: str = None,
    p: Position = None,
    script: str = None,
    buttonText: str = None,
    balloonText: str = 'Script Button',
    shortcut: str = None,
    bg: str = 'LightSteelBlue1',
    define_g: bool = True,
    define_name: str = '__main__',
    silent: bool = False,
) -> None:
    raise NotImplementedError

# @+node:ekr.20031218072017.3706: *4* LeoTree: Must be defined in subclasses
# Drawing & scrolling.

def redraw_tree(self, p: Position = None) -> Position:
    raise NotImplementedError

redraw_now = redraw_tree

def scrollTo(self, p: Position) -> None:
    raise NotImplementedError

# Headlines.

def editLabel(
    self,
    p: Position,
    selectAll: bool = False,
    selection: tuple = None,
) -> tuple[Widget, Any]:  # Any is the best possible annotation.
    raise NotImplementedError

def headline_wrapper(self, p: Position) -> Widget:
    raise NotImplementedError

# @+node:ekr.20260707105715.1: *3* @@@button clone-callers (abandoned)
g.cls()

import re

h = '--- signatures return str'
p = g.findNodeAnywhere(c, h)
u = c.undoer
assert p, h

@others  # Define helpers

# Make root node
undoData = u.beforeInsertNode(c.p)  # c.p is subject of 'insertAfter'
root = c.lastTopLevel().insertAfter()
root.h = 'Callers'
# Discover all names.
names = set()
for p in p.children():
    if p.h.startswith('class '):
        continue
    s = g.splitLines(p.b)[0]
    if m := re.match(r'^def ([_\w]+)', s):
        names.add(m.group(1))
        # find_clones(c, m.group(1))
    else:
        print(f"Skip: {p.h}")
# Do cff for all names:
for name in sorted(names):
    find_clones(c, name)
    
# Finish
c.selectPosition(root)
c.demote()
u.afterInsertNode(root, 'clone-callers', undoData)
c.redraw(root)
# @+node:ekr.20260707111229.1: *4* function: find_clones
def find_clones(c, name):
    """
    do clone-find-all for name(...)
    """
    finder = c.findCommands
    # find_text = rf'\.{name}\(.*?\)'
    find_text = f"[ .]{name}\(.*?\)"
    print(f"cff: {name}")
    settings = g.Bunch(
        # State...
        in_headline     = False,
        reverse         = False,
        # Find/change strings...
        find_text       = find_text,
        change_text     = '',
        # Find options...
        file_only       = False,
        ignore_case     = False,
        mark_changes    = False,
        mark_finds      = False,
        node_only       = False,
        pattern_match   = True,
        search_body     = True,
        search_headline = False,
        suboutline_only = False,
        whole_word      = False
    )  # fmt: skip
    finder.do_clone_find_all_flattened(settings)
    # for p in c.all_unique_positions():
    #     for s in g.splitLines(p.b):
    #         if m := re.match(rf'^.*?\b{name}\(', s):
    #             if s.strip().startswith('def'):
    #                 continue
    #             else:
    #                 where_s = f"{g.truncate(p.h, 30):>40} {name:>30}"
    #                 print(f"{where_s:<70} {g.truncate(s.strip(), 80)}")
    #                 break
# @-all
# @@nosearch
# @-leo
