diff --git a/.claude/skills/run-ride/SKILL.md b/.claude/skills/run-ride/SKILL.md new file mode 100644 index 000000000..c4323af21 --- /dev/null +++ b/.claude/skills/run-ride/SKILL.md @@ -0,0 +1,149 @@ +--- +name: run-ride +description: Run, launch, start, screenshot or GUI-test RIDE from source on a headless machine. Drives the real wxPython app (docking panes, tree, menus) via a driver script and asserts what actually rendered. Use for any change to ui/, editor/, or anything wxPython/AUI that unit tests cannot cover. +--- + +# Running RIDE + +RIDE is a wxPython desktop GUI. It has no in-process automation API, so it is driven +from outside: a private Xvfb display, a window manager, XTEST input via `xdotool`, +and screenshots for observation. All of that is wrapped by +`.claude/skills/run-ride/driver.py`. + +Paths below are relative to the repo root. Everything here was executed on Fedora 42, +Python 3.13, wxPython 4.2.x. + +## Prerequisites + +The driver needs these on `PATH` (all were already installed here): + +```bash +for t in Xvfb xdotool import xdpyinfo wmctrl xfwm4 convert; do printf "%-10s %s\n" "$t" "$(command -v $t || echo MISSING)"; done +``` + +On Fedora these come from `xorg-x11-server-Xvfb`, `xdotool`, `ImageMagick`, +`xorg-x11-utils`, `wmctrl`, `xfwm4`. A window manager is **required**, not optional — +see Gotchas. + +No build step: RIDE runs straight from `src/`. + +## Run (agent path) + +```bash +python .claude/skills/run-ride/driver.py up # Xvfb + xfwm4 + RIDE, waits until ready +python .claude/skills/run-ride/driver.py shot /tmp/ride.png +python .claude/skills/run-ride/driver.py down # stops everything, restores settings.cfg +``` + +`up` opens `rtest/testdir` by default; pass another path as an argument. It normalises the +main window to `0,0 1400x900` (so all gesture coordinates are reproducible) and parks the +floating Files pane out of the way. + +Commands: + +| Command | What it does | +|---|---| +| `up [suite]` | start display, WM and RIDE; wait for the main window | +| `down` | stop RIDE/WM/Xvfb, restore `~/.robotframework/ride/settings.cfg` | +| `shot [path]` | screenshot the whole display | +| `windows` | list top-level windows (a floating pane is its own window) | +| `dock-tree` | drag the floating Test Suites pane onto the left dock guide (retries 3x) | +| `float-tree` | drag the docked pane back out into a floating mini-frame | +| `check-tree` | assert the tree actually painted; exit 0 = RENDERED, 1 = BLANK | +| `toggle-tree` | hide/show the tree via the View menu | +| `click X Y` / `key KEY` | raw input | +| `park` / `normalize` | re-park floating panes / re-apply the known window rect | +| `log` | RIDE's stdout+stderr for this run | + +Verified interaction — selecting a node updates the window title: + +```bash +python .claude/skills/run-ride/driver.py click 85 165 +python .claude/skills/run-ride/driver.py windows | head -1 +``` + +``` +0x00400094 0 localhost.localdomain RIDE - Suite +``` + +### check-tree: the assertion that matters + +`check-tree` counts unique colours in the tree pane. A painted tree has hundreds; a blank +pane has ~2. This is the machine-checkable form of "the panel went blank", which is a real +recurring bug class here (fixed in `a75aa8ebd`; wxGTK stops sending paint events to a +reparented `ScrolledWindow`). + +**Always run `dock-tree` immediately before `check-tree`.** Pixels cannot distinguish a +blank pane from a hidden one — with the pane hidden, the region shows the editor +underneath and reads as RENDERED. + +### A/B a GUI regression across commits + +`RIDE_SRC` points the driver at another checkout, so you can run an old commit with today's +driver. This is how the docking fix was verified: + +```bash +git worktree add /tmp/ride-ctrl HEAD~1 +cp -r src/robotide/preferences/configobj/. /tmp/ride-ctrl/src/robotide/preferences/configobj/ +RIDE_SRC=/tmp/ride-ctrl/src python .claude/skills/run-ride/driver.py up +RIDE_SRC=/tmp/ride-ctrl/src python .claude/skills/run-ride/driver.py dock-tree +RIDE_SRC=/tmp/ride-ctrl/src python .claude/skills/run-ride/driver.py check-tree +``` + +Before the fix vs after, same driver, same gesture: + +``` +tree (docked TREE_REGION) unique colours: 2 -> BLANK # HEAD~1, exit 1 +tree (docked TREE_REGION) unique colours: 585 -> RENDERED # HEAD, exit 0 +``` + +Clean up with `git worktree remove --force /tmp/ride-ctrl`. + +## Test suite + +```bash +timeout 300 xvfb-run -a python -m pytest utest/ui/ -q +``` + +81 tests, ~50s. `utest/ui/` is the relevant subset for UI work. Note that **no unit test +covers docking, painting or pane layout** — that is exactly why this driver exists. + +## Run (human path) + +`invoke devel` runs RIDE from source on your own display. Useless headless, and it does not +give you a programmatic handle on the app — prefer the driver above. + +## Gotchas + +- **`PYTHONPATH` must point at `src/`.** Plain `python -m robotide.__init__` imports the + *installed* package from site-packages, so you silently test released code instead of your + edits. The driver sets it and logs the resolved path — check it with `driver.py log | head -1`. +- **A window manager is required.** Without one, AUI floating mini-frames misbehave and + `ClientToScreen` warnings flood the log. The driver starts `xfwm4`. +- **`xdotool key --window ` does nothing.** It sends a synthetic XSendEvent that GTK + ignores. Only XTEST (plain `xdotool key`, i.e. `driver.py key`) works. +- **The F12 accelerator is unreliable** even via XTEST. Use `toggle-tree` (View menu), which + is deterministic. +- **RIDE captures stdout**, so `print()` added inside the app does not reach the terminal. + Write debug output to a file instead. +- **Runs rewrite `~/.robotframework/ride/settings.cfg`** (pane perspective, `opened`, + `docked`). `up` backs it up and `down` restores it — always finish with `down`, or the + user's layout silently changes. +- **Docking needs a genuine stepped drag.** AUI only raises its docking guides after a + stream of motion events, and the drop must land *on* the guide (~(34, 478) at this window + size). Twenty pixels off and the pane just moves instead of docking. `dock-tree` retries + three times because the first drag does sometimes miss. +- **The floating Files pane overlaps the tree region.** Left in place it leaks colours into + `check-tree` and a blank tree reads as RENDERED. `up` and `check-tree` park it. +- **A new `git worktree` has an empty `configobj` submodule**; copy it from the main + checkout as shown above or RIDE will not import. + +## Troubleshooting + +| Symptom | Fix | +|---|---| +| `no RIDE window after 90s` | `driver.py log` — usually an import error from a wrong `RIDE_SRC`. | +| `check-tree` says RENDERED but the screenshot looks empty | The pane is hidden or floating, so the region measured something else. Run `dock-tree` first. | +| `dock-tree` prints `still floating` after 3 attempts | Window geometry drifted; run `normalize`, then retry. | +| Stale `Xvfb` blocks startup | `driver.py down`, then remove `/tmp/.X99-lock`. | +| Want a second instance | `RIDE_DISPLAY=:98 python .claude/skills/run-ride/driver.py up`. | diff --git a/.claude/skills/run-ride/driver.py b/.claude/skills/run-ride/driver.py new file mode 100755 index 000000000..2591abe1e --- /dev/null +++ b/.claude/skills/run-ride/driver.py @@ -0,0 +1,378 @@ +#!/usr/bin/env python3 +"""Headless launcher/driver for RIDE (wxPython GUI). + +RIDE has no in-process automation API, so this drives it from outside via XTEST +(xdotool) on a private Xvfb display, and observes it via screenshots. + + python .claude/skills/run-ride/driver.py up + python .claude/skills/run-ride/driver.py shot /tmp/a.png + python .claude/skills/run-ride/driver.py dock-tree + python .claude/skills/run-ride/driver.py check-tree + python .claude/skills/run-ride/driver.py down + +Run `driver.py help` for the full command list. +""" +import os +import shutil +import signal +import subprocess +import sys +import time + +DISPLAY = os.environ.get('RIDE_DISPLAY', ':99') +SCREEN_W, SCREEN_H = 1400, 900 +REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) +# Point RIDE_SRC at another checkout (e.g. a `git worktree` of an older commit) to +# run that code with this driver -- how you A/B a GUI regression. +SRC = os.path.abspath(os.environ.get('RIDE_SRC', os.path.join(REPO, 'src'))) +RUNDIR = f"/tmp/ride-run{DISPLAY.replace(':', '-')}" +SETTINGS = os.path.expanduser('~/.robotframework/ride/settings.cfg') +SETTINGS_BAK = os.path.join(RUNDIR, 'settings.cfg.bak') +DEFAULT_SUITE = 'rtest/testdir' + +# Geometry of the docked tree pane once the main window is normalised to +# 0,0 SCREEN_W x SCREEN_H. Used by check-tree and the dock/float gestures. +TREE_REGION = (2, 100, 270, 760) # x, y, w, h +LEFT_DOCK_GUIDE = (34, 478) # AUI left docking guide +DOCKED_CAPTION = (60, 89) # "Test Suites" caption when docked + + +def env(): + e = dict(os.environ) + e['DISPLAY'] = DISPLAY + return e + + +def x(*args, **kw): + """Run a command on the driver's display.""" + return subprocess.run(args, env=env(), capture_output=True, text=True, **kw) + + +def xdo(*args): + return x('xdotool', *args).stdout.strip() + + +def need(*tools): + missing = [t for t in tools if not shutil.which(t)] + if missing: + sys.exit(f"missing required tools: {' '.join(missing)}\n" + f"install with: sudo dnf install {' '.join(missing)} # or apt-get install") + + +def pidfile(name): + return os.path.join(RUNDIR, f'{name}.pid') + + +def write_pid(name, pid): + os.makedirs(RUNDIR, exist_ok=True) + with open(pidfile(name), 'w') as f: + f.write(str(pid)) + + +def read_pid(name): + try: + with open(pidfile(name)) as f: + return int(f.read().strip()) + except (OSError, ValueError): + return None + + +def kill(name): + pid = read_pid(name) + if pid: + for sig in (signal.SIGTERM, signal.SIGKILL): + try: + os.kill(pid, sig) + time.sleep(1) + except ProcessLookupError: + break + try: + os.remove(pidfile(name)) + except OSError: + pass + + +def display_up(): + return x('xdpyinfo').returncode == 0 + + +def main_window(): + ids = xdo('search', '--name', '^RIDE - ') + return ids.split('\n')[0] if ids else None + + +def find_window(pattern): + ids = xdo('search', '--name', pattern) + return ids.split('\n')[0] if ids else None + + +def cmd_up(args): + """Start Xvfb + window manager + RIDE, and wait until the main window exists.""" + need('Xvfb', 'xfwm4', 'xdotool', 'import', 'xdpyinfo') + suite = args[0] if args else DEFAULT_SUITE + os.makedirs(RUNDIR, exist_ok=True) + + # RIDE rewrites settings.cfg (pane perspective, opened/docked). Keep the user's copy. + if os.path.exists(SETTINGS) and not os.path.exists(SETTINGS_BAK): + shutil.copy(SETTINGS, SETTINGS_BAK) + print(f"backed up settings.cfg -> {SETTINGS_BAK}") + + if not display_up(): + lock = f"/tmp/.X{DISPLAY.lstrip(':')}-lock" + if os.path.exists(lock): + os.remove(lock) + p = subprocess.Popen(['Xvfb', DISPLAY, '-screen', '0', f'{SCREEN_W}x{SCREEN_H}x24'], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + write_pid('xvfb', p.pid) + for _ in range(30): + time.sleep(0.5) + if display_up(): + break + else: + sys.exit("Xvfb failed to start") + print(f"Xvfb up on {DISPLAY} ({SCREEN_W}x{SCREEN_H})") + + # A window manager is required: without one, AUI floating mini-frames misbehave. + if not read_pid('wm'): + p = subprocess.Popen(['xfwm4'], env=env(), + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + write_pid('wm', p.pid) + time.sleep(2) + print("xfwm4 up") + + # PYTHONPATH is load-bearing: without it, `import robotide` resolves to the + # installed package in site-packages and you test released code, not this tree. + e = env() + e['PYTHONPATH'] = SRC + log = open(os.path.join(RUNDIR, 'ride.log'), 'w') + p = subprocess.Popen( + [sys.executable, '-c', + 'import robotide, sys; sys.stderr.write("robotide from %s\\n" % robotide.__file__); ' + f'from robotide import main; main({suite!r})'], + cwd=REPO, env=e, stdout=log, stderr=subprocess.STDOUT) + write_pid('ride', p.pid) + print(f"launching RIDE (pid {p.pid}) with suite {suite!r} from {SRC}") + + for _ in range(90): + time.sleep(1) + if main_window(): + break + if p.poll() is not None: + sys.exit(f"RIDE exited early; see {RUNDIR}/ride.log") + else: + sys.exit(f"no RIDE window after 90s; see {RUNDIR}/ride.log") + time.sleep(4) # let plugins finish loading and the tree populate + cmd_normalize([]) + park_floaters() + print("RIDE ready") + + +def park_floaters(): + """Move floating panes off the tree region. + + The Files pane floats by default and overlaps TREE_REGION; left where it is, + its contents leak into check-tree and a blank tree reads as RENDERED. + """ + for name in ('^Files$',): + w = find_window(name) + if w: + xdo('windowmove', w, str(SCREEN_W - 340), str(SCREEN_H - 300)) + time.sleep(1.5) + + +def cmd_park(args): + """Move floating panes (e.g. Files) away from the docked tree region.""" + park_floaters() + print("floating panes parked") + + +def cmd_normalize(args): + """Move/resize the main window to a known rect so gesture coords are reproducible.""" + w = main_window() + if not w: + sys.exit("no RIDE main window") + xdo('windowmove', w, '0', '0') + xdo('windowsize', w, str(SCREEN_W), str(SCREEN_H)) + time.sleep(1.5) + xdo('windowactivate', '--sync', w) + print(f"main window normalised to 0,0 {SCREEN_W}x{SCREEN_H}") + + +def cmd_down(args): + """Stop RIDE, the WM and Xvfb, and restore the user's settings.cfg.""" + for name in ('ride', 'wm', 'xvfb'): + kill(name) + if os.path.exists(SETTINGS_BAK): + shutil.copy(SETTINGS_BAK, SETTINGS) + os.remove(SETTINGS_BAK) + print("restored settings.cfg") + print("stopped") + + +def cmd_shot(args): + path = args[0] if args else '/tmp/ride-shot.png' + r = x('import', '-window', 'root', path) + if r.returncode: + sys.exit(f"screenshot failed: {r.stderr}") + print(path) + + +def cmd_windows(args): + print(x('wmctrl', '-l').stdout.rstrip() or '(none)') + + +def cmd_click(args): + px, py = args[0], args[1] + xdo('mousemove', px, py, 'sleep', '0.3', 'click', '1') + time.sleep(1) + print(f"clicked {px},{py}") + + +def cmd_key(args): + """Send a key via XTEST. Never use `xdotool key --window` -- GTK ignores it.""" + w = main_window() + if w: + xdo('windowactivate', '--sync', w) + time.sleep(0.5) + xdo('key', args[0]) + time.sleep(1.5) + print(f"key {args[0]}") + + +def _drag(path, settle=1.0): + sx, sy = path[0] + xdo('mousemove', str(sx), str(sy), 'sleep', '0.4', 'mousedown', '1', 'sleep', '0.4') + for px, py in path[1:]: + # AUI needs a stream of motion events to raise its docking guides. + xdo('mousemove', str(px), str(py), 'sleep', '0.15') + time.sleep(settle) + xdo('mouseup', '1') + time.sleep(3) + + +def win_geometry(wid): + """Return (x, y, w, h) for a window id.""" + out = xdo('getwindowgeometry', '--shell', wid) + g = dict(line.split('=', 1) for line in out.splitlines() if '=' in line) + return int(g['X']), int(g['Y']), int(g['WIDTH']), int(g['HEIGHT']) + + +def cmd_dock_tree(args): + """Drag the floating Test Suites pane onto the left docking guide.""" + gx, gy = LEFT_DOCK_GUIDE + for attempt in (1, 2, 3): + w = find_window('^Test Suites$') + if not w: + print("docked") + return 0 + # Park it somewhere predictable, then grab its AUI-drawn caption bar. + # The caption is ~12px tall at the top of the mini-frame; compute it from + # the real geometry rather than assuming a fixed size. + xdo('windowmove', w, '300', '430') + time.sleep(1.5) + wx_, wy_, ww, wh = win_geometry(w) + cap = (wx_ + ww // 2, wy_ + 6) + _drag([cap, (cap[0] - 20, 460), (380, 470), (300, 475), + (200, gy), (120, gy), (60, gy), (gx, gy)]) + if not find_window('^Test Suites$'): + print(f"docked (attempt {attempt})") + return 0 + print(f"attempt {attempt}: still floating, retrying") + print("WARNING: still floating -- the drop missed the guide") + return 1 + + +def cmd_float_tree(args): + """Drag the docked Test Suites pane out into a floating mini-frame.""" + if find_window('^Test Suites$'): + print("already floating") + return + cx, cy = DOCKED_CAPTION + _drag([(cx, cy), (150, 200), (350, 300), (550, 380), (700, 430)]) + print("floating" if find_window('^Test Suites$') else "WARNING: still docked") + + +VIEW_MENU = (300, 38) # "View" in the menu bar +VIEW_MENU_ITEM_1 = (392, 67) # "View Test Suites Explorer" (first item) + + +def cmd_toggle_tree(args): + """Hide/show the tree via View > View Test Suites Explorer. + + Use this rather than `key F12`: the F12 accelerator does not reliably pick up + synthetic key events here, while the menu path always works. + """ + w = main_window() + if w: + xdo('windowactivate', '--sync', w) + time.sleep(0.5) + xdo('mousemove', str(VIEW_MENU[0]), str(VIEW_MENU[1]), 'sleep', '0.4', 'click', '1') + time.sleep(1.2) + xdo('mousemove', str(VIEW_MENU_ITEM_1[0]), str(VIEW_MENU_ITEM_1[1]), + 'sleep', '0.4', 'click', '1') + time.sleep(3) + print("toggled tree visibility") + + +def cmd_check_tree(args): + """Report whether the docked tree pane has rendered content. + + Counts unique colours in the pane's rectangle. A painted tree has many + (icons, text, selection); a blank pane has a handful. This is the + assertion for the 'tree goes blank after docking' class of bug. + + When the pane floats, its own window is measured. When it is docked, + TREE_REGION is measured. + + PRECONDITION when docked: the pane must be *visible*. Pixels cannot tell a + blank tree pane from a hidden one -- with the pane hidden, TREE_REGION shows + the editor underneath and reads as RENDERED. Always `dock-tree` immediately + before `check-tree`; never trust it straight after `toggle-tree`. + """ + shot = os.path.join(RUNDIR, 'check.png') + floating = find_window('^Test Suites$') + if floating: + where = 'floating pane window' + r = x('import', '-window', floating, shot) + if r.returncode: + sys.exit(f"screenshot failed: {r.stderr}") + crop = [] + else: + where = 'docked TREE_REGION' + park_floaters() + x('import', '-window', 'root', shot) + cx, cy, cw, ch = TREE_REGION + crop = ['-crop', f'{cw}x{ch}+{cx}+{cy}', '+repage'] + r = x('convert', shot, *crop, '-format', '%k', 'info:') + if r.returncode: + sys.exit(f"convert failed: {r.stderr}") + colours = int(r.stdout.strip()) + verdict = 'RENDERED' if colours >= 10 else 'BLANK' + print(f"tree ({where}) unique colours: {colours} -> {verdict}") + return 0 if verdict == 'RENDERED' else 1 + + +def cmd_log(args): + p = os.path.join(RUNDIR, 'ride.log') + print(open(p).read() if os.path.exists(p) else '(no log)') + + +def cmd_help(args): + print(__doc__) + print("commands:") + for name, fn in sorted(COMMANDS.items()): + print(f" {name:<12} {(fn.__doc__ or '').strip().splitlines()[0] if fn.__doc__ else ''}") + + +COMMANDS = { + 'up': cmd_up, 'down': cmd_down, 'shot': cmd_shot, 'windows': cmd_windows, + 'click': cmd_click, 'key': cmd_key, 'dock-tree': cmd_dock_tree, + 'float-tree': cmd_float_tree, 'check-tree': cmd_check_tree, + 'normalize': cmd_normalize, 'park': cmd_park, 'toggle-tree': cmd_toggle_tree, 'log': cmd_log, 'help': cmd_help, +} + +if __name__ == '__main__': + if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS: + cmd_help([]) + sys.exit(0 if len(sys.argv) > 1 and sys.argv[1] == 'help' else 2) + sys.exit(COMMANDS[sys.argv[1]](sys.argv[2:]) or 0) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 610e37df9..4d17f0750 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -9,6 +9,7 @@ and this project adheres to http://semver.org/spec/v2.0.0.html[Semantic Versioni == https://github.com/robotframework/RIDE[Unreleased] === Fixed +- Fixed blank Project Explorer panel when docking. Long time existing issue. - Fixed bad resizing of File Explorer content when opening Test Suites. - Fix selection of items (variables, test names, keywords) from Project Explorer and highlight at Text Editor. - Fixed Tab spacing in Text Editor. When pressing tab the expected spaces were not written, causing failing steps. diff --git a/README.adoc b/README.adoc index c993e0653..d5f1527f0 100644 --- a/README.adoc +++ b/README.adoc @@ -46,7 +46,7 @@ Likewise, the current version of wxPython, is 4.2.5, but RIDE is known to work w `pip install -U robotframework-ride` -(3.9 <= python <= 3.14) Install current development version (**2.2.5dev6**) with: +(3.9 <= python <= 3.14) Install current development version (**2.2.5dev7**) with: `pip install -U https://github.com/robotframework/RIDE/archive/develop.zip` diff --git a/src/robotide/application/CHANGELOG.html b/src/robotide/application/CHANGELOG.html index a78d36a85..eceb5e283 100644 --- a/src/robotide/application/CHANGELOG.html +++ b/src/robotide/application/CHANGELOG.html @@ -1,6 +1,8 @@ Changelog

