# @+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.20260805111552.1: *3* 'insert-qt-icon' & helper
@cmd('insert-qt-icon')
def insertQtIcon(self, event: LeoKeyEvent | None = None) -> None:
    """
    Show a window previewing all standard Qt icons.
    Add each selected icon to c.p.h.
    """
   
    controller = self
    d = g.app.scriptDict
    key = 'insert_qt_icon_window'
    insert_qt_icon_window = d.get(key)
    if not insert_qt_icon_window:
        << define class QtPreviewWindow >>
        d[key] = insert_qt_icon_window = QtPreviewWindow()
    insert_qt_icon_window.show()

# @+node:ekr.20260805111828.1: *4* << define class QtPreviewWindow >>
from PyQt6.QtWidgets import (  # pylint: disable=no-name-in-module
    QGridLayout,
    QPushButton,
    QStyle,
    QWidget,
)

# Adapted from https://gist.github.com/ostr00000/30c9e732550baa0c13a73fd3320e7d55
class QtPreviewWindow(QWidget):
    def __init__(self):
        super().__init__()
        icon_names = sorted(
            [z for z in dir(QStyle.StandardPixmap) if z.startswith("SP_")]
        )
        layout = QGridLayout()
        for i, icon_name in enumerate(icon_names):
            button = QPushButton(icon_name)
            pixmap = getattr(QStyle.StandardPixmap, icon_name)
            if pixmap is None:
                g.trace(f"No pixmap for {icon_name=}")
                continue

            def callback(
                icon_name: str = icon_name, pixmap: QStyle.StandardPixmap = pixmap
            ) -> None:
                controller._addPixmapToNode(icon_name, pixmap)

            button.released.connect(callback)
            styled_icon = self.style().standardIcon(pixmap)  # type:ignore
            button.setIcon(styled_icon)
            layout.addWidget(button, int(i / 4), int(i % 4))
        self.setLayout(layout)

# @+node:ekr.20260805114313.1: *4* ec._addPixmapToNode (to do)
def _addPixmapToNode(self, icon_name: str, pixmap: QStyle.StandardPixmap) -> None:
    """Convert the given pixmap to an icon and add it to c.p.h."""
    c = self.c
    assert isinstance(pixmap, QStyle.StandardPixmap)
    g.trace(icon_name)

    # Convert the pixmap to a QIcon?.

    # iconList = self.getIconList(p.v)
    # iconList.extend(aList)
    # self.setIconList(p, iconList)

    c.p.setDirty()
    c.setChanged()
    c.redraw()

# @+node:ekr.20120419093256.10048: *3* ../plugins/free_layout.py
<< free_layout docstring >>
# Written by Terry Brown.
<< free_layout imports >>
<< free_layout annotations >>
@others
@language python
@tabwidth -4
@pagewidth 70
# @+node:ekr.20110319161401.14467: *4* << free_layout docstring >>
"""
Free layout
===========

Adds flexible panel layout through context menus on the handles between panels.

Uses NestedSplitter, a more intelligent QSplitter, from leo.plugins.nested_splitter

Requires Qt.

Commands (bindable with @settings-->@keys-->@shortcuts):

free-layout-load
    Open context menu for loading a different layout,
    convenient keyboard shortcut target.
free-layout-restore
    Use the layout this outline had when it was opened.
free-layout-zoom
    Zoom or unzoom the current pane

"""

# @+node:tbrown.20110203111907.5520: *4* << free_layout imports >>
from __future__ import annotations
import json
from typing import Any, TYPE_CHECKING
from leo.core import leoGlobals as g

#
# Qt imports. May fail from the bridge.
try:  # #1973
    from leo.core.leoQt import QtWidgets
    from leo.core.leoQt import MouseButton
    from leo.plugins.nested_splitter import NestedSplitter  # NestedSplitterChoice
except Exception:
    QtWidgets = None  # type:ignore
    MouseButton = None  # type:ignore
    NestedSplitter = None

# Do not call g.assertUi('qt') here. It's too early in the load process.
# @+node:ekr.20220828125201.1: *4* << free_layout annotations >>
if TYPE_CHECKING:  # pragma: no cover
    from leo.core.leoCommands import Commands as Cmdr
    from leo.core.leoGui import LeoKeyEvent
    from leo.core.leoNodes import Position
    from leo.plugins.qt_text import QTextMixin

    QSplitter = QtWidgets.QSplitter
    QWidget = QtWidgets.QWidget


# @+node:tbrown.20110203111907.5521: *4* free_layout:init
def init() -> bool:
    """Return True if the free_layout plugin can be loaded."""
    # #3910: The free_layout and nested_splitter plugins are deprecated.
    return bool(g.app.gui.guiName() == "qt" and NestedSplitter)


# @+node:ekr.20110318080425.14389: *4* class FreeLayoutController
class FreeLayoutController:
    << FreeLayoutController docstring >>
    << define default_layout >>

    @others


# @+node:ekr.20201013042712.1: *5* << FreeLayoutController docstring >>
"""Glue between Leo and the NestedSplitter gui widget.  All Leo aware
code should be in here, none in NestedSplitter.

*ALSO* implements the provider interface for NestedSplitter, in
ns_provides, ns_provide, ns_context, ns_do_context, which
NestedSplitter uses as callbacks to populate splitter-handle context-menu
and the empty pane Action button menu:

see nested_splitter.py-->class%20NestedSplitter%20(QSplitter)-->register_provider

ns_provides
  tell NestedSplitter which Action button items we can provide
ns_provide
  provide the advertised service when an Action button item we
  advertised is selected
ns_context
  tell NestedSplitter which splitter-handle context-menu items
  we can provide
ns_do_context
  provide the advertised service when a splitter-handle context-menu
  item we advertised is selected
"""

# @+node:ekr.20201013042741.1: *5* << define default_layout >>
default_layout = {
    'content': [
        {
            'content': [
                '_leo_pane:outlineFrame',
                '_leo_pane:logFrame',
            ],
            'orientation': 1,
            'sizes': [509, 275],
        },
        '_leo_pane:bodyFrame',
    ],
    'orientation': 2,
    'sizes': [216, 216],
}
# @+node:ekr.20110318080425.14390: *5*  flc.ctor
def __init__(self, c: Cmdr) -> None:
    """Ctor for FreeLayoutController class."""
    self.c = c
    g.registerHandler('after-create-leo-frame', self.init)
    # Plugins must be loaded first to provide their widgets in panels etc.
    g.registerHandler('after-create-leo-frame2', self.loadLayouts)

# @+node:tbrown.20110203111907.5522: *5*  flc.init
def init(self, tag: str, keys: dict) -> None:
    """Attach to an outline and

    - add tags to widgets to indicate that they're essential
      (tree, body, log-window-tabs) and

    - tag the log-window-tabs widget as the place to put widgets
      from free-layout panes which are closed

    - register this FreeLayoutController as a provider of menu items
      for NestedSplitter
    """
    c = self.c
    if not NestedSplitter:
        return
    if c != keys.get('c'):
        return
    # Careful: we could be unit testing.
    splitter = self.get_top_splitter()  # A NestedSplitter.
    if not splitter:
        return
    # by default NestedSplitter's context menus are disabled, needed
    # once to globally enable them
    NestedSplitter.enabled = True
    # when NestedSplitter disposes of children, it will either close
    # them, or move them to another designated widget.  Here we set
    # up two designated widgets
    logTabWidget = splitter.findChild(QtWidgets.QWidget, "logTabWidget")
    splitter.root.holders['_is_from_tab'] = logTabWidget
    splitter.root.holders['_is_permanent'] = 'TOP'
    # allow body and tree widgets to be "removed" to tabs on the log tab panel
    bodyWidget = splitter.findChild(QtWidgets.QFrame, "bodyFrame")
    bodyWidget._is_from_tab = "Body"
    treeWidget = splitter.findChild(QtWidgets.QFrame, "outlineFrame")
    treeWidget._is_from_tab = "Tree"
    # also the other tabs will have _is_from_tab set on them by the
    # offer_tabs menu callback above
    # if the log tab panel is removed, move it back to the top splitter
    logWidget = splitter.findChild(QtWidgets.QFrame, "logFrame")
    logWidget._is_permanent = True
    # tag core Leo components (see ns_provides)
    splitter.findChild(QtWidgets.QWidget, "outlineFrame")._ns_id = '_leo_pane:outlineFrame'
    splitter.findChild(QtWidgets.QWidget, "logFrame")._ns_id = '_leo_pane:logFrame'
    splitter.findChild(QtWidgets.QWidget, "bodyFrame")._ns_id = '_leo_pane:bodyFrame'
    splitter.register_provider(self)
    splitter.splitterClicked_connect(self.splitter_clicked)

# @+node:tbrown.20120119080604.22982: *5* flc.embed
def embed(self) -> None:
    """called from ns_do_context - embed layout in outline's
    @settings, an alternative to the Load/Save named layout system
    """
    # Careful: we could be unit testing.
    top_splitter = self.get_top_splitter()
    if not top_splitter:
        return
    c = self.c
    layout = top_splitter.get_saveable_layout()
    nd = g.findNodeAnywhere(c, "@data free-layout-layout")
    if not nd:
        settings: Position | None
        settings = g.findNodeAnywhere(c, "@settings")
        if not settings:
            settings = c.rootPosition().insertAfter()
            settings.h = "@settings"
        nd = settings.insertAsNthChild(0)
    nd.h = "@data free-layout-layout"
    nd.b = json.dumps(layout, indent=4)
    nd = nd.parent()
    if not nd or nd.h != "@settings":
        g.es("WARNING: @data free-layout-layout node is not under an active @settings node")
    c.redraw()

# @+node:ekr.20160424035257.1: *5* flc.get_main_splitter
def get_main_splitter(self, w: QTextMixin | None = None) -> NestedSplitter | None:
    """
    Return the main splitter.

    We tacitly assume that this splitter contains the body pane.
    """
    top = self.get_top_splitter()
    return top if top.objectName() == 'main_splitter' else None

