0.6 — Application Settings, v-Prefixed Widgets & Native Menubar#
The 0.6 series adds per-project application settings, a family of inheritance-aware rounded widgets, native Windows menubar control, and developer-authored, user-selectable tab styling.
0.6.0 — Application Settings, v-Prefixed Widgets & Autocomplete Fixes#
Released.
Per-project application settings stored in .VIS/settings.json, accessed via
Project.Settings and surfaced through a built-in tabbed Settings window,
plus the v-prefixed widget family and two AutocompleteEntry popup fixes.
Storage & API#
ProjectSettings(VIStk/Structures/_Settings.py) —project.Settings.get(key, default)/.set(key, value)/.save(), backed by.VIS/settings.json(newVINFO.p_settingspath).getresolution order: stored override → explicitdefaultargument → frameworkDEFAULTStable →None.A full default
settings.jsonis generated atVIS newand on first Host launch (ProjectSettings.ensure_file(), idempotent) so all options are visible/editable; in memory only genuine overrides are tracked andsave()writes the complete resolved set back. A missing or corrupt file falls back to defaults without crashing.effective()(full resolved map, for the UI),reset(key),__contains__, and adirtyflag — the Host skips the shutdown write when nothing changed.Saved automatically on Host shutdown. The capture commits only after every window has closed without veto, so a vetoed quit leaves settings untouched.
Window & display#
window.default_width/default_height/default_align(center + 8 compass points) /min_width/min_heightapplied to the session’s first window.window.remember_geometry— restore the primary window’s last size and position.window.fullscreen_on_launch— open the first window maximized.
Host & startup#
host.start_with_os— toggles the Windows startup-registry entry (_register_startup/unregister_startup), reconciled on Save; disabled and not persisted over on non-Windows.host.remember_tabs— reopen the previous session’s screens as tabs. Themax_tabslimit is bypassed during restore so a remembered session isn’t truncated; screens no longer in the project are dropped silently.
Appearance#
appearance.font_family/font_sizeapplied at launch to Tk’s named fonts (TkDefaultFontetc.), which default and ttk widgets inherit.appearance.color_scheme— stored placeholder for the styles system (no effect yet).Live re-styling of already-open windows is deferred to 0.6.2.
Notifications#
notifications.enabled(stored; consumed by the 0.9 Toast widget) andnotifications.duration_ms— now the defaultInfoRow.show_bannerduration when a caller omits it.
Settings UI#
Tabbed (
ttk.Notebook) settings UI opened from a framework-provided HostMenu > Settings entry. The entry is opt-in per project viahost.settings_menuinproject.json(defaultfalse). It opens as a single-instance tab, falling back to a modal window when no chromed window exists. Save / Cancel / Restore Defaults; controls seeded fromeffective(); Save writes overrides-only and rejects negative / non-numeric numeric input.host.register_settings_panel(name, setup_fn)— apps contribute their own tabs;setup_fn(parent_frame)builds into the tab body (mirrors a screen’ssetup). A panel that raises shows an inline error rather than a blank tab.
Settings scope notes#
The spec’s tray items (start-minimized-to-tray, a tray Settings entry) were dropped: the 0.5.3 always-Host refactor removed the system tray, so the Host now lives only as long as its windows.
Remember-open-tabs is a screens-only restore: the open screens reopen as tabs in one window; split-pane layouts and multiple windows are not reconstructed.
Window-size settings apply to the session’s primary window; drag-detached / popped-out windows keep the default size.
v-Prefixed Widgets#
Drafted as 0.6.2; shipped in 0.6.0.
A family of v-prefixed widgets in VIStk.Widgets that subclass the
classic tk widgets — ttk cannot take per-instance bg/fg/font,
so a themed widget can’t be recoloured on its own. They (1) inherit visual
properties from their parent by default and (2) optionally render rounded
corners, consolidating the “pill / chip / card” pattern screens otherwise
hand-roll with Canvas polygons.
Widget |
Base and behaviour |
|---|---|
|
|
|
|
|
|
|
|
|
|
vWidget base — VIStk/Widgets/_vWidget.py
A pure mixin (subclasses
object, nevertk.Widget), combined with a native widget by multiple inheritance:class vLabel(vWidget, Label).vWidget.__init__runs first in the MRO — computes inheritance, pops the rounded kwargs — then cooperativesuper().__init__()creates the Tcl widget exactly once, so avLabelgenuinely is both.Parent inheritance — each subclass sets
_INHERIT; any of those options the caller omits are filled from the parent at construction (explicit options always win). Classic parents are read viacget, ttk parents viaStyle().lookup(...)with aSystemButtonFacefallback. The snapshot is taken at construction;refresh()re-pulls on demand.Rounded corners — opt in with
radius(0means a plain native widget, no extra machinery). Classic widgets are always rectangular, so the look is built from an anti-aliased PIL rounded rectangle (4x supersampled, Lanczos-downscaled) regenerated on<Configure>; corners are painted with the parent background so they blend on a solid-colour parent. Colours resolve throughwinfo_rgb, so every Tk colour name works. Optionaloutline/outline_width/corner_bg.``radius_style`` —
"pixels"(default) or"percent", whereradiusis a percentage of the maximum round (half the short side), soradius=100, radius_style="percent"is a full pill or circle. Resolved byeffective_radius(radius, style, w, h)from the live size inside every render path, so a percentage radius survives resizes; the result is always clamped to half the short side.Runtime recolour —
configure()/config()repaints the corners live when a paint-affecting option changes (_REPAINT_OPTS;vButtonaddsstate), so hover and disabled states reach the corners.Clobber-proof resize repaint — the
<Configure>repaint is installed on a dedicated bindtag (_RENDER_TAG) routed through one class-level dispatcher, so a caller’s ownwidget.bind("<Configure>", fn)withoutadd="+"can no longer silently replace it and leave the widget square.make_rounded_image(...),rounded_pil_image(...),effective_radius(...)andvWidgetitself are exported so callers can build further v-widgets.
Two rounding strategies, because leaves and containers have different problems:
``RoundedLeaf`` (
VIStk/Widgets/_vLeaf.py) — a classic Label/Button has one image slot, which both the rounded fill and a caller’simage=want. In fill mode (no caller image) the fill is painted into the slot withcompound="center"and the widget’s own text draws over it, which works at any radius including a true circle. In tile mode (the caller passedimage=) the slot is left alone soimage/compound/anchorbehave exactly like native tkinter, and the corners are instead fourr``x``rcrops of the same rounded image plus thin edge strips carrying the outline. Strip thickness is measured from the rendered image (_border_band) rather than assumed to equaloutline_width— the supersampled outline bleeds wider, and fixed-width strips clipped it. Tiles are opaque, so large radii need fill mode.``RoundedContainer`` (
VIStk/Widgets/_vContainer.py) — shared machinery forvFrameandvLabelFrame, which differ only by native base. Draws the fill onto a lowered backgroundLabel, then insets every child byceil(radius*(1 - 1/sqrt2))across all geometry managers (.Layout.cell,place,pack,grid), because a Tk child is an opaque rectangle that would square off any corner it reaches. The inset is invisible when the child shares the frame’sbg. Whole-pixel insetting can still leave a sub-pixel notch on an outlined frame, so a ~1px corner patch — an exact crop of the border image — is drawn on the child, above its content, at only the corners a child actually occupies._skip_child()excludes children the machinery must not touch (the background label,vLabelFrame’s title).Layout.margin(VIStk/Objects/_Layout.py) — a uniform pixel inset on aLayout: everycell()shrinks away from the frame edges while inter-cell boundaries stay shared. Layered on as a fixed pixel offset, so it stays correct on resize.
vLabelFrame needs one extra trick: a LabelFrame lays children out in a
content area below the title band, so a background image placed there misses
the top and bottom border. Rounded vLabelFrame instead floats the fill image
across the whole frame (content-origin offset negated, relwidth/
relheight cleared) and keeps the title on top by routing it through a
labelwidget — an internal Label mirroring text/fg/font, or
the caller’s own, lifted above the fill. Tk still positions it per
labelanchor, so the title breaks the rounded border exactly like a native
one; configure/cget/widget["text"] proxy to it transparently. The
native rectangular border is flattened so the rounded outline is the only
chrome.
Native-option discoverability#
tkinter hides each widget’s options behind **kw and only lists them in the
__init__ docstring, so vButton(...) gave callers no hint that
command/relief/anchor/… are accepted. Both surfaces now expose
them:
help()/ REPL / Sphinx —vWidget.__init_subclass__lifts the native option block from the tk base’s__init__.__doc__and appends it to each v-widget’s__init__doc at class-creation time, so it is always in sync with the installed Tk.Editor hover / autocomplete — each
__init__types**kwargs: Unpack[_XKw](VIStk/Widgets/_vtypes.pyTypedDicts mirroring<widget>.keys()). TheUnpack/_vtypesimports areTYPE_CHECKING-only, so there is zero runtime cost and no new runtime dependency.pillowadded topyproject.tomldependencies — already used byObjects/_VIMG.py, previously undeclared.
AutocompleteEntry popup scoping & tracking#
Drafted as 0.6.3; shipped in 0.6.0. Two fixes to the suggestion popup in
VIStk/Widgets/_AutocompleteEntry.py.
Popup scoped to its own window — the popup was a borderless
Toplevelpinned withattributes("-topmost", True), which is global to the desktop: while suggestions showed it floated above every application, so any Alt-Tab, notification or focus race left an orphan-looking box over whatever app was now active. It is nowtransient(top)+lift(top)to its own toplevel — above its own window (and out of the taskbar) only.overrideredirectstays, since borderless is right for a dropdown.Popup tracks window move/resize — geometry was computed once in
_show_popup, so moving or resizing the parent while suggestions were open stranded the popup at stale screen coordinates. A<Configure>handler on the toplevel now re-runs positioning (extracted into_position_popup), keeping the popup glued below the entry and matching its width. Positioning is deferred toafter_idlebecause a toplevel<Configure>fires before the geometry manager re-lays-out the entry.Clobber-proof cleanup — that handler is routed through a per-instance private bindtag (
_AutocompletePopup<id>) rather than a directtop.bind(...), so hiding the popup removes only our own binding and can never clobber another<Configure>handler on the window. Mirrors the v-widget bindtag approach above.
0.6.1 — Native Menubar Images, Right-Aligned Entries & Separate Window Title-Bar Icon#
Released.
Top-level HostMenu entries can now carry an image on the real Windows
menubar strip and be right-aligned on it. Tk accepts image= on a
menubar entry but the native Windows menu never rendered it, and Tk exposes no
right-justify at all — so VIStk patches the actual HMENU Tk built, after
Tk builds it. Everything degrades gracefully off Windows.
_MenuNative#
New private module VIStk/Widgets/_MenuNative.py (ctypes / user32,
SetMenuItemInfoW).
available()/menu_height()— platform gate (Windows only) and the native bar height (SM_CYMENU, fallback 20).pil_to_hbitmap(img)/delete_hbitmap(h)— PIL image to a 32bpp premultiplied-alpha top-down DIB section (PARGB), so the themed menu alpha-blends it cleanly; plain RGBA draws black halos.patch(window, position, hbitmap=..., right=...)— resolves the wrapper window’s HWND viawm_frame()(notwinfo_id()), then read-modify-writesfType(MFT_RIGHTJUSTIFY) and/or setshbmpItem(MIIM_BITMAP) by position, and callsDrawMenuBar. All VIStk menus aretearoff=0, so the Tk entry index equals the native position.current_hbitmap(window, position)reads a patch back, for cheap verification.Win32 quirk worth knowing:
MFT_RIGHTJUSTIFYright-aligns the patched item and every item after it.
HostMenu#
add_project_command(label, command, image=None, compound=None, align=None)—imagemay now also be aPIL.Image.Image: on Windows it is rendered natively on the bar and never handed to Tk; elsewhere it falls back toImageTk.PhotoImage(reference kept alive on the menu), which the Tk-drawn X11 menubar renders.align="right"right-justifies the entry natively, with or without an image; ignored off Windows.set_native_image(label, pil_image)— swap an entry’s bitmap in place viaSetMenuItemInfowithout touching the Tk entry; anentryconfigurewould make Tk rebuild the native menu and drop every patch. Frees the replacedHBITMAP. For live badges that re-render on state change.refresh_native()— re-apply every registered patch by each label’s current index (labels that no longer resolve are skipped). Needed because Tk rebuilds the native menu on every Tk-side mutation and silently drops out-of-band patches, so every menubar-mutating method (attach,set_project_items,add_project_command,clear_project_items,set_screen_items,clear_screen_items,build_shared_menu,apply_overrides,reset_overrides,restore_defaults) now ends by scheduling one coalesced re-patch onafter_idle.<Map>re-applies too, and<Destroy>frees allHBITMAPs. Public as an escape hatch for callers that mutatemenubardirectly.native_menubar_supported()/native_menu_height()— static delegates to_MenuNativeso callers can pick the native path and size their bitmap without importing the private module.Ordering with a right-aligned entry — since
MFT_RIGHTJUSTIFYdrags all subsequent items right, once any right-aligned entry exists the add paths insert new left-side entries before the leftmost right-aligned entry instead of appending; right-aligned entries themselves still append at the end.Host.register_menubar_accessoryremains the path for live widgets at the menubar’s right edge; its docstring now points static text/bitmap badges at the native path.
Separate window title-bar icon#
A window’s title-bar (chrome) icon and its taskbar icon can now differ.
Windows keeps two icon slots per window — ICON_SMALL (title bar, alt-tab)
and ICON_BIG (taskbar) — but Tk’s iconphoto fills both with one image,
so previously they were always identical. VIStk now drives them independently,
degrading to shared-image behaviour off Windows and when unconfigured.
Project.d_window_icon(VIStk/Structures/_Project.py) — read fromdefaults.window_iconinproject.json(optional;Nonepreserves the prior behaviour). The only project-level way to set the window title-bar icon. The taskbar icon remainsd_icon(defaults.icon).Screen.window_icon(VIStk/Structures/_Screen.py) — read from a screen’swindow_iconinproject.json. Overridesd_window_iconfor the title bar only when the screen owns a standalone (chromeless) window. Tabbed screens share a chromed window that may host other screens, so theirwindow_iconis ignored by design.DetachedWindow._load_icon(VIStk/Objects/_DetachedWindow.py) now fills the two slots separately: taskbar (ICON_BIG) viaiconphotoexactly as before, title bar (ICON_SMALL) fromd_window_icon, overridable by a chromeless screen’swindow_icon. Because_load_icononly receives ascreen_namefor chromeless windows, a tabbed screen can never repaint its shared window’s icon — the constraint is structural, not a runtime check._apply_titlebar_icon(icon_name)— Windows-only; resolves the wrapper HWND viaGetParent(winfo_id())and setsICON_SMALLwithWM_SETICON. No-op elsewhere._titlebar_hicon(icons_dir, icon_name)plus_HICON_CACHE— builds an HICON from any PIL-readable icon (rendered to a small temp ICO andLoadImageW’d), cached process-wide so handles are built once and outlive theWM_SETICONcall — Windows does not copy the icon.
0.6.2 — Tab Styles#
Released.
Developer-authored, user-selectable window chrome. The tab bar’s colours and
shape now come from a named style resolved against two orthogonal axes —
colour scheme (light/dark) and a TabStyle preset — instead of
hardcoded greys. Developers curate the looks; the end user picks from that menu
in Settings > Appearance > Tab style. classic on the light scheme is
byte-identical to the pre-styles bar.
VIStk.Styles#
New package (_palette.py, _tabstyle.py).
Palette— every chrome colour as a named role (bar_bg,tab_active,tab_inactive,tab_hover,close_hover,separator,accent,tab_fg,tab_active_fg, focused/unfocused/drag variants, …).LIGHTreproduces the historical greys exactly;DARKis a neutral dark scheme;base_palette(scheme)picks one.TabStyle— a look recipe:indicator(none/underline/topline),separators,radius, partial paletteoverrides(a"$role"value copies another resolved role so overrides stay scheme-aware), and anaccentshortcut.TabStyle.from_preset(base, ...)derives a new look from a built-in.resolve(scheme, name) -> ResolvedStyle— concretePalette+ render flags; never raises on bad input (unknown style →classic, unknown scheme → light).Four built-in presets:
classic(grey fill cue + separators),underline(flat, accent bar under the active tab),topline(fill cue + accent bar on top),pill(fully-rounded accent capsule).
TabBar styling API#
Process-wide class state; applies live to every open bar.
TabBar.setStyle(name)— switch to one of the built-ins by name ("classic"/"underline"/"topline"/"pill"); raisesValueErrorfor an unknown name.TabBar.setPalette(*, bar=, tab=, selected=, text=, close=, selected_text=)— recolour the active style live. Each argument is a Tk colour (name or hex); omitted ones keep their value.bar→ strip bg,tab→ unselected tab,selected→ selected tab,text→ label + ✕ colour,close→ the ✕ close-button highlight,selected_text→ the selected tab’s text only. Overrides are sticky: stored in_palette_overridesand re-applied on top of every resolved style, so they survive asetStyleswitch and the Host applying the user’s saved pick at launch.register_tab_style(name, style)/offer_styles(names, default=)— author custom looks and curate the Settings menu (typically fromScreens/styles.py).set_tab_style(style, scheme=)— the low-level apply used by the Host; accepts a registered name or aResolvedStyle, layers the stickysetPaletteoverrides, and broadcasts to every bar in_TABBAR_REGISTRY.apply_style(resolved)— per-bar live recolour; rebuilds the tab widgets only when the corner radius crosses 0 (plainButton↔ rounded pill), suppressing navigation callbacks so it fires no screen focus.
Rendering#
Per-corner rounding —
vWidget/vButton/rounded_pil_imagetake acorners=(tl, tr, br, bl)tuple (PIL’srounded_rectangle). A pill tab is one capsule from two widgets: the label rounds its left end, the ✕ its right end (same fill, abutting). Newmax_radius(w, h, corners)clamps a one-sided capsule end toheight/2(its two arcs stack vertically) instead ofmin(w, h)/2, so a narrow ✕ cap matches the wide label;effective_radiustakes the corners too.Active-tab geometry — tabs fill the full bar height and butt flush to the left edge; the ✕ glyph is enlarged and its foreground tracks the label (white on the active pill, black otherwise).
Indicator timing — the underline/topline accent bar re-places itself on
<Configure>and retries until a freshly-packed tab has real geometry, so it shows on the launch tab and on newly-opened tabs without needing a click.
Wiring#
Hostresolvesappearance.tab_style×appearance.color_schemeon its first update (afterScreens/styles.pyis imported, before the first window); an unoffered pick falls back to the app default.appearance.tab_styleadded toProjectSettings.DEFAULTS; Settings > Appearance gains a live “Tab style” dropdown.Screens/styles.pyscaffold (andForm.zip) curates the menu and shows a commented custom-style example;host.txtimports it.VIStk.Stylesadded to thepyproject.tomlpackages list so wheels include the new package.
Fixes#
Pane focus dimming now keys off window-level
<Activate>/<Deactivate>instead of<FocusIn>/<FocusOut>, which fired on the toplevel whenever focus moved to a child widget — so the active tab stays highlighted while you work inside the window and only dims when another window takes focus.``DateEntry`` popup flip — the calendar popup now flips to open above the entry when opening below would spill past the bottom of the containing window (or screen) — e.g. a date field near the bottom of a form — so it opens up into the window instead of off the bottom edge. Falls back to below when there is no room above either.
0.6.3 — Tab Labels, Single-Instance Windows & Rounded-Button Fixes#
Released.
Tab labels a screen can own outright, single-instance behaviour for chromeless
windows, a released-binary packaging fix, and two rounded-vButton rendering
bugs.
Tab label replacement#
set_tab_info(frame, info, replace=True) makes the info string the whole
tab label instead of a "<screen name> --- <info>" suffix — for screens that
already supply a self-identifying label ("WO 21930") where the screen name
would only repeat it.
Threaded through
TabManager.set_tab_info(key, info, replace=False)and stored per tab asreplace_label. An empty info string always falls back to the screen name, so a tab is never blank.Affects the visible label only —
display_nameandbase_nameare untouched, so tab lookup, duplicate-label uniquification and per-tab namespace resolution are unaffected.DetachedWindowmirrors it in the window title: areplacetab titles as"<project>: WO 21930"rather than repeating the screen name.
Chromeless single-instance windows#
Host._open_chromeless() now performs the same lookup _open_tab() does: a
screen marked single_instance that is already open is focused, deiconified
and raised instead of opening a second window. Chromeless windows still register
their screen as a tab, so the existing instance is found.
VIS release package data#
Packages listed in collect_packages are now always directory-shipped rather
than compiled to a single .pyd. Compiling them preserved the Python but
dropped every non-.py file alongside it (Tcl scripts, .dlls,
cacert.pem), which broke tkinterweb and certifi in released binaries.
Fixes#
Rounded ``vButton`` losing its shape on hover/press — a rounded
vButtoncould flip to a plain rectangle (no radius, no outline) and stay that way. Tk drives its ownactivestate from Tcl —tk::ButtonDownruns$w configure -relief sunken -state activeon press, and under X11/Aquatk::ButtonEnterdoes it on plain hover — so Python’sconfigure()never runs, the rounded fill / corner tiles are never repainted, and Tk paints the native square face usingactivebackground/activeforeground. Because the state only clears viatk::ButtonUp/tk::ButtonLeave, a drag that swallows the<ButtonRelease-1>(dragging a tab out of its bar) left the button stuckactive— the rectangle was permanent, not a flash.vButton._sync_active_colors()now mirrors the restingbg/fgonto the active options in rounded mode, re-applied whenever the resting colours, the hover fill orstatechange; explicitly-passed values still win, andradius=0is untouched.TabBarno longer passesactivebackgroundto its pill tabs (it drives hover itself viaconfig(bg=...)); the plain-Buttonstyles still get the native hover colour.Rounded ``vButton`` stuck in hover between siblings —
_maybe_unhover()kept hover whenever the widget under the pointer had the hovered button’s path as a string prefix, to cover the corner/edge tiles (which are children). Sibling paths collide under that test —.!vbuttonis a prefix of.!vbutton2— so moving the pointer from onevButtonstraight onto another in the same parent left the first permanently in itsactive_fill, andconfigure()stops tracking_v_rest_bgwhile_v_hoveris set, so it never recovered. Now matched on the"."child separator, and a widget destroyed between<Leave>and the idle callback no longer raisesTclError.