_Changelog


All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

_1.1. Fixed

  • +Fixed blank Project Explorer panel when docking. Long time existing issue. +
  • Fixed bad resizing of File Explorer content when opening Test Suites.
  • Fix selection of items (variables, test names, keywords) from Project Explorer and highlight at Text Editor. diff --git a/src/robotide/application/releasenotes.py b/src/robotide/application/releasenotes.py index 7b6263127..0b67cbad5 100644 --- a/src/robotide/application/releasenotes.py +++ b/src/robotide/application/releasenotes.py @@ -170,13 +170,12 @@ def set_content(self, html_win, content):
  • 🐞 - The feature Auto-Save may cause a crash of RIDE in certain systems. This was experienced in AlmaLinux 9.6 with Gnome Unity, Python 3.13 and wxPython 4.2.3. The files are correctly saved, but RIDE closes. It is recommended to set the Auto-Save time as zero if this happens.
  • 🐞 - In Grid Editor, when showing settings, scrolling down with mouse or using down is not working. You can change to Text Editor and back to Grid Editor, to restore normal behavior.
  • -
  • 🐞 - The Test Suites Explorer, may be visible or hidden with F12, or toggled floating/docked, but content may - disappear. You should try to make it reappear by toggling Files Explorer, F11, or by editing settings.cfg.