# @+node:ekr.20160424035254.1: *5* flc.get_secondary_splitter
def get_secondary_splitter(self) -> NestedSplitter | None:
    """
    Return the secondary splitter, that is, the splitter containing the outline pane.
    """
    top = self.get_top_splitter()
    w = top and top.find_child(QtWidgets.QWidget, 'outlineFrame')
    while w:
        w = w.parent()
        if NestedSplitter is not None and isinstance(w, NestedSplitter):
            return w
    return None

# @+node:tbrown.20110621120042.22914: *5* flc.get_top_splitter
def get_top_splitter(self) -> NestedSplitter | None:
    """Return the top splitter of c.frame.top."""
    f = self.c.frame
    if hasattr(f, 'top') and f.top:
        child = f.top.findChild(NestedSplitter)
        return child and child.top()
    return None

# @+node:ekr.20120419095424.9927: *5* flc.loadLayouts (sets wrap=True)
def loadLayouts(self, tag: str, keys: dict, reloading: bool = False) -> None:
    """loadLayouts - Load the outline's layout

    :Parameters:
    - `tag`: from hook event
    - `keys`: from hook event
    - `reloading`: True if this is not the initial load, see below

    When called from the `after-create-leo-frame2` hook this defaults
    to False.  When called from the `restore-layout` command, this is set
    True, and the layout the outline had *when first loaded* is restored.
    Useful if you want to temporarily switch to a different layout and then
    back, without having to remember the original layouts name.
    """
    trace = 'layouts' in g.app.debug
    c = self.c
    if not (g.app and g.app.db):
        return  # Can happen when running from the Leo bridge.
    if c != keys.get('c'):
        return
    d = g.app.db.get('ns_layouts') or {}
    if trace:
        g.trace(tag)
        g.printObj(keys, tag="keys")
    if layout := c.config.getData("free-layout-layout"):
        layout = json.loads('\n'.join(layout))
    if name := c.db.get('_ns_layout'):
        if reloading:
            name = c.free_layout.original_layout
            c.db['_ns_layout'] = name
        else:
            c.free_layout.original_layout = name
        if layout:
            g.es(
                "NOTE: embedded layout in @settings/@data free-layout-layout overrides saved layout "
                + name
            )
        else:
            layout = d.get(name)
    # EKR: Create commands that will load each layout.
    if d:
        for name in sorted(d.keys()):

            def func(event: LeoKeyEvent) -> None:
                if layout := d.get(name):  # noqa
                    c.free_layout.get_top_splitter().load_layout(c, layout)
                else:
                    g.trace('no layout', name)  # noqa

            name_s = name.strip().lower().replace(' ', '-')
            commandName = f"free-layout-load-{name_s}"
            c.k.registerCommand(commandName, func)
    # Careful: we could be unit testing or in the Leo bridge.
    if layout:
        if splitter := c.free_layout.get_top_splitter():
            splitter.load_layout(c, layout)

# @+node:tbrown.20110628083641.11730: *5* flc.ns_context
def ns_context(self) -> list[tuple[str, str]]:
    ans: list[Any] = [
        ('Embed layout', '_fl_embed_layout'),
        ('Save layout', '_fl_save_layout'),
    ]
    if d := g.app.db.get('ns_layouts', {}):
        ans.append({'Load layout': [(k, '_fl_load_layout:' + k) for k in d]})
        ans.append({'Delete layout': [(k, '_fl_delete_layout:' + k) for k in d]})
        ans.append(('Forget layout', '_fl_forget_layout:'))
        ans.append(('Restore initial layout', '_fl_restore_layout:'))
    ans.append(('Restore default layout', '_fl_restore_default:'))
    ans.append(('Help for this menu', '_fl_help:'))
    return ans

# @+node:tbrown.20110628083641.11732: *5* flc.ns_do_context
def ns_do_context(self, id_: QWidget, splitter: QSplitter, index: int) -> bool:
    c = self.c
    if id_.startswith('_fl_embed_layout'):
        self.embed()
        return True
    if id_.startswith('_fl_restore_default'):
        self.get_top_splitter().load_layout(c, layout=self.default_layout)
    if id_.startswith('_fl_help'):
        self.c.putHelpFor(__doc__)
        # g.handleUrl("https://leo-editor.github.io/leo-editor/")
        return True
    if id_ == '_fl_save_layout':
        if self.c.config.getData("free-layout-layout"):
            g.es("WARNING: embedded layout in")
            g.es("@settings/@data free-layout-layout")
            g.es("will override saved layout")
        layout = self.get_top_splitter().get_saveable_layout()
        name = g.app.gui.runAskOkCancelStringDialog(
            self.c,
            title="Save layout",
            message="Name for layout?",
        )
        if name:
            self.c.db['_ns_layout'] = name
            d = g.app.db.get('ns_layouts', {})
            d[name] = layout
            # make sure g.app.db's __set_item__ is hit so it knows to save
            g.app.db['ns_layouts'] = d
        return True
    if id_.startswith('_fl_load_layout:'):
        if self.c.config.getData("free-layout-layout"):
            g.es("WARNING: embedded layout in")
            g.es("@settings/@data free-layout-layout")
            g.es("will override saved layout")
        name = id_.split(':', 1)[1]
        self.c.db['_ns_layout'] = name
        layout = g.app.db['ns_layouts'][name]
        self.get_top_splitter().load_layout(c, layout)
        return True
    if id_.startswith('_fl_delete_layout:'):
        name = id_.split(':', 1)[1]
        if 'yes' == g.app.gui.runAskYesNoCancelDialog(
            c, "Really delete Layout?", f"Really permanently delete the layout '{name}'?"
        ):
            d = g.app.db.get('ns_layouts', {})
            del d[name]
            # make sure g.app.db's __set_item__ is hit so it knows to save
            g.app.db['ns_layouts'] = d
            if '_ns_layout' in self.c.db:
                del self.c.db['_ns_layout']
        return True
    if id_.startswith('_fl_forget_layout:'):
        if '_ns_layout' in self.c.db:
            del self.c.db['_ns_layout']
        return True
    if id_.startswith('_fl_restore_layout:'):
        self.loadLayouts("reload", {'c': self.c}, reloading=True)
        return True
    return False

# @+node:tbrown.20110628083641.11724: *5* flc.ns_provide
def ns_provide(self, id_: str) -> str | QTextMixin | None:
    if id_.startswith('_leo_tab:'):
        id_ = id_.split(':', 1)[1]
        top = self.get_top_splitter()
        logTabWidget = top.find_child(QtWidgets.QWidget, "logTabWidget")
        for n in range(logTabWidget.count()):
            if logTabWidget.tabText(n) == id_:
                w = logTabWidget.widget(n)
                w.setHidden(False)
                w._is_from_tab = logTabWidget.tabText(n)
                w.setMinimumSize(20, 20)
                return w
        # didn't find it, maybe it's already in a splitter
        return 'USE_EXISTING'
    if id_.startswith('_leo_pane:'):
        id_ = id_.split(':', 1)[1]
        if w := self.get_top_splitter().find_child(QtWidgets.QWidget, id_):
            w.setHidden(False)  # may be from Tab holder
            w.setMinimumSize(20, 20)
        return w
    return None

# @+node:tbrown.20110627201141.11745: *5* flc.ns_provides
def ns_provides(self) -> list[tuple[str, str]]:
    ans: list[tuple[str, str]] = []
    # list of things in tab widget
    logTabWidget = self.get_top_splitter().find_child(QtWidgets.QWidget, "logTabWidget")
    for n in range(logTabWidget.count()):
        text = str(logTabWidget.tabText(n))
        if text in ('Body', 'Tree'):
            continue  # handled below
        if text == 'Log':
            # if Leo can't find Log in tab pane, it creates another
            continue
        ans.append((text, '_leo_tab:' + text))
    ans.append(('Tree', '_leo_pane:outlineFrame'))
    ans.append(('Body', '_leo_pane:bodyFrame'))
    ans.append(('Tab pane', '_leo_pane:logFrame'))
    return ans

# @+node:tbnorth.20160510122413.1: *5* flc.splitter_clicked
def splitter_clicked(
    self,
    splitter: QSplitter,
    handle: QSplitter,
    event: LeoKeyEvent | None,
    release: str,
    double: bool,
) -> None:
    """
    splitter_clicked - middle click release will zoom adjacent
    body / tree panes

    :param NestedSplitter splitter: splitter containing clicked handle
    :param NestedSplitterHandle handle: clicked handle
    :param QMouseEvent event: mouse event for click
    :param bool release: was it a Press or Release event
    :param bool double: was it a double click event
    """
    if not event:
        return
    if not release or event.button() != MouseButton.MiddleButton:
        return
    if splitter.root.zoomed:  # unzoom if *any* handle clicked
        splitter.zoom_toggle()
        return
    before = splitter.widget(splitter.indexOf(handle) - 1)
    after = splitter.widget(splitter.indexOf(handle))
    for pane in before, after:
        if pane.objectName() == 'bodyFrame':
            pane.setFocus()
            splitter.zoom_toggle()
            return
        if pane.objectName() == 'outlineFrame':
            pane.setFocus()
            splitter.zoom_toggle(local=True)
            return

# @+node:ekr.20160416065221.1: *4* commands: free_layout.py
# @+node:tbrown.20140524112944.32658: *5* @g.command free-layout-context-menu
@g.command('free-layout-context-menu')
def free_layout_context_menu(event: LeoKeyEvent | None = None) -> None:
    """
    Open free layout's context menu, using the first divider of the top
    splitter for context.
    """
    c = event.get('c') if event else None
    if not c:
        return
    splitter = c.free_layout.get_top_splitter()
    handle = splitter.handle(1)
    handle.splitter_menu(handle.rect().topLeft())


# @+node:tbrown.20130403081644.25265: *5* @g.command free-layout-restore
@g.command('free-layout-restore')
def free_layout_restore(event: LeoKeyEvent | None = None) -> None:
    """
    Restore layout outline had when it was loaded.
    """
    if c := event.get('c') if event else None:
        c.free_layout.loadLayouts('reload', {'c': c}, reloading=True)


