wish.window

The tabbed window: the character editor and the live map, in one place.

Both tabs are the windows that already existed, unchanged in what they do. They are QMainWindow`s used as pages, which Qt allows and which is what keeps this file thin: the editor's form, bindings and losslessness rules are untouched, and the map still paints exactly what `render.py gives it.

What the outer window adds is the three things a merged application owes you:

  • one connection. wish/session.py owns it, because VICE serves exactly one binary-monitor connection and ignores the second in silence.

  • one status bar, answering “what am I looking at” – the file on the editor tab, the connection and the party’s square on the map tab.

  • one title, carrying the open save and a dirty marker whichever tab shows.

The editor tab is never handed the target. That is the project’s first decision made structural (docs/README.md): editor/ imports nothing from automap/, and the file path works with no emulator anywhere.

Module Attributes

SWITCHED_TITLE

Said in the Messages panel when #357 (The automapper reads the shared Game disks folder, so setting a title's own folder does not make it map that title) step 4 switches the window to a title the machine turned out to be running, rather than refusing.

ANY_BACKEND

backends.find takes whichever answers first.

FIRST_RUN

The first run's size, when nothing has been remembered yet.

Functions

dress(app)

The application's name and its icon, before the first window exists.

load_maps([disks])

Every GEO off the game disks, or nothing if they cannot be found.

load_maps_titled([disks, game])

The maps and their title, or nothing at all.

run([save, game_disk, maps, area, tab, ...])

Classes

WishWindow

The application window.

wish.window.ANY_BACKEND = ''

backends.find takes whichever answers first. The ordinary case, and what an empty Settings.backend means.

Type:

No preference

wish.window.FIRST_RUN = (1875, 1030)

The first run’s size, when nothing has been remembered yet. Wide enough for the map, the roster cards and the notes column side by side; clamped to the screen, so a smaller display gets its own size rather than this one.

wish.window.SWITCHED_TITLE = 'Now mapping {title}.'

Said in the Messages panel when #357 (The automapper reads the shared Game disks folder, so setting a title's own folder does not make it map that title) step 4 switches the window to a title the machine turned out to be running, rather than refusing. Approved by Donald, 2026-09-07, chosen over two longer wordings that named the disks or the folder: the line says the thing a player wants to know – the automapper is on this title now – and leaves the reason it changed to the debug log beside it.

class wish.window.WishWindow[source]

Bases: PyQt6.QtWidgets.QMainWindow

The application window.

__init__(save=None, game_disk=None, maps=None, area=None, settings=None, session=None, tab=MAP_TAB, title=None, disks=None)[source]
Parameters:
announce(title, text)[source]

A modal note. A method so a test can silence it.

Parameters:
Return type:

None

closeEvent(event)[source]

The editor asks about unsaved changes first, and may refuse.

Its own closeEvent owns that question – the merged window must not grow a second copy of it and get the two out of step.

Return type:

None

disks_text()[source]

The resolved Game directory as a plain path, for the two tabs.

Return type:

str | None

follow_save()[source]

