Action plugin — complete reference

This notebook demonstrates all five event hooks available in the per-instance plugin system:

Hook

Event

Signature

rdown_handler

Right mouse-button press

(viewer, MouseContext) -> None

motion_handler

Mouse motion (any button)

(viewer, MouseContext) -> None

ldown_handler

Left mouse-button press

(viewer, MouseContext) -> None

key_handler

Key press

(viewer, KeyboardSnapshot) -> bool

paint_handler

OpenGL paint (after data, before UI)

(viewer) -> None

The plugin built here lets the user:

  • Right-click → add a marker point

  • Left-click → select / highlight the nearest previously added point

  • Mouse motion → show live coordinates in the status bar

  • Ctrl+Z → undo last marker

  • Ctrl+C → clear all markers

  • Paint hook → draw crosses on every marker in OpenGL

1 — wx startup

Important: always use %gui wx in a notebook — never wx.App().
wx.App() captures the event loop and blocks the kernel.
[1]:
import sys
%gui wx

2 — Create the MapViewer

[2]:
from wolfhece.PyDraw import WolfMapViewer

viewer = WolfMapViewer(None, title="Plugin complete demo", w=1200, h=800)
viewer.Show()
INFO:root:Importing wolfhece modules
INFO:root:wolfhece modules imported
INFO:wolfhece.tablet_wintab:WinTab : contexte ouvert sur HWND=0x20906F6 (pressure_max=32767)
[2]:
False

3 — Plugin state

All mutable state lives in plain Python objects — accessible from anywhere in the notebook.

[3]:
import numpy as np
from wolfhece._viewer_plugin_handlers import MouseContext, KeyboardSnapshot

# ── Persistent state ──────────────────────────────────────────────────────
markers: list[tuple[float, float]] = []   # list of (x, y) world positions
selected_idx: list[int] = [-1]           # mutable box for the selected marker index

ACTION_ID = 'mark points'               # action string — arbitrary lowercase

print(f"State initialised — action id: '{ACTION_ID}'")
State initialised — action id: 'mark points'

4 — Handler definitions

4a — Right-click: add a marker

[4]:
def _rdown_add_marker(v: WolfMapViewer, ctx: MouseContext) -> None:
    """Right-click adds a marker at the snapped cursor position."""
    markers.append((ctx.x_snap, ctx.y_snap))
    n = len(markers)
    print(f"[Marker #{n}]  X={ctx.x_snap:.3f}  Y={ctx.y_snap:.3f}")
    v.Paint()   # force immediate redraw to show the new cross

4b — Left-click: select nearest marker

[5]:
def _ldown_select_nearest(v: WolfMapViewer, ctx: MouseContext) -> None:
    """Left-click selects the marker closest to the cursor."""
    if not markers:
        return
    pts = np.array(markers)
    dists = np.hypot(pts[:, 0] - ctx.x, pts[:, 1] - ctx.y)
    idx = int(np.argmin(dists))
    selected_idx[0] = idx
    x, y = markers[idx]
    print(f"Selected marker #{idx + 1}  X={x:.3f}  Y={y:.3f}  (dist={dists[idx]:.1f})")
    v.Paint()

4c — Mouse motion: live coordinates in status bar

[6]:
def _motion_show_coords(v: WolfMapViewer, ctx: MouseContext) -> None:
    """Display cursor world-coordinates in the viewer status bar."""
    v.set_statusbar_text(f"X={ctx.x:.2f}  Y={ctx.y:.2f}  |  {len(markers)} marker(s)")

4d — Key handler: Ctrl+Z undo, Ctrl+C clear

The key handler must return True to consume the event (prevent the default viewer behaviour).

[7]:
import wx

def _key_handler(v: WolfMapViewer, kb: KeyboardSnapshot) -> bool:
    """Ctrl+Z = undo last marker.  Ctrl+C = clear all."""
    if not kb.ctrl:
        return False   # let default processing handle non-ctrl keys

    if kb.key_code == ord('Z'):
        if markers:
            removed = markers.pop()
            if selected_idx[0] >= len(markers):
                selected_idx[0] = len(markers) - 1
            print(f"Undo — removed marker at X={removed[0]:.3f}  Y={removed[1]:.3f}")
            v.Paint()
        return True    # consumed

    if kb.key_code == ord('C'):
        markers.clear()
        selected_idx[0] = -1
        print("All markers cleared.")
        v.Paint()
        return True    # consumed

    return False       # not our key — let default process it

4e — Paint hook: draw markers in OpenGL