# @+node:tbrown.20131111194858.29876: *5* @g.command free-layout-load
@g.command('free-layout-load')
def free_layout_load(event: LeoKeyEvent | None = None) -> None:
    """Load layout from menu."""
    c = event.get('c') if event else None
    if not c:
        return
    d = g.app.db.get('ns_layouts', {})
    menu = QtWidgets.QMenu(c.frame.top)
    for k in d:
        menu.addAction(k)
    pos = c.frame.top.window().frameGeometry().center()
    action = menu.exec(pos)
    if action is None:
        return
    name = str(action.text())
    c.db['_ns_layout'] = name
    # layout = g.app.db['ns_layouts'][name]
    layouts = g.app.db.get('ns_layouts', {})
    if layout := layouts.get(name):
        c.free_layout.get_top_splitter().load_layout(c, layout)


# @+node:tbrown.20140522153032.32658: *5* @g.command free-layout-zoom
@g.command('free-layout-zoom')
def free_layout_zoom(event: LeoKeyEvent | None = None) -> None:
    """(un)zoom the current pane."""
    if c := event.get('c') if event else None:
        c.free_layout.get_top_splitter().zoom_toggle()


# @+node:ekr.20160327060009.1: *5* free_layout:register_provider
def register_provider(c: Cmdr, provider_instance: object) -> None:
    """Register the provider instance with the top splitter."""
    if getattr(c, 'free_layout', None):
        if splitter := c.free_layout.get_top_splitter():
            splitter.register_provider(provider_instance)


# @+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.20110605121601.17954: *3* ../plugins/nested_splitter.py
"""Nested splitter classes."""

from __future__ import annotations
from typing import Any, cast
from leo.core import leoGlobals as g
from leo.core.leoQt import QtCore, QtGui, QtWidgets
from leo.core.leoQt import ContextMenuPolicy, Orientation, QAction


@others
@language python
@tabwidth -4
@pagewidth 70
# @+node:ekr.20110605121601.17956: *4* init
def init():
    # Allow this to be imported as a plugin,
    # but it should never be necessary to do so.
    return True


# @+node:tbrown.20120418121002.25711: *4* class NestedSplitterTopLevel (QWidget)
class NestedSplitterTopLevel(QtWidgets.QWidget):
    """A QWidget to wrap a NestedSplitter to allow it to live in a top
    level window and handle close events properly.

    These windows are opened by the splitter handle context-menu item
    'Open Window'.

    The NestedSplitter itself can't be the top-level widget/window,
    because it assumes it can wrap itself in another NestedSplitter
    when the user wants to "Add Above/Below/Left/Right".  I.e. wrap
    a vertical nested splitter in a horizontal nested splitter, or
    visa versa.  Parent->SplitterOne becomes Parent->SplitterTwo->SplitterOne,
    where parent is either Leo's main window's QWidget 'centralwidget',
    or one of these NestedSplitterTopLevel "window frames".
    """

    @others


# @+node:tbrown.20120418121002.25713: *5* __init__
def __init__(self, *args, **kargs):
    """Init. taking note of the FreeLayoutController which owns this"""
    self.owner = kargs['owner']
    del kargs['owner']
    window_title = kargs.get('window_title')
    del kargs['window_title']
    super().__init__(*args, **kargs)
    if window_title:
        self.setWindowTitle(window_title)

# @+node:tbrown.20120418121002.25714: *5* closeEvent (NestedSplitterTopLevel)
def closeEvent(self, event: QtGui.QCloseEvent) -> None:
    """A top-level NestedSplitter window has been closed, check all the
    panes for widgets which must be preserved, and move any found
    back into the main splitter."""
    widget = self.findChild(NestedSplitter)
    # top level NestedSplitter in window being closed
    other_top = self.owner.top()
    # top level NestedSplitter in main splitter
    # adapted from NestedSplitter.remove()
    count = widget.count()
    all_ok = True
    to_close = []
    # get list of widgets to close so index based access isn't
    # derailed by closing widgets in the same loop
    for splitter in widget.self_and_descendants():
        for i in range(splitter.count() - 1, -1, -1):
            to_close.append(splitter.widget(i))
    for w in to_close:
        all_ok &= widget.close_or_keep(w, other_top=other_top) is not False
    # it should always be ok to close the window, because it should always
    # be possible to move widgets which must be preserved back to the
    # main splitter, but if not, keep this window open
    if all_ok or count <= 0:
        self.owner.closing(self)
    else:
        event.ignore()

# @+node:ekr.20110605121601.17959: *4* class NestedSplitterChoice (QWidget)
class NestedSplitterChoice(QtWidgets.QWidget):
    """When a new pane is opened in a nested splitter layout, this widget
    presents a button, labeled 'Action', which provides a popup menu
    for the user to select what to do in the new pane"""

    @others


# @+node:ekr.20110605121601.17960: *5* __init__ (NestedSplitterChoice)
def __init__(self, parent=None):
    """ctor for NestedSplitterChoice class."""
    super().__init__(parent)
    self.setLayout(QtWidgets.QVBoxLayout())
    button = QtWidgets.QPushButton("Action", self)  # EKR: 2011/03/15
    self.layout().addWidget(button)
    button.setContextMenuPolicy(ContextMenuPolicy.CustomContextMenu)
    button.customContextMenuRequested.connect(
        lambda pnt: self.parent().choice_menu(self, button.mapToParent(pnt))
    )
    button.clicked.connect(lambda: self.parent().choice_menu(self, button.pos()))

# @+node:ekr.20110605121601.17961: *4* class NestedSplitterHandle (QSplitterHandle)
class NestedSplitterHandle(QtWidgets.QSplitterHandle):
    """Show the context menu on a NestedSplitter splitter-handle to access
    NestedSplitter's special features"""

    @others


# @+node:ekr.20110605121601.17962: *5* nsh.__init__
def __init__(self, owner):
    """Ctor for NestedSplitterHandle class."""
    super().__init__(owner.orientation(), owner)
    # Confusing!
    # self.setStyleSheet("background-color: green;")
    self.setContextMenuPolicy(ContextMenuPolicy.CustomContextMenu)
    self.customContextMenuRequested.connect(self.splitter_menu)

# @+node:ekr.20110605121601.17963: *5* nsh.__repr__
def __repr__(self):
    return f"(NestedSplitterHandle) at: {id(self)}"

__str__ = __repr__

# @+node:ekr.20110605121601.17964: *5* nsh.add_item
def add_item(self, func, menu, name, tooltip=None):
    """helper for splitter_menu menu building"""
    act = QAction(name, self)
    act.setObjectName(name.lower().replace(' ', '-'))
    act.triggered.connect(lambda checked: func())
    if tooltip:
        act.setToolTip(tooltip)
    menu.addAction(act)

# @+node:tbrown.20131130134908.27340: *5* nsh.show_tip
def show_tip(self, action):
    """show_tip - show a tooltip, calculate the box in which
    the pointer must stay for the tip to remain visible

    :Parameters:
    - `self`: this handle
    - `action`: action triggering event to display
    """
    if action.toolTip() == action.text():
        tip = ""
    else:
        tip = action.toolTip()
    pos = QtGui.QCursor.pos()
    x = pos.x()
    y = pos.y()
    rect = QtCore.QRect(x - 5, y - 5, x + 5, y + 5)
    if hasattr(action, 'parentWidget'):  # 2021/07/17.
        parent = action.parentWidget()
    else:
        return
    if not parent:
        g.trace('===== no parent =====')
        return
    QtWidgets.QToolTip.showText(pos, tip, parent, rect)