New Features and Fixes Highlights

    +
  • Fixed blank Project Explorer panel when docking. Long time existing issue.
  • Fixed bad resizing of File Explorer content when opening Test Suites.
  • Improved spaces detection in test suites reader.
  • Fix selection of items (variables, test names, keywords) from Project Explorer and highlight at Text Editor.
  • diff --git a/src/robotide/ui/mainframe.py b/src/robotide/ui/mainframe.py index de2d2aa77..388e05073 100644 --- a/src/robotide/ui/mainframe.py +++ b/src/robotide/ui/mainframe.py @@ -296,19 +296,25 @@ def _init_ui(self): """ # DEBUG: self.leftpanel = wx.Panel(self, name="left_panel", size = (275, 250)) if new_ui: # Only when creating UI we add panes - # Tree is always created here - self.tree = Tree(self, self.actions, self._application.settings) + # Tree is always created here, inside a plain panel. AUI must manage the holder and + # never the Tree itself: docking a floating pane reparents the managed window, and a + # reparented wx.ScrolledWindow (which CustomTreeCtrl is) stops receiving paint events + # on wxGTK, leaving a correctly sized but permanently blank panel. The Files pane uses + # the same idiom (FileExplorer is a wx.Panel wrapping its tree_ctrl). + self.tree_holder = wx.Panel(self) + self.tree = Tree(self.tree_holder, self.actions, self._application.settings) self.tree.SetMinSize(wx.Size(275, 250)) self.tree.SetFont(wx.Font(self.fontinfo)) - # self.leftpanel.Bind(wx.EVT_SIZE, self.tree.OnSize) - # self.aui_mgr.AddPane(self.leftpanel, aui.AuiPaneInfo().Name("left_panel").Caption("left_panel").Left()) + tree_sizer = wx.BoxSizer(wx.VERTICAL) + tree_sizer.Add(self.tree, 1, wx.EXPAND) + self.tree_holder.SetSizer(tree_sizer) + self.tree_holder.SetMinSize(wx.Size(275, 250)) # DEBUG: Next was already called from application.py # print(f"DEBUG: mainframe.py RideFrame NEW UI tree caption {_('Test Suites')}") - self.aui_mgr.AddPane(self.tree, + self.aui_mgr.AddPane(self.tree_holder, aui.AuiPaneInfo().Name("tree_content").Caption(_('Test Suites')).CloseButton(True) .LeftDockable(True) ) # DEBUG: remove .CloseButton(False) when restore is fixed - # DEBUG: self.aui_mgr.GetPane(self.tree).DestroyOnClose() # TreePlugin will manage showing the Tree self.actions.register_actions(action_info_collection(_menudata, self, data_nt=self._menudata_nt, container=self.tree)) @@ -502,14 +508,16 @@ def OnFloatDock(self, event): # panelabel = event.pane.caption etype = event.GetEventType() # strs = "Pane %s "%panelabel + # Only the persisted docked/floating state is recorded here. The tree keeps its nodes and + # repaints by itself across the transition, so rebuilding it would just discard the current + # selection; and calling into the AUI manager from these events re-enters its own drag + # handling. if etype == aui.wxEVT_AUI_PANE_FLOATING: # strs += "is about to be floated" if event.pane.name == "file_manager": self.filemgr.update_tree() elif event.pane.name == "tree_content": self._application.treeplugin.set_float_docked(False) - self._application.treeplugin.on_show_tree(None) - self.tree.refresh_view() # elif etype == aui.wxEVT_AUI_PANE_FLOATED: # strs += "has been floated" elif etype == aui.wxEVT_AUI_PANE_DOCKING: @@ -518,8 +526,6 @@ def OnFloatDock(self, event): self.filemgr.update_tree() elif event.pane.name == "tree_content": self._application.treeplugin.set_float_docked(True) - self._application.treeplugin.on_show_tree(None) - self.tree.refresh_view() # elif etype == aui.wxEVT_AUI_PANE_DOCKED: # strs += "has been docked" # print("DEBUG: " + strs + "\n") diff --git a/src/robotide/ui/treeplugin.py b/src/robotide/ui/treeplugin.py index d3d4c846b..edbc455f7 100644 --- a/src/robotide/ui/treeplugin.py +++ b/src/robotide/ui/treeplugin.py @@ -66,7 +66,6 @@ class TreePlugin(Plugin): "docked": True, "own colors": False } - show_count = 0 def __init__(self, application): Plugin.__init__(self, application, default_settings=self.defaults) @@ -81,20 +80,19 @@ def __init__(self, application): self._tree.SetForegroundColour(Colour(7, 0, 70)) self._tree.SetOwnForegroundColour(Colour(7, 0, 70)) """ + # AUI manages the holder panel, not the Tree itself -- see RideFrame._init_ui. + self._holder = self._tree.GetParent() if self._tree else None self._mgr = aui.GetManager(self._tree) self.pane_id = self._tree.GetId() self._model = self.model - self._tree.Bind(wx.EVT_SHOW, self.on_show_tree) - self._tree.Bind(wx.EVT_MOVE, self.on_tab_changed) - # self._tree.Bind(aui.wxEVT_AUI_PANE_CLOSED, self.toggle_view) + # Deliberately not bound to EVT_SHOW/EVT_MOVE: rebuilding the tree from a visibility or + # move event destroys every node (and its handler data) as a side effect of the panel + # merely becoming visible. Repopulating happens on explicit model changes instead. # parent, action_registerer, , default_settings={'collapsed':True} self.opened = self.settings['opened'] - self._pane = self._mgr.GetPane(self._tree) + self._pane = self._mgr.GetPane("tree_content") self.font = wx.Font(self._app.fontinfo) # self._tree.GetFont() self._tree.SetFont(self.font) - self.show_count = 0 - # print(f"DEBUG: TreePlugin init self.pane_id={self.pane_id} \n" - # f"self._pane = {self._pane}") def register_frame(self, parent=None): if parent: @@ -103,13 +101,11 @@ def register_frame(self, parent=None): register = self._mgr.InsertPane else: register = self._mgr.AddPane - register(self._tree, wx.lib.agw.aui.AuiPaneInfo().Name("tree_content"). + register(self._holder or self._tree, wx.lib.agw.aui.AuiPaneInfo().Name("tree_content"). Caption(_('Test Suites')).CloseButton(True).LeftDockable(True)) self._mgr.Update() def enable(self): - # DEBUG: This does not work (in KDE/Plasma on Fedora 42), the panel has no tree, when we dock from floating - # DEBUG: On other O.S. it may work, so we leave the feature on. self.register_action(ActionInfo(_('View'), _('View Test Suites Explorer'), self.toggle_view, shortcut='F12', doc=_('Show Test Suites tree panel'), @@ -135,7 +131,6 @@ def close_tree(self): """ def close_tree(self): - self.show_count = 0 self.opened = False self._mgr = aui.GetManager(self._app.frame) # print(f"DEBUG: TreePlugin ENTER close_tree self._mgr={self._mgr} _tree={self._tree} == frame {self.frame.tree}") @@ -201,20 +196,12 @@ def toggle_view(self, event): # print(f"DEBUG: TreePlugin ENTER toggle_view {event=} in not None tree is {self.opened}") self.save_setting('opened', not self.opened) if not self.opened: - self.show_count = 0 self.opened = True self.on_show_tree(None) else: self.close_tree() def on_show_tree(self, event): - # print(f"DEBUG: TreePlugin on_show_tree ENTER {event=}" - # f" COUNTER={self.show_count} event. == tree? {self._tree}") - self.show_count += 1 - if self.show_count >= 2: - # print(f"DEBUG: TreePlugin on_show_tree COUNTER={self.show_count} >=2 returning") - self.show_count = 0 - return __ = event if not self._parent: self._parent = self.frame @@ -238,6 +225,14 @@ def on_show_tree(self, event): self._tree.Show(True) # print(f"DEBUG: treeplugin on_show_tree {html_font_face=} {html_font_size=}") self._tree.SetMinSize(wx.Size(200, 225)) + # The pane is the holder panel, so it has to be shown too, otherwise the Tree above is + # only made visible inside a still-hidden parent (this is the F12 / restore path). + if self._holder: + self._holder.Show(True) + self._holder.SetMinSize(wx.Size(200, 225)) + pane_info = self._mgr.GetPane("tree_content") + if pane_info.IsOk(): + pane_info.Show() # self.aui_mgr.DetachPane(self._tree) # self.aui_mgr.Update() # DEBUG: Let's use own method @@ -259,7 +254,6 @@ def on_show_tree(self, event): self._tree.Raise() self.save_setting('opened', True) self._mgr.Update() - self._tree.populate(self._model) self._update_tree() def set_float_docked(self, state: bool): @@ -269,15 +263,10 @@ def on_tree_selection(self, message): if self.is_focused(): self._tree.tree_node_selected(message.item) - def on_tab_changed(self, event): - __ = event - self._update_tree() - def _update_tree(self, event=None): __ = event # print(f"DEBUG: treeplugin.py TreePlugin _update_tree called model={self._model}") - self._tree.populate(self._model) - self._tree.refresh_view() + self._tree.populate(self._model) # populate() already calls refresh_view() self._tree.Update() diff --git a/src/robotide/version.py b/src/robotide/version.py index 9474cba31..424ad307a 100644 --- a/src/robotide/version.py +++ b/src/robotide/version.py @@ -15,4 +15,4 @@ # # Automatically generated by `tasks.py`. -VERSION = 'v2.2.5dev6' +VERSION = 'v2.2.5dev7'