A save was opened: point the automatic backup folder at it, and remember its folder for the next File > Open (#66).

The backup half does nothing at all once the user has chosen a folder in the dialog – “never change it after they’ve specified it themselves” – but the remembered-folder half always follows: it is a convenience, not a preference, so every open updates it.

Return type:

None

game()[source]

Which title is open, as a Game, or None if nothing says.

label_backends()[source]

Say which are answering, when the dialog asks and not before.

probe() is a TCP connect with a short timeout; doing it on the poll timer would be noise, and doing it once at startup would be stale by the time anybody looked.

The state is also kept in backend_status, apart from the label: the dialog draws it as a badge, because on Windows “Ultimate not answering, unverified…” ran into its own label as one sentence.

Return type:

None

map_game()[source]

Which title the automapper is labelling with, as a Game.

Not `game()`, which is the open save’s and is None with nothing open. The fast-travel list has to agree with the map, and the map always has a title – it falls back through the open save, the disks folder and games.DEFAULT (docs/138-multiple-games.md §3). A list keyed off anything else would offer one game’s areas in another’s session, which is the whole of #14.

observe_title(title)[source]

The machine is drawing a different configured title’s map.

#357 (The automapper reads the shared Game disks folder, so setting a title's own folder does not make it map that title) step 4, Donald’s ruling of 2026-09-07: switch to it rather than refuse, and say so. Setting self._title before reload_disks is the whole of the mechanism – game() reads it, resolve_disks asks game_folders for that title’s own row, and AutomapBinding.set_maps (called from reload_disks) takes title_check back to UNKNOWN and re-applies the per-title controls for the new title.

Parameters:

title (str)

Return type:

None

preferences()[source]

File > Preferences. Returns the dialog, which is what a test wants.

Return type:

wish.preferences.PreferencesDialog

reload_disks()[source]

Re-resolve where the disks are and hand the answer to both tabs.

Return type:

None

set_backup_folder(folder)[source]

The user typed, browsed or cleared one in the dialog.

Anything they typed is theirs and is never moved again. Clearing it is the way back to automatic: a setting a user cannot undo is a trap, and the field fills in again from the open save – immediately when there is one, otherwise the next time one is opened.

Parameters:

folder (str)

Return type:

None

set_fast_travel_targets(ids)[source]

Which areas the Fast Travel dropdown offers, by goldbox/areas.py id.

Empty is a choice like any other and is saved as one: the setting is None only until somebody has ticked or unticked anything. Filed under the open title’s key, because an area id means nothing without one.

Return type:

None

set_interval(interval_ms)[source]

How often to poll, or 0 for the backend’s own.

Parameters:

interval_ms (int)

Return type:

None

set_saves_folder(folder)[source]

The user typed, browsed or cleared the saves folder in the dialog (#66 steps 2 and 3).

Unlike the backup folder above, this has no automatic state to fall back to: it is either set or it is not, and clearing it goes back to the folder a save was last opened from, or beside the one already open.

Parameters:

folder (str)

Return type:

None

set_ultimate_host(host)[source]

Where the Commodore 64 Ultimate is. Empty means “no device”.

Parameters:

host (str)

Return type:

None

show_dialog(dialog)[source]

Put a dialog up. A method so a test can open one without blocking.

Return type:

None

show_log()[source]

Open the log in whatever the desktop uses for a text file.

Return type:

None

wish.window.dress(app)[source]

The application’s name and its icon, before the first window exists.

Three separate mechanisms, one drawing:

  • `setWindowIcon` is the title bar, Alt-Tab and the taskbar button of a running window. It is a QIcon carrying a pixmap per size, painted from ui/icons.py, so Qt picks rather than scales. What a pinned shortcut and Explorer show is the .ico in the executable’s resource instead – wish.spec – and the two have to be the same drawing, which they are because both come from the same path data.

  • `setDesktopFileName` is how GNOME and KDE match a window to its .desktop entry, and without it a Wayland window gets the toolkit’s generic icon whatever setWindowIcon said. wish is the id the freedesktop icons under assets/icons/hicolor are named for.

  • the app user model id is Windows’ key for taskbar grouping and pinning. Left unset, a Python-hosted window can be grouped under the interpreter rather than under itself, and a pin can attach to the wrong thing.

And on Linux, none of that is enough on its own. The desktop matches the window to a .desktop entry and then looks up the icon by name in its icon theme, so with neither installed it draws a generic gear whatever setWindowIcon said. Donald hit exactly that on 2026-09-05. A wheel ships both into <prefix>/share, which is on the search path for a pip install --user and not for a virtualenv or a pipx install – so tools.installdesktop.ensure puts them in the user’s own directory the first time it finds none. It refuses to run on anything but Linux, when an entry already exists anywhere on the search path, when WISH_NO_DESKTOP_INSTALL is set, and in a headless or offscreen run – the last of which is what keeps the test suite out of somebody’s home directory.

Return type:

None

wish.window.load_maps(disks=None)[source]

Every GEO off the game disks, or nothing if they cannot be found.

Parameters:

disks (str | None)

Return type:

dict

wish.window.load_maps_titled(disks=None, game=None)[source]

The maps and their title, or nothing at all.

Nothing here is fatal: with no disks the map tab draws an empty grid and says so, and the editor tab does not care at all. A truncated download sitting in the folder is the same – a reason to draw no map, not a reason to take the window down when somebody points the preference at it.

Parameters:

disks (str | None)

Return type:

tuple[dict, object]

wish.window.run(save=None, game_disk=None, maps=None, area=None, tab=EDITOR_TAB, interval_ms=None, title=None, disks=None)[source]
Parameters:
  • save (str | None)

  • game_disk (str | None)

  • maps (dict | None)

  • area (str | None)

  • tab (int)

  • interval_ms (int | None)

  • title (str | None)

  • disks (str | None)

Return type:

int