# @+node:ekr.20110605121601.17965: *5* nsh.splitter_menu
def splitter_menu(self, pos):
    """build the context menu for NestedSplitter"""
    splitter = self.splitter()
    if not splitter.enabled:
        g.trace('splitter not enabled')
        return
    index = splitter.indexOf(self)
    # get three pairs
    widget, neighbour, count = splitter.handle_context(index)
    lr = 'Left', 'Right'
    ab = 'Above', 'Below'
    split_dir = 'Vertically'
    if self.orientation() == Orientation.Vertical:
        lr, ab = ab, lr
        split_dir = 'Horizontally'
    # blue/orange - color-blind friendly
    color = '#729fcf', '#f57900'
    sheet = []
    for i in 0, 1:
        sheet.append(widget[i].styleSheet())
        widget[i].setStyleSheet(sheet[-1] + f"\nborder: 2px solid {color[i]};")
    menu = QtWidgets.QMenu()
    menu.hovered.connect(self.show_tip)

    def pl(n):
        return 's' if n > 1 else ''

    def di(s):
        return {
            'Above': 'above',
            'Below': 'below',
            'Left': 'left of',
            'Right': 'right of',
        }[s]

    # Insert.

    def insert_callback(index=index):
        splitter.insert(index)

    self.add_item(insert_callback, menu, 'Insert', "Insert an empty pane here")
    # Remove, +0/-1 reversed, we need to test the one that remains
    # First see if a parent has more than two splits
    # (we could be a sole surviving child).
    max_parent_splits = 0
    up = splitter.parent()
    while isinstance(up, NestedSplitter):
        max_parent_splits = max(max_parent_splits, up.count())
        up = up.parent()
        if max_parent_splits >= 2:
            break  # two is enough
    for i in 0, 1:
        # keep = splitter.widget(index)
        # cull = splitter.widget(index - 1)
        if (
            max_parent_splits >= 2  # more splits upstream
            or splitter.count() > 2  # 3+ splits here, or 2+ downstream
            or neighbour[not i]
            and neighbour[not i].max_count() >= 2
        ):

            def remove_callback(i=i, index=index):
                splitter.remove(index, i)

            self.add_item(
                remove_callback,
                menu,
                f"Remove {count[i]:d} {lr[i]}",
                f"Remove the {count[i]} pane{pl(count[i])} {di(lr[i])} here",
            )
    # Swap.

    def swap_callback(index=index):
        splitter.swap(index)

    self.add_item(
        swap_callback,
        menu,
        f"Swap {count[0]:d} {lr[0]} {count[1]:d} {lr[1]}",
        f"Swap the {count[0]:d} pane{pl(count[0])} {di(lr[0])} here "
        f"with the {count[1]:d} pane{pl(count[1])} {di(lr[1])} here",
    )
    # Split: only if not already split.
    for i in 0, 1:
        if not neighbour[i] or neighbour[i].count() == 1:

            def split_callback(i=i, index=index, splitter=splitter):
                splitter.split(index, i)

            self.add_item(split_callback, menu, f"Split {lr[i]} {split_dir}")
    for i in 0, 1:

        def mark_callback(i=i, index=index):
            splitter.mark(index, i)

        self.add_item(mark_callback, menu, f"Mark {count[i]:d} {lr[i]}")
    # Swap With Marked.
    if splitter.root.marked:
        for i in 0, 1:
            if not splitter.invalid_swap(widget[i], splitter.root.marked[2]):

                def swap_mark_callback(i=i, index=index, splitter=splitter):
                    splitter.swap_with_marked(index, i)

                self.add_item(
                    swap_mark_callback, menu, f"Swap {count[i]:d} {lr[i]} With Marked"
                )
    # Add.
    for i in 0, 1:
        if (
            not isinstance(splitter.parent(), NestedSplitter)
            or splitter.parent().indexOf(splitter) == [0, splitter.parent().count() - 1][i]
        ):

            def add_callback(i=i, splitter=splitter):
                splitter.add(i)

            self.add_item(add_callback, menu, f"Add {ab[i]}")
    # Rotate All.
    self.add_item(splitter.rotate, menu, 'Toggle split direction')

    def rotate_only_this(index=index):
        splitter.rotateOne(index)

    self.add_item(rotate_only_this, menu, 'Toggle split/dir. just this')
    # equalize panes

    def eq(splitter=splitter.top()):
        splitter.equalize_sizes(recurse=True)

    self.add_item(eq, menu, 'Equalize all')
    # (un)zoom pane

    def zoom(splitter=splitter.top()):
        splitter.zoom_toggle()

    self.add_item(zoom, menu, ('Un' if splitter.root.zoomed else '') + 'Zoom pane')
    # open window
    if splitter.top().parent().__class__ != NestedSplitterTopLevel:
        # don't open windows from windows, only from main splitter
        # so owner is not a window which might close.  Could instead
        # set owner to main splitter explicitly.  Not sure how right now.
        submenu = menu.addMenu('Open window')
        if 1:
            self.add_item(lambda: splitter.open_window(), submenu, "Empty")
        # adapted from choice_menu()
        if splitter.root.marked and splitter.top().max_count() > 1:
            self.add_item(
                lambda: splitter.open_window(action="_move_marked_there"),
                submenu,
                "Move marked there",
            )
        for provider in splitter.root.providers:
            if hasattr(provider, 'ns_provides'):
                for title, id_ in provider.ns_provides():

                    def cb(id_=id_):
                        splitter.open_window(action=id_)

                    self.add_item(cb, submenu, title)
    submenu = menu.addMenu('Debug')
    act = QAction("Print splitter layout", self)

    def print_layout_c(checked, splitter=splitter):
        layout = splitter.top().get_layout()
        g.printObj(layout)

    act.triggered.connect(print_layout_c)
    submenu.addAction(act)

    def load_items(menu, items):
        for i in items:
            if isinstance(i, dict):
                for k in i:
                    load_items(menu.addMenu(k), i[k])
            else:
                title, id_ = i

                def cb(checked, id_=id_):
                    splitter.context_cb(id_, index)

                act = QAction(title, self)
                act.triggered.connect(cb)
                menu.addAction(act)

    for provider in splitter.root.providers:
        if hasattr(provider, 'ns_context'):
            load_items(menu, provider.ns_context())

    point = pos
    global_point = self.mapToGlobal(point)
    menu.exec(global_point)

    for i in 0, 1:
        widget[i].setStyleSheet(sheet[i])

# @+node:tbnorth.20160510091151.1: *5* nsh.mouseEvents
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
    """mouse event - mouse pressed on splitter handle,
    pass info. up to splitter

    :param QMouseEvent event: mouse event
    """
    super().mousePressEvent(event)
    self.splitter()._splitter_clicked(self, event, release=False, double=False)

def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None:
    """mouse event - mouse pressed on splitter handle,
    pass info. up to splitter

    :param QMouseEvent event: mouse event
    """
    super().mouseReleaseEvent(event)
    self.splitter()._splitter_clicked(self, event, release=True, double=False)

def mouseDoubleClickEvent(self, event: QtGui.QMouseEvent) -> None:
    """mouse event - mouse pressed on splitter handle,
    pass info. up to splitter

    :param QMouseEvent event: mouse event
    """
    super().mouseDoubleClickEvent(event)
    self.splitter()._splitter_clicked(self, event, release=True, double=True)

# @+node:ekr.20110605121601.17966: *4* class NestedSplitter (QSplitter)
class NestedSplitter(QtWidgets.QSplitter):
    # Allow special behavior to be turned of at import stage.
    # useful if other code must run to set up callbacks, that other code can re-enable.
    enabled = True
    other_orientation = {
        Orientation.Vertical: Orientation.Horizontal,
        Orientation.Horizontal: Orientation.Vertical,
    }
    # a regular signal, but you can't use its .connect() directly,
    # use splitterClicked_connect()
    _splitterClickedSignal = QtCore.pyqtSignal(
        QtWidgets.QSplitter, QtWidgets.QSplitterHandle, QtGui.QMouseEvent, bool, bool
    )

    @others


# @+node:ekr.20110605121601.17967: *5* ns.__init__
def __init__(self, parent=None, orientation=None, root=None):
    """Ctor for NestedSplitter class."""
    if orientation is None:
        orientation = Orientation.Horizontal
    # This creates a NestedSplitterHandle.
    super().__init__(orientation, parent)
    if root is None:
        root = self.top(local=True)
        if root == self:
            root.marked = None  # Tuple: self,index,side-1,widget
            root.providers = []
            root.holders = {}
            root.windows = []
            root._main = self.parent()  # holder of the main splitter
            # list of top level NestedSplitter windows opened from 'Open Window'
            # splitter handle context menu
            root.zoomed = False

        # NestedSplitter is a kind of meta-widget, in that it manages
        # panes across multiple actual splitters, even windows.
        # So to create a signal for a click on splitter handle, we
        # need to propagate the .connect() call across all the
        # actual splitters, current and future
        root._splitterClickedArgs = []  # save for future added splitters
    for args in root._splitterClickedArgs:
        # apply any .connect() calls that occurred earlier
        self._splitterClickedSignal.connect(*args)

    self.root = root

# @+node:ekr.20110605121601.17968: *5* ns.__repr__
def __repr__(self):
    # parent = self.parent()
    # name = parent and parent.objectName() or '<no parent>'
    name = self.objectName() or '<no name>'
    return f"(NestedSplitter) {name} at {id(self)}"

__str__ = __repr__

# @+node:ekr.20110605121601.17969: *5* ns.overrides of QSplitter methods
# @+node:ekr.20110605121601.17970: *6* ns.createHandle
def createHandle(self, *args, **kargs):
    return NestedSplitterHandle(self)

# @+node:tbrown.20110729101912.30820: *6* ns.childEvent
def childEvent(self, event: QtCore.QChildEvent) -> None:
    """If a panel client is closed not by us, there may be zero
    splitter handles left, so add an Action button

    unless it was the last panel in a separate window, in which
    case close the window"""
    QtWidgets.QSplitter.childEvent(self, event)
    if not event.removed():
        return
    local_top = self.top(local=True)
    # if only non-placeholder pane in a top level window deletes
    # itself, delete the window
    if (
        isinstance(local_top.parent(), NestedSplitterTopLevel)
        and local_top.count() == 1  # one left, could be placeholder
        and isinstance(local_top.widget(0), NestedSplitterChoice)  # is placeholder
    ):
        local_top.parent().deleteLater()
        return
    # don't leave a one widget splitter
    if self.count() == 1 and local_top != self:
        self.parent().addWidget(self.widget(0))
        self.deleteLater()
    if parent := self.parentWidget():
        layout = parent.layout()  # QLayout, not a NestedSplitter
    else:
        layout = None
    if self.count() == 1 and self.top(local=True) == self:
        if self.max_count() <= 1 or not layout:
            # maintain at least two items
            self.insert(0)
            # shrink the added button
            self.setSizes([0] + self.sizes()[1:])
        else:
            # replace ourselves in out parent's layout with our child
            pos = layout.indexOf(self)
            child = self.widget(0)
            layout.insertWidget(pos, child)
            pos = layout.indexOf(self)
            layout.takeAt(pos)
            self.setParent(None)

# @+node:ekr.20110605121601.17971: *5* ns.add
def add(self, side, w=None):
    """wrap a horizontal splitter in a vertical splitter, or
    visa versa"""
    orientation = self.other_orientation[self.orientation()]
    layout = self.parent().layout()
    if isinstance(self.parent(), NestedSplitter):
        # don't add new splitter if not needed, i.e. we're the
        # only child of a previously more populated splitter
        if w is None:
            w = NestedSplitterChoice(self.parent())
        self.parent().insertWidget(self.parent().indexOf(self) + side, w)
        # in this case, where the parent is a one child, no handle splitter,
        # the (prior to this invisible) orientation may be wrong
        # can't reproduce this now, but this guard is harmless
        self.parent().setOrientation(orientation)
    elif layout:
        new = NestedSplitter(None, orientation=orientation, root=self.root)
        # parent set by layout.insertWidget() below
        old = self
        pos = layout.indexOf(old)
        new.addWidget(old)
        if w is None:
            w = NestedSplitterChoice(new)
        new.insertWidget(side, w)
        layout.insertWidget(pos, new)
    else:
        # fail - parent is not NestedSplitter and has no layout
        pass

# @+node:tbrown.20110621120042.22675: *5* ns.add_adjacent (finds layout)
def add_adjacent(self, what, widget_id, side='right-of', name=None):
    """add a widget relative to another already present widget"""
    horizontal, vertical = Orientation.Horizontal, Orientation.Vertical
    layout = self.top().get_layout()

    def hunter(layout, id_):
        """Recursively look for this widget"""
        for n, i in enumerate(layout['content']):
            if i == id_ or (
                isinstance(i, QtWidgets.QWidget)
                and (i.objectName() == id_ or i.__class__.__name__ == id_)
            ):
                return layout, n
            if not isinstance(i, QtWidgets.QWidget):
                # then it must be a layout dict
                if x := hunter(i, id_):
                    return x
        return None

    # find the layout containing widget_id

    l = hunter(layout, widget_id)
    if l is None:
        return False
    layout, pos = l
    # self.dump_layout(layout)
    orient = layout['orientation']
    if (
        orient == horizontal
        and side in ('right-of', 'left-of')
        or orient == vertical
        and side in ('above', 'below')
    ):
        # easy case, just insert the new thing, what,
        # either side of old, in existing splitter
        if side in ('right-of', 'below'):
            pos += 1
        layout['splitter'].insert(pos, what)
    else:
        # hard case, need to replace old with a new splitter
        if side in ('right-of', 'left-of'):
            ns = NestedSplitter(orientation=horizontal, root=self.root)
        else:
            ns = NestedSplitter(orientation=vertical, root=self.root)
        if name:
            g.trace(name, ns)
            ns.setObjectName(name)
        old = layout['content'][pos]
        if not isinstance(old, QtWidgets.QWidget):  # see get_layout()
            old = layout['splitter']
        # put new thing, what, in new splitter, no impact on anything else
        ns.insert(0, what)
        # then swap the new splitter with the old content
        layout['splitter'].replace_widget_at_index(pos, ns)
        # now put the old content in the new splitter,
        # doing this sooner would mess up the index (pos)
        ns.insert(0 if side in ('right-of', 'below') else 1, old)
    return True

# @+node:ekr.20110605121601.17972: *5* ns.choice_menu
def choice_menu(self, button, pos):
    """build menu on Action button"""
    menu = QtWidgets.QMenu(self.top())  # #1995
    index = self.indexOf(button)
    if (
        self.root.marked
        and not self.invalid_swap(button, self.root.marked[3])
        and self.top().max_count() > 2
    ):
        act = QAction("Move marked here", self)
        act.triggered.connect(lambda checked: self.replace_widget(button, self.root.marked[3]))
        menu.addAction(act)
    for provider in self.root.providers:
        if hasattr(provider, 'ns_provides'):
            for title, id_ in provider.ns_provides():

                def cb(checked, id_=id_):
                    self.place_provided(id_, index)

                act = QAction(title, self)
                act.triggered.connect(cb)
                menu.addAction(act)
    if menu.isEmpty():
        act = QAction("Nothing marked, and no options", self)
        menu.addAction(act)

    point = button.pos()
    global_point = button.mapToGlobal(point)
    menu.exec(global_point)

# @+node:tbrown.20120418121002.25712: *5* ns.closing
def closing(self, window):
    """forget a top-level additional layout which was closed"""
    self.windows.remove(window)

# @+node:tbrown.20110628083641.11723: *5* ns.place_provided
def place_provided(self, id_, index):
    """replace Action button with provided widget"""
    provided = self.get_provided(id_)
    if provided is None:
        return
    self.replace_widget_at_index(index, provided)
    self.top().prune_empty()
    # user can set up one widget pane plus one Action pane, then move the
    # widget into the action pane, level 1 pane and no handles
    if self.top().max_count() < 2:
        print('Adding Action widget to maintain at least one handle')
        self.top().insert(0, NestedSplitterChoice(self.top()))

# @+node:tbrown.20110628083641.11729: *5* ns.context_cb
def context_cb(self, id_, index):
    """find a provider to provide a context menu service, and do it"""
    for provider in self.root.providers:
        if hasattr(provider, 'ns_do_context'):
            if provider.ns_do_context(id_, self, index):
                break

# @+node:ekr.20110605121601.17973: *5* ns.contains
def contains(self, widget):
    """check if widget is a descendant of self"""
    for i in range(self.count()):
        if widget == self.widget(i):
            return True
        if isinstance(self.widget(i), NestedSplitter):
            if self.widget(i).contains(widget):
                return True
    return False

# @+node:ekr.20240518111804.1: *5* ns.dump_layout (new)
def dump_layout(self, layout: dict) -> None:
    print('')
    print(f"Dump of ns layout for {self.__class__.__name__}({self.objectName()})")
    self.dump_inner_layout(layout, level=0)

def dump_inner_layout(self, content: dict, level: int) -> None:
    ws = level * 2 * ' '
    widget_seen = False
    print(
        '\n'.join(
            [  # Python 3.9 does not allow newlines within f-strings.
                f"{ws}contents level {level}...",
                f"  {ws}orientation: {content.get('orientation')}",
                f"  {ws}sizes: {content.get('sizes')}",
            ]
        )
    )
    inner_content = content.get('content')
    assert isinstance(inner_content, list)
    for i, inner in enumerate(inner_content):
        if isinstance(inner, dict):
            self.dump_inner_layout(inner, level + 1)
            continue
        if not widget_seen:
            widget_seen = True
            print(f"  {ws}widgets level {level}...")
        print(f"    {ws}widget {i} {inner.__class__.__name__}({inner.objectName()})")

# @+node:tbrown.20120418121002.25439: *5* ns.find_child
def find_child(self, child_class, child_name=None):
    """Like QObject.findChild, except search self.top()
    *AND* each window in self.root.windows
    """
    child = self.top().findChild(child_class, child_name)
    if not child:
        for window in self.root.windows:
            if child := window.findChild(child_class, child_name):
                break
    return child

# @+node:ekr.20110605121601.17974: *5* ns.handle_context
def handle_context(self, index):
    """for a handle, return (widget, neighbour, count)

    This is the handle's context in the NestedSplitter, not the
    handle's context menu.

    widget
      the pair of widgets either side of the handle
    neighbour
      the pair of NestedSplitters either side of the handle, or None
      if the neighbours are not NestedSplitters, i.e.
      [ns0, ns1] or [None, ns1] or [ns0, None] or [None, None]
    count
      the pair of nested counts of widgets / splitters around the handle
    """
    widget = [self.widget(index - 1), self.widget(index)]
    neighbour = [(i if isinstance(i, NestedSplitter) else None) for i in widget]
    count = []
    for i in 0, 1:
        if neighbour[i]:
            l = [ii.count() for ii in neighbour[i].self_and_descendants()]
            n = sum(l) - len(l) + 1  # count leaves, not splitters
            count.append(n)
        else:
            count.append(1)
    return widget, neighbour, count

# @+node:tbrown.20110621120042.22920: *5* ns.equalize_sizes
def equalize_sizes(self, recurse=False):
    """make all pane sizes equal"""
    if not self.count():
        return
    for i in range(self.count()):
        self.widget(i).setHidden(False)
    size = sum(self.sizes()) / self.count()
    self.setSizes([int(size)] * self.count())  # #2281
    if recurse:
        for i in range(self.count()):
            if isinstance(self.widget(i), NestedSplitter):
                self.widget(i).equalize_sizes(recurse=True)

# @+node:ekr.20110605121601.17975: *5* ns.insert
def insert(self, index, w=None):
    """insert a pane with a widget or, when w==None, Action button"""
    if w is None:  # do NOT use 'not w', fails in PyQt 4.8
        w = NestedSplitterChoice(self)
        # A QWidget, with self as parent.
        # This creates the menu.
    self.insertWidget(index, w)
    self.equalize_sizes()
    return w

# @+node:ekr.20110605121601.17976: *5* ns.invalid_swap
def invalid_swap(self, w0, w1):
    """check for swap violating hierarchy"""
    return (
        w0 == w1
        or isinstance(w0, NestedSplitter)
        and w0.contains(w1)
        or isinstance(w1, NestedSplitter)
        and w1.contains(w0)
    )

# @+node:ekr.20110605121601.17977: *5* ns.mark
def mark(self, index, side):
    """mark a widget for later swapping"""
    self.root.marked = (self, index, side - 1, self.widget(index + side - 1))

# @+node:ekr.20110605121601.17978: *5* ns.max_count
def max_count(self):
    """find max widgets in this and child splitters"""
    counts = []
    count = 0
    for i in range(self.count()):
        count += 1
        if isinstance(self.widget(i), NestedSplitter):
            counts.append(self.widget(i).max_count())
    counts.append(count)
    return max(counts)

# @+node:tbrown.20120418121002.25438: *5* ns.open_window
def open_window(self, action=None):
    """open a top-level window, a TopLevelFreeLayout instance, to hold a
    free-layout in addition to the one in the outline's main window"""
    ns = NestedSplitter(root=self.root)
    window = NestedSplitterTopLevel(owner=self.root, window_title=ns.get_title(action))
    hbox = QtWidgets.QHBoxLayout()
    window.setLayout(hbox)
    hbox.setContentsMargins(0, 0, 0, 0)
    window.resize(400, 300)
    hbox.addWidget(ns)
    # NestedSplitters must have two widgets so the handle carrying
    # the all important context menu exists
    ns.addWidget(NestedSplitterChoice(ns))
    button = NestedSplitterChoice(ns)
    ns.addWidget(button)
    if action == '_move_marked_there':
        ns.replace_widget(button, ns.root.marked[3])
    elif action is not None:
        ns.place_provided(action, 1)
    ns.setSizes([0, 1])  # but hide one initially
    self.root.windows.append(window)
    # copy the main main window's stylesheet to new window
    w: QtCore.QObject | None = self.root  # A Qt Widget, class NestedSplitter
    sheets = []
    while w:
        if s := w.styleSheet():
            sheets.append(str(s))
        w = w.parent()
    sheets.reverse()
    ns.setStyleSheet('\n'.join(sheets))
    window.show()

# @+node:tbrown.20110627201141.11744: *5* ns.register_provider
def register_provider(self, provider: object) -> None:
    """Register something which provides some of the ns_* methods.

    NestedSplitter tests for the presence of the following methods on
    the registered things, and calls them when needed if they exist.

    ns_provides()
      should return a list of ('Item name', '__item_id') strings,
      'Item name' is displayed in the Action button menu, and
      '__item_id' is used in ns_provide().
    ns_provide(id_)
      should return the widget to replace the Action button based on
      id_, or None if the called thing is not the provider for this id_
    ns_context()
      should return a list of ('Item name', '__item_id') strings,
      'Item name' is displayed in the splitter handle context-menu, and
      '__item_id' is used in ns_do_context().  May also return a dict,
      in which case each key is used as a sub-menu title, whose menu
      items are the corresponding dict value, a list of tuples as above.
      dicts and tuples may be interspersed in lists.
    ns_do_context()
      should do something based on id_ and return True, or return False
      if the called thing is not the provider for this id_
    ns_provider_id()
      return a string identifying the provider (at class or instance level),
      any providers with the same id will be removed before a new one is
      added
    """
    # drop any providers with the same id
    if hasattr(provider, 'ns_provider_id'):
        id_ = cast(Any, provider).ns_provider_id()
        cull = []
        for i in self.root.providers:
            if hasattr(i, 'ns_provider_id') and i.ns_provider_id() == id_:
                cull.append(i)
        for i in cull:
            self.root.providers.remove(i)
    self.root.providers.append(provider)