The paint hook runs after all data layers (_plotting()) but before the UI overlays (toolbar, palette, etc.).
Use raw OpenGL.GL calls — the projection matrix is already set to world coordinates.
[8]:
from OpenGL.GL import (
    glBegin, glEnd, glVertex2f, glColor4f, glLineWidth,
    GL_LINES,
)

# Cross size as a fraction of the visible width
CROSS_FRACTION = 0.008

def _paint_markers(v: WolfMapViewer) -> None:
    """Draw a cross at every marker; selected marker is highlighted."""
    if not markers:
        return

    half = (v.xmax - v.xmin) * CROSS_FRACTION

    glLineWidth(2.0)
    glBegin(GL_LINES)
    for i, (mx, my) in enumerate(markers):
        if i == selected_idx[0]:
            glColor4f(1.0, 0.8, 0.0, 1.0)   # gold = selected
        else:
            glColor4f(1.0, 0.0, 0.0, 1.0)   # red = normal
        glVertex2f(mx - half, my);  glVertex2f(mx + half, my)   # horizontal bar
        glVertex2f(mx, my - half);  glVertex2f(mx, my + half)   # vertical bar
    glEnd()
    glLineWidth(1.0)

5 — Register and activate

[9]:
viewer.register_action(
    ACTION_ID,
    rdown_handler  = _rdown_add_marker,
    ldown_handler  = _ldown_select_nearest,
    motion_handler = _motion_show_coords,
    key_handler    = _key_handler,
    paint_handler  = _paint_markers,
)

viewer.start_action(
    ACTION_ID,
    'Right-click: add marker | Left-click: select nearest | Ctrl+Z: undo | Ctrl+C: clear'
)
print(f"Action '{ACTION_ID}' registered and active.")
INFO:root:ACTION : Right-click: add marker | Left-click: select nearest | Ctrl+Z: undo | Ctrl+C: clear
Action 'mark points' registered and active.
[Marker #1]  X=2.956  Y=20.453
[Marker #2]  X=22.024  Y=25.672
Selected marker #1  X=2.956  Y=20.453  (dist=0.2)
Selected marker #2  X=22.024  Y=25.672  (dist=0.4)
Selected marker #1  X=2.956  Y=20.453  (dist=0.4)

6 — Inspect markers

[10]:
print(f"{len(markers)} marker(s)  |  selected index: {selected_idx[0]}")
for i, (x, y) in enumerate(markers):
    flag = " ← selected" if i == selected_idx[0] else ""
    print(f"  {i+1:3d}.  X={x:.3f}  Y={y:.3f}{flag}")
2 marker(s)  |  selected index: 0
    1.  X=2.956  Y=20.453 ← selected
    2.  X=22.024  Y=25.672

7 — Export markers as numpy array

[11]:
if markers:
    pts = np.array(markers, dtype=float)
    print(f"Shape: {pts.shape}")
    print(pts)
else:
    print("No markers yet.")
Shape: (2, 2)
[[ 2.95636527 20.45272155]
 [22.02393162 25.67233468]]

8 — Deactivate and clean up

[12]:
viewer.end_action('End mark points')
viewer.unregister_action(ACTION_ID)
print("Plugin deactivated and unregistered.")
INFO:root:ACTION : End mark points
Plugin deactivated and unregistered.

Appendix — Available hooks summary

viewer.register_action(
    'my action',

    # (viewer, MouseContext) -> None
    rdown_handler  = ...,   # right mouse-button pressed
    motion_handler = ...,   # mouse moved (any button state)
    ldown_handler  = ...,   # left mouse-button pressed

    # (viewer, KeyboardSnapshot) -> bool
    # Return True to consume the key event (prevents viewer default).
    key_handler    = ...,

    # (viewer) -> None   — raw OpenGL, world-coordinate projection active
    # Called: after all _plotting() layers, before sculpt cursor + UI overlays.
    paint_handler  = ...,
)

MouseContext attributes

Attribute

Type

Description

x, y

float

Raw world coordinates

x_snap, y_snap

float

Grid-snapped world coordinates

x_pixel, y_pixel

int

Screen pixel coordinates

shift, ctrl, alt

bool

Keyboard modifiers

left_down, right_down, middle_down

bool

Button states during motion

pressure

float

Stylus pressure [0, 1] (1.0 = mouse)

KeyboardSnapshot attributes

Attribute

Type

Description

key_code

int

ord('A'), wx.WXK_F1, etc.

ctrl, shift, alt

bool

Modifiers

is_down

bool

True = key-down event

held

frozenset[int]

All non-modifier keys currently held (polled)