# @+node:ekr.20110605121601.17980: *5* ns.remove & helper
def remove(self, index, side):
    widget = self.widget(index + side - 1)
    # clear marked if it's going to be deleted
    if self.root.marked and (
        self.root.marked[3] == widget
        or isinstance(self.root.marked[3], NestedSplitter)
        and self.root.marked[3].contains(widget)
    ):
        self.root.marked = None
    # send close signal to all children
    if isinstance(widget, NestedSplitter):
        count = widget.count()
        all_ok = True
        for splitter in widget.self_and_descendants():
            for i in range(splitter.count() - 1, -1, -1):
                all_ok &= self.close_or_keep(splitter.widget(i)) is not False
        if all_ok or count <= 0:
            widget.setParent(None)
    else:
        self.close_or_keep(widget)

# @+node:ekr.20110605121601.17981: *6* ns.close_or_keep
def close_or_keep(self, widget, other_top=None):
    """when called from a closing secondary window, self.top() would
    be the top splitter in the closing window, and we need the client
    to specify the top of the primary window for us, in other_top"""
    if widget is None:
        return True
    for k in self.root.holders:
        if hasattr(widget, k):
            holder = self.root.holders[k]
            if holder == 'TOP':
                holder = other_top or self.top()
            if hasattr(holder, "addTab"):
                holder.addTab(widget, getattr(widget, k))
            else:
                holder.addWidget(widget)
            return True
    if widget.close():
        widget.setParent(None)
        return True
    return False

# @+node:ekr.20110605121601.17982: *5* ns.replace_widget & replace_widget_at_index
def replace_widget(self, old, new):
    "Swap the provided widgets in place"
    sizes = self.sizes()
    new.setParent(None)
    self.insertWidget(self.indexOf(old), new)
    self.close_or_keep(old)
    new.show()
    self.setSizes(sizes)

def replace_widget_at_index(self, index, new):
    """Replace the widget at index with w."""
    sizes = self.sizes()
    old = self.widget(index)
    if old != new:
        new.setParent(None)
        self.insertWidget(index, new)
        self.close_or_keep(old)
        new.show()
        self.setSizes(sizes)

# @+node:ekr.20110605121601.17983: *5* ns.rotate
def rotate(self, descending=False):
    """Change orientation - current rotates entire hierarchy, doing less
    is visually confusing because you end up with nested splitters with
    the same orientation - avoiding that would mean doing rotation by
    inserting out widgets into our ancestors, etc.
    """
    for i in self.top().self_and_descendants():
        if i.orientation() == Orientation.Vertical:
            i.setOrientation(Orientation.Horizontal)
        else:
            i.setOrientation(Orientation.Vertical)

# @+node:vitalije.20170713085342.1: *5* ns.rotateOne
def rotateOne(self, index):
    """Change orientation - only of splitter handle at index."""
    psp = self.parent()
    if self.count() == 2 and isinstance(psp, NestedSplitter):
        i = psp.indexOf(self)
        sizes: list = psp.sizes()
        [a, b] = self.sizes()
        s = sizes[i]
        s1 = int(a * s / (a + b))
        s2 = int(b * s / (a + b))
        sizes[i : i + 1] = [s1, s2]
        prev = self.widget(0)
        next = self.widget(1)
        psp.insertWidget(i, prev)
        psp.insertWidget(i + 1, next)
        psp.setSizes(sizes)
        assert psp.widget(i + 2) is self
        psp.remove(i + 3, 0)
        psp.setSizes(sizes)
    elif self is self.root and self.count() == 2:
        self.rotate()
    elif self.count() == 2:
        self.setOrientation(self.other_orientation[self.orientation()])
    else:
        orientation = self.other_orientation[self.orientation()]
        prev = self.widget(index - 1)
        next = self.widget(index)
        if None in (prev, next):
            return
        sizes = self.sizes()
        s1, s2 = sizes[index - 1 : index + 1]
        sizes[index - 1 : index + 1] = [s1 + s2]
        newsp = NestedSplitter(self, orientation=orientation, root=self.root)
        newsp.addWidget(prev)
        newsp.addWidget(next)
        self.insertWidget(index - 1, newsp)
        prev.setHidden(False)
        next.setHidden(False)
        newsp.setSizes([s1, s2])
        self.setSizes(sizes)

# @+node:ekr.20110605121601.17984: *5* ns.self_and_descendants
def self_and_descendants(self):
    """Yield self and all **NestedSplitter** descendants"""
    for i in range(self.count()):
        if isinstance(self.widget(i), NestedSplitter):
            for w in self.widget(i).self_and_descendants():
                yield w
    yield self

# @+node:ekr.20110605121601.17985: *5* ns.split
def split(self, index, side, w=None, name=None):
    """replace the adjacent widget with a NestedSplitter containing
    the widget and an Action button"""
    sizes = self.sizes()
    old = self.widget(index + side - 1)
    if w is None:
        w = NestedSplitterChoice(self)
    if isinstance(old, NestedSplitter):
        old.addWidget(w)
        old.equalize_sizes()
    else:
        orientation = self.other_orientation[self.orientation()]
        new = NestedSplitter(self, orientation=orientation, root=self.root)
        self.insertWidget(index + side - 1, new)
        new.addWidget(old)
        new.addWidget(w)
        new.equalize_sizes()
    self.setSizes(sizes)

# @+node:ekr.20110605121601.17986: *5* ns.swap
def swap(self, index):
    """swap widgets either side of a handle"""
    self.insertWidget(index - 1, self.widget(index))

# @+node:ekr.20110605121601.17987: *5* ns.swap_with_marked
def swap_with_marked(self, index, side):
    osplitter, oidx, oside, ow = self.root.marked
    idx = index + side - 1
    # convert from handle index to widget index
    # 1 already subtracted from oside in mark()
    w = self.widget(idx)
    if self.invalid_swap(w, ow):
        return
    self.insertWidget(idx, ow)
    osplitter.insertWidget(oidx, w)
    self.root.marked = self, self.indexOf(ow), 0, ow
    self.equalize_sizes()
    osplitter.equalize_sizes()

# @+node:ekr.20110605121601.17988: *5* ns.top
def top(self, local=False) -> Any:
    """find top (outer) widget, which is not necessarily root"""
    top: Any
    if local:
        top = self
        while isinstance(top.parent(), NestedSplitter):
            top = top.parent()
    else:
        top = self.root._main.findChild(NestedSplitter)
    return top

# @+node:ekr.20110605121601.17989: *5* ns.get_layout
def get_layout(self):
    """
    Return a dict describing the layout.

    Usually you would call ns.top().get_layout()
    """
    ans = {
        'content': [],
        'orientation': self.orientation(),
        'sizes': self.sizes(),
        'splitter': self,
    }
    for i in range(self.count()):
        w = self.widget(i)
        if isinstance(w, NestedSplitter):
            ans['content'].append(w.get_layout())
        else:
            ans['content'].append(w)
    return ans

# @+node:tbrown.20110628083641.11733: *5* ns.get_saveable_layout
def get_saveable_layout(self):
    """
    Return the dict for saveable layouts.

    The content entry for non-NestedSplitter items is the provider ID
    string for the item, or 'UNKNOWN', and the splitter entry is omitted.
    """
    ans: dict[str, Any] = {
        'content': [],
        'orientation': 1 if self.orientation() == Orientation.Horizontal else 2,
        'sizes': self.sizes(),
    }
    for i in range(self.count()):
        w = self.widget(i)
        if isinstance(w, NestedSplitter):
            ans['content'].append(w.get_saveable_layout())
        else:
            ans['content'].append(getattr(w, '_ns_id', 'UNKNOWN'))
    return ans

# @+node:ekr.20160416083415.1: *5* ns.get_splitter_by_name
def get_splitter_by_name(self, name):
    """Return the splitter with the given objectName()."""
    if self.objectName() == name:
        return self
    for i in range(self.count()):
        w = self.widget(i)
        # Recursively test w and its descendants.
        if isinstance(w, NestedSplitter):
            if w2 := w.get_splitter_by_name(name):
                return w2
    return None

# @+node:tbrown.20110628083641.21154: *5* ns.load_layout
def load_layout(self, c, layout, level=0):
    trace = 'layouts' in g.app.debug
    if trace:
        g.trace('level', level)
        tag = f"layout: {c.shortFileName()}"
        g.printObj(layout, tag=tag)
    if layout['orientation'] == 1:
        self.setOrientation(Orientation.Horizontal)
    else:
        self.setOrientation(Orientation.Vertical)

    found = 0
    if level == 0:
        for i in self.self_and_descendants():
            for n in range(i.count()):
                i.widget(n)._in_layout = False
    for content_layout in layout['content']:
        if isinstance(content_layout, dict):
            new = NestedSplitter(root=self.root, parent=self)
            new._in_layout = True
            self.insert(found, new)
            found += 1
            new.load_layout(c, content_layout, level + 1)
        else:
            if provided := self.get_provided(content_layout):
                self.insert(found, provided)
                provided._in_layout = True
                found += 1
            else:
                print(f"No provider for {content_layout}")
    self.prune_empty()
    if self.count() != len(layout['sizes']):
        not_in_layout = set()
        for i in self.self_and_descendants():
            for n in range(i.count()):
                c = i.widget(n)
                if not (hasattr(c, '_in_layout') and c._in_layout):
                    not_in_layout.add(c)
        for i in not_in_layout:
            self.close_or_keep(i)
        self.prune_empty()
    if self.count() == len(layout['sizes']):
        self.setSizes(layout['sizes'])
    else:
        print(
            f"Wrong pane count at level {level:d}, count:{self.count():d}, sizes:{len(layout['sizes']):d}"
        )
        self.equalize_sizes()

# @+node:tbrown.20110628083641.21156: *5* ns.prune_empty
def prune_empty(self):
    for i in range(self.count() - 1, -1, -1):
        w = self.widget(i)
        if isinstance(w, NestedSplitter):
            if w.max_count() == 0:
                w.setParent(None)
                # w.deleteLater()

# @+node:tbrown.20110628083641.21155: *5* ns.get_provided
def find_by_id(self, id_):
    for s in self.self_and_descendants():
        for i in range(s.count()):
            if getattr(s.widget(i), '_ns_id', None) == id_:
                return s.widget(i)
    return None

def get_provided(self, id_):
    """IMPORTANT: nested_splitter should set the _ns_id attribute *only*
    if the provider doesn't do it itself.  That allows the provider to
    encode state information in the id.

    Also IMPORTANT: nested_splitter should call all providers for each id_, not
    just providers which previously advertised the id_.  E.g. a provider which
    advertises leo_bookmarks_show may also be the correct provider for
    leo_bookmarks_show:4532.234 - let the providers decide in ns_provide().
    """
    for provider in self.root.providers:
        if hasattr(provider, 'ns_provide'):
            if provided := provider.ns_provide(id_):
                if provided == 'USE_EXISTING':
                    # provider claiming responsibility, and saying
                    # we already have it, i.e. it's a singleton
                    if w := self.top().find_by_id(id_):
                        if not hasattr(w, '_ns_id'):
                            # IMPORTANT: see docstring
                            w._ns_id = id_
                        return w
                else:
                    if not hasattr(provided, '_ns_id'):
                        # IMPORTANT: see docstring
                        provided._ns_id = id_
                    return provided
    return None

# @+node:ekr.20200917063155.1: *5* ns.get_title
def get_title(self, id_):
    """Like get_provided(), but just gets a title for a window"""
    if id_ is None:
        return "Leo widget window"
    for provider in self.root.providers:
        if hasattr(provider, 'ns_title'):
            if provided := provider.ns_title(id_):
                return provided
    return "Leo unnamed window"

# @+node:tbrown.20140522153032.32656: *5* ns.zoom_toggle
def zoom_toggle(self, local=False):
    """zoom_toggle - (Un)zoom current pane to be only expanded pane

    :param bool local: just zoom pane within its own splitter
    """
    if self.root.zoomed:
        for ns in self.top().self_and_descendants():
            if hasattr(ns, '_unzoom'):
                # this splitter could have been added since
                ns.setSizes(ns._unzoom)
    else:
        focused = g.app.gui.qtApp.focusWidget()
        parents = []
        parent = focused
        while parent:
            parents.append(parent)
            parent = parent.parent()
        if not focused:
            g.es("Not zoomed, and no focus")
        for ns in (self if local else self.top()).self_and_descendants():
            # FIXME - shouldn't be doing this across windows
            ns._unzoom = ns.sizes()
            for i in range(ns.count()):
                w = ns.widget(i)
                if w in parents:
                    sizes = [0] * len(ns._unzoom)
                    sizes[i] = sum(ns._unzoom)
                    ns.setSizes(sizes)
                    break
    self.root.zoomed = not self.root.zoomed

# @+node:tbnorth.20160510092439.1: *5* ns._splitter_clicked
def _splitter_clicked(self, handle, event, release, double):
    """_splitter_clicked - coordinate propagation of signals
    for clicks on handles.  Turned out not to need any particular
    coordination, handles could call self._splitterClickedSignal.emit
    directly, but design wise this is a useful control point.

    :param QSplitterHandle handle: handle that was clicked
    :param QMouseEvent event: click event
    :param bool release: was it a release event
    :param bool double: was it a double click event
    """
    self._splitterClickedSignal.emit(self, handle, event, release, double)

# @+node:tbnorth.20160510123445.1: *5* splitterClicked_connect
def splitterClicked_connect(self, *args):
    """Apply .connect() args to all actual splitters,
    and store for application to future splitters.
    """
    self.root._splitterClickedArgs.append(args)
    for splitter in self.top().self_and_descendants():
        splitter._splitterClickedSignal.connect(*args)

# @+node:tbrown.20130813134319.11942: *3* ../plugins/richtext.py
# This plugin is obsolete. There are no plans to make it work with Qt6.
<< docstring >>
# This complaint is probably valid.
# pylint: disable=no-name-in-module
<< imports >>
@others
@language python
@tabwidth -4
# @+node:tbrown.20130813134319.14333: *4* << docstring >> (richtext.py)
"""
richtext.py - Rich text editing
===============================

This plugin allows you to use CKEditor__ to edit rich text
in Leo.  Text is stored as HTML in Leo nodes.

__ http://ckeditor.com/

``richtext.py`` provides these ``Alt-X`` commands (also available from
Plugins -> richtext menu):

  cke-text-close
    Close the rich text editor, unhide the regular editor.
  cke-text-open
    Open the rich text editor, hide the regular editor.
  cke-text-switch
    Switch between regular and rich text editor.
  cke-text-toggle-autosave
    Toggle autosaving of changes when you leave a node.
    Be careful not to convert plain text (e.g. source code) to rich
    text unintentionally.  As long as you make no edits, the original
    text will not be changed.

Unless autosaving is enabled, you must confirm saving of edits
each time you edit a node with the rich text editor.

``@rich`` in the headline or first few lines (1000 characters) of a node or its
ancestors will automatically open the rich text editor. ``@norich`` cancels this
action.  Manually opened editors are not affected.

``richtext.py`` uses these ``@settings``:

  @bool richtext_cke_autosave = False
    Set this to True for rich text edits to be saved automatically.

    *BE CAREFUL* - plain-text nodes will be converted to rich text
    without confirmation if you edit them in rich text mode when
    this is True.

  @data richtext_cke_config Configuration info. for CKEditor, see
    http://docs.ckeditor.com/#!/guide/dev_configuration the content of this node
    is the javascript object passed to ``CKEDITOR.replace()`` as it's second
    argument. The version supplied in LeoSettings.leo sets up a sensible
    toolbar. To enable *all* CKEditor toolbar features copy this setting to
    myLeoSettings.leo and remove the default content, i.e. make this node blank,
    then CKEditor will generate a toolbar with all available features.

To make a button to toggle the editor on and off, use::

    @button rich
      c.doCommandByName('cke-text-switch')

"""

# @+node:tbrown.20130813134319.14335: *4* << imports >> (richtext.py)
import time
from typing import Any, cast
from urllib.parse import unquote
from leo.core import leoGlobals as g
from leo.core.leoQt import QtCore, QtWidgets, QtWebKit, QtWebKitWidgets

#
# Fail fast, right after all imports.
g.assertUi('qt')  # May raise g.UiTypeException, caught by the plugins manager.
#
# Alias.
real_webkit = QtWebKit and 'engine' not in g.os_path_basename(QtWebKit.__file__).lower()


# @+node:tbrown.20130813134319.14337: *4* init (richtext.py)
def init():
    """Return True if the plugin has loaded successfully."""
    if not QtWebKit:
        return False
    name = g.app.gui.guiName()
    ok = name == 'qt'
    if ok:
        g.registerHandler('after-create-leo-frame', onCreate)
        g.registerHandler('select3', at_rich_check)
        g.plugin_signon(__name__)
    elif name != 'nullGui':
        print('richtext.py plugin not loading because gui is not Qt')
    return ok


# @+node:tbrown.20130813134319.5691: *4* class CKEEditor
class CKEEditor(QtWidgets.QWidget):
    @others


# @+node:tbrown.20130813134319.7225: *5* __init__ & reloadSettings (CKEEditor)
def __init__(self, *args, **kwargs):
    self.c = kwargs['c']
    del kwargs['c']
    super().__init__(*args, **kwargs)
    # were we opened by an @ rich node? Calling code will set
    self.at_rich = False
    # are we being closed by leaving an @ rich node? Calling code will set
    self.at_rich_close = False
    # read settings.
    self.reloadSettings()
    # load HTML template
    template_path = g.os_path_join(g.computeLeoDir(), 'plugins', 'cke_template.html')
    self.template = open(template_path).read()
    path = g.os_path_join(g.computeLeoDir(), 'external', 'ckeditor')
    self.template = self.template.replace(
        '[CKEDITOR]', QtCore.QUrl.fromLocalFile(path).toString()
    )
    # make widget containing QWebView
    self.setLayout(QtWidgets.QVBoxLayout())
    self.layout().setSpacing(0)
    self.layout().setContentsMargins(0, 0, 0, 0)
    # enable inspector, if this really is QtWebKit
    if real_webkit:
        QtWebKit.QWebSettings.globalSettings().setAttribute(
            QtWebKit.QWebSettings.DeveloperExtrasEnabled, True
        )
    self.webview = QtWebKitWidgets.QWebView()
    self.layout().addWidget(self.webview)
    g.registerHandler('select3', self.select_node)
    g.registerHandler('unselect1', self.unselect_node)
    # load current node
    self.select_node('', {'c': self.c, 'new_p': self.c.p})

def reloadSettings(self):
    c = self.c
    c.registerReloadSettings(self)
    # read autosave preference
    if not hasattr(self.c, '_ckeeditor_autosave'):
        auto = self.c.config.getBool("richtext-cke-autosave") or False
        cast(Any, self.c)._ckeeditor_autosave = auto
        if auto:
            g.es("NOTE: automatic saving of rich text edits")
    # load config
    self.config = self.c.config.getData("richtext_cke_config")
    if self.config:
        self.config = '\n'.join(self.config).strip()

# @+node:tbrown.20130813134319.7226: *5* select_node
def select_node(self, tag, kwargs):
    c = kwargs['c']
    if c != self.c:
        return

    p = kwargs['new_p']

    self.v = p.v  # to ensure unselect_node is working on the right node
    # currently (20130814) insert doesn't trigger unselect/select, but
    # even if it did, this would be safest

    data = self.template
    if p.b.startswith('<'):  # already rich text, probably
        content = p.b
        self.was_rich = True
    else:
        self.was_rich = p.b.strip() == ''
        # put anything except whitespace in a <pre/>
        content = "<pre>%s</pre>" % p.b if not self.was_rich else ''

    data = data.replace('[CONTENT]', content)

    # replace textarea with CKEditor, with or without config.
    if self.config:
        data = data.replace('[CONFIG]', ', ' + self.config)
    else:
        data = data.replace('[CONFIG]', '')

    # try and make the path for URL evaluation relative to the node's path
    path = c.getPath(p)
    if p.h.startswith('@'):  # see if it's a @<file> node of some sort
        nodepath = p.h.split(None, 1)[-1]
        nodepath = g.os_path_join(path, nodepath)
        if not g.os_path_isdir(nodepath):  # remove filename
            nodepath = g.os_path_dirname(nodepath)
        if g.os_path_isdir(nodepath):  # append if it's a directory
            path = nodepath

    self.webview.setHtml(data, QtCore.QUrl.fromLocalFile(path + "/"))

# @+node:tbrown.20130813134319.7228: *5* unselect_node
def unselect_node(self, tag, kwargs):
    c = kwargs['c']
    if c != self.c:
        return None
    # read initial content and request and wait for final content
    frame = self.webview.page().mainFrame()
    ele = frame.findFirstElement("#initial")
    text = str(ele.toPlainText()).strip()
    if text == '[empty]':
        return None  # no edit
    frame.evaluateJavaScript('save_final();')
    ele = frame.findFirstElement("#final")
    for attempt in range(10):  # wait for up to 1 second
        new_text = str(ele.toPlainText()).strip()
        if new_text == '[empty]':
            time.sleep(0.1)
            continue
        break
    if new_text == '[empty]':
        print("Didn't get new text")
        return None
    text = unquote(str(text))
    new_text = unquote(str(new_text))
    if new_text != text:
        if self.c._ckeeditor_autosave:
            ans = 'yes'
        else:
            text = "Save edits?"
            if not self.was_rich:
                text += " *converting plain text to rich*"
            ans = g.app.gui.runAskYesNoCancelDialog(self.c, "Save edits?", text)
        if ans == 'yes':
            c.vnode2position(self.v).b = new_text
            c.redraw()  # but node has content marker still doesn't appear?
        elif ans == 'cancel':
            return 'STOP'
        else:
            pass  # discard edits
    return None

# @+node:tbrown.20130813134319.7229: *5* close
def close(self):
    if self.c and not self.at_rich_close:
        # save changes?
        self.unselect_node('', {'c': self.c, 'old_p': self.c.p})
    self.c = None
    g.unregisterHandler('select3', self.select_node)
    g.unregisterHandler('unselect1', self.unselect_node)
    return QtWidgets.QWidget.close(self)

# @+node:tbrown.20130813134319.5694: *4* class CKEPaneProvider
class CKEPaneProvider:
    ns_id = '_add_cke_pane'

    def __init__(self, c):
        self.c = c
        # Careful: we may be unit testing.
        if hasattr(c, 'free_layout'):
            splitter = c.free_layout.get_top_splitter()
            if splitter:
                splitter.register_provider(self)

    def ns_provides(self):
        return [('Rich text CKE editor', self.ns_id)]

    def ns_provide(self, id_):
        if id_ == self.ns_id:
            w = CKEEditor(c=self.c)
            return w
        return None

    def ns_provider_id(self):
        # used by register_provider() to unregister previously registered
        # providers of the same service
        return self.ns_id


# @+node:tbrown.20130813134319.14339: *4* onCreate
def onCreate(tag, key):
    c = key.get('c')

    CKEPaneProvider(c)


# @+node:tbrown.20130814090427.22458: *4* at_rich_check
def at_rich_check(tag, key):
    p = key.get('new_p')

    do = 'close'
    for nd in p.self_and_parents():
        if '@norich' in nd.h or '@norich' in nd.b[:1000]:
            do = 'close'
            break
        if '@rich' in nd.h or '@rich' in nd.b[:1000]:
            do = 'open'
            break

    if do == 'close':
        cmd_CloseEditor(key, at_rich=True)
    elif do == 'open':
        cmd_OpenEditor(key, at_rich=True)


# @+node:tbrown.20130813134319.5692: *4* @g.command('cke-text-open')
@g.command('cke-text-open')
def cmd_OpenEditor(event=None, at_rich=False):
    """Open the rich text editor, hide the regular editor."""
    c = event.get('c')
    splitter = c.free_layout.get_top_splitter()
    rte = splitter.find_child(CKEEditor, '')
    if rte:
        if not at_rich:
            g.es("CKE Editor appears to be open already")
        return
    body = splitter.find_child(QtWidgets.QWidget, 'bodyFrame')
    w = CKEEditor(c=c)
    w.at_rich = at_rich
    splitter = body.parent()
    splitter.replace_widget(body, w)


# @+node:tbrown.20130813134319.5693: *4* @g.command('cke-text-close')
@g.command('cke-text-close')
def cmd_CloseEditor(event=None, at_rich=False):
    """Close the rich text editor, unhide the regular editor."""
    c = event.get('c')
    splitter = c.free_layout.get_top_splitter()
    if not splitter:
        return
    rte = splitter.find_child(CKEEditor, '')
    if not rte:
        if not at_rich:
            g.es("No editor open")
        return
    if at_rich and not rte.at_rich:
        # don't close manually opened editor
        return
    body = splitter.get_provided('_leo_pane:bodyFrame')
    splitter = rte.parent()
    rte.at_rich_close = True
    splitter.replace_widget(rte, body)


# @+node:tbrown.20130813134319.7233: *4* @g.command('cke-text-switch')
@g.command('cke-text-switch')
def cmd_SwitchEditor(event):
    """Switch between regular and rich text editor."""
    c = event.get('c')
    splitter = c.free_layout.get_top_splitter()
    rte = splitter.find_child(CKEEditor, '')
    if not rte:
        cmd_OpenEditor(event)
    else:
        cmd_CloseEditor(event)


# @+node:tbrown.20130813134319.7231: *4* @g.command('cke-text-toggle-autosave')
@g.command('cke-text-toggle-autosave')
def cmd_ToggleAutosave(event):
    """
    Toggle autosaving of changes when you leave a node.

    Be careful not to convert plain text (e.g. source code) to rich
    text unintentionally.  As long as you make no edits, the original
    text will not be changed.
    """
    c = event.get('c')
    c._ckeeditor_autosave = not c._ckeeditor_autosave
    g.es("Rich text autosave " + ("ENABLED" if c._ckeeditor_autosave else "disabled"))


# @+node:ekr.20121128031949.12605: *3* 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: *4* 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: *4* 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: *4* 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: *4* 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: *4* 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: *4* 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: *5* 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: *4* 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: *4* 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: *5* 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: *4* 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: *4* 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: *4* 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: *4* 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: *4* 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: *4* 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: *4* 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: *4* 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: *4* 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.20260820040558.1: ** Temporary attic
# @+node:ekr.20230714143451.1: *3* TestEditFileCommands.test_diff_two_branches
def test_diff_two_branches(self):
    c = self.c
    u = c.undoer
    x = GitDiffController(c=c)

    # Setup the outline.
    root = c.rootPosition()
    root.h = '@file leoGlobals.py'
    root.deleteAllChildren()
    while root.hasNext():
        root.next().doDelete()
    c.selectPosition(root)

    # Run the test in the leo-editor directory (the parent of the .git directory).
    new_dir = g.finalize_join(g.app.loadDir, '..', '..')
    old_dir = os.getcwd()
    os.chdir(new_dir)
    try:
        # Run the command, suppressing output from git.
        expected_last_headline = 'git-diff-branches 6.8.10 6.8.10.dev1'
        try:
            sys.stdout = open(os.devnull, 'w')
            x.diff_two_branches(
                branch1='main',
                branch2='devel',
                fn='leo/core/leoGlobals.py',  # Don't use backslashes.
            )
        finally:
            sys.stdout = sys.__stdout__
        # #3497: Silently skip the test if nothing has changed.
        if c.lastTopLevel() == root:
            return
        self.assertEqual(c.lastTopLevel().h, expected_last_headline)
        u.undo()
        self.assertEqual(c.lastTopLevel(), root)
        u.redo()
        self.assertEqual(c.lastTopLevel().h, expected_last_headline)
    finally:
        os.chdir(old_dir)

# @+node:ekr.20110605121601.18503: *3* LeoQtGui.runScrolledMessageDialog (deleted)
def runScrolledMessageDialog(
    self,
    short_title: str = '',
    title: str = 'Message',
    label: str = '',
    msg: str = '',
    c: Cmdr | None = None,
    **keys: Any,
) -> None:
    if g.unitTesting:
        return None

    def send() -> Any:
        return g.doHook(
            'scrolledMessage',
            short_title=short_title,
            title=title,
            label=label,
            msg=msg,
            c=c,
            **keys,
        )

    if not c or not c.exists:
        << no c error>>
    else:
        if retval := send():
            return retval
        << load viewrendered plugin >>
        if retval := send():
            return retval
        << no dialog error >>
    << emergency fallback >>

# @+node:ekr.20110605121601.18504: *4* << no c error>>
g.es_print_error(
    '%s\n%s\n\t%s'
    % (
        "The qt plugin requires calls to g.app.gui.scrolledMessageDialog to include 'c'",
        "as a keyword argument",
        g.callers(),
    )
)
# @+node:ekr.20110605121601.18505: *4* << load viewrendered plugin >>
pc = g.app.pluginsController
# Load viewrendered (and call vr.onCreate) *only* if not already loaded.
if not pc.isLoaded('viewrendered.py') and not pc.isLoaded('viewrendered3.py'):
    if pc.loadOnePlugin('viewrendered.py'):
        g.blue('viewrendered plugin loaded.')
# @+node:ekr.20110605121601.18506: *4* << no dialog error >>
g.es_print_error(f'No handler for the "scrolledMessage" hook.\n\t{g.callers()}')
# @+node:ekr.20110605121601.18507: *4* << emergency fallback >>
dialog = QtWidgets.QMessageBox(None)
# That is, not a fixed size dialog.
dialog.setWindowFlags(WindowType.Dialog)
dialog.setWindowTitle(title)
if msg:
    dialog.setText(msg)
dialog.setIcon(Icon.Information)
dialog.addButton('Ok', ButtonRole.YesRole)
try:
    if c:
        c.in_qt_dialog = True
    dialog.exec()
finally:
    if c:
        c.in_qt_dialog = False
# @-all
# @@nosearch
# @-leo
