Skip to main content

The Qt application shell

Read-only survey of PyMOL’s Qt main window: layout, menu bar, command line, feedback area, quick buttons, dialogs, startup and shutdown. Every claim is anchored to a file:line in packages/engine/, which is unmodified upstream. Where the port stands. The shell is apps/web/src/shell/ (AppShell.tsx, extGuiDock.ts, orthoPanel.ts, StatusBar.tsx, ConnectionOverlay.tsx); the menu bar is apps/web/src/features/menubar/ with the menu data generated into apps/web/src/features/menubar/generated/menudata.ts; the command line and feedback log are apps/web/src/features/console/; dialogs are apps/web/src/features/dialogs/ and apps/web/src/features/render/; the seams §12 lists are filled by packages/bridge/tenmol_bridge/shims.py. Primary sources read:
  • packages/engine/modules/pmg_qt/pymol_qt_gui.py (1267 lines) — the QMainWindow shell, menu builder, command line, feedback area, quick buttons, app startup.
  • packages/engine/modules/pymol/_gui.py (1033 lines) — the actual menu-bar data model (PyMOLDesktopGUI.get_menudata), command history, movie-program helpers, recent-files sqlite DB.
  • packages/engine/modules/pmg_qt/mimic_pmg_tk.py (177 lines) — Pmw/Tk menu-bar shim so legacy plugins can add menu items.
  • packages/engine/modules/pmg_qt/mimic_tk.py (128 lines) — tkMessageBox/tkFileDialog → Qt shim, injected into sys.modules.
  • packages/engine/modules/pmg_qt/__init__.py (3 lines) — empty placeholder docstring only, no code.
  • Supporting: packages/engine/modules/pmg_qt/pymol_gl_widget.py, packages/engine/modules/pmg_qt/keymapping.py, packages/engine/modules/pymol/gui.py, packages/engine/modules/pymol/Qt/utils.py, packages/engine/modules/pymol/invocation.py, packages/engine/modules/pymol/save_shortcut.py, packages/engine/modules/pymol/colorprinting.py, packages/engine/modules/pymol/parser.py, packages/engine/data/pmg_qt/styles/pymol.sty, packages/engine/modules/pmg_qt/forms/*.ui.

0. Menus are not generated from packages/engine/modules/pymol/menu.py

A common assumption, and wrong for the main window:
  • The Qt menu bar is built from self.get_menudata(cmd) at packages/engine/modules/pmg_qt/pymol_qt_gui.py:353.
  • get_menudata is defined at packages/engine/modules/pymol/_gui.py:55 on the toolkit-independent mixin PyMOLDesktopGUI (packages/engine/modules/pymol/_gui.py:9), which PyMOLQtGUI inherits (packages/engine/modules/pmg_qt/pymol_qt_gui.py:31).
  • The only other consumer of get_menudata is the legacy Tk skin at packages/engine/modules/pmg_tk/skins/normal/__init__.py:1072 (grep result; not part of this area).
  • packages/engine/modules/pymol/menu.py is imported by the C++ layer, at packages/engine/layer1/P.cpp:2020 (P_menu = PImportModuleOrFatal("pymol.menu")), and drives the viewport-internal popup menus via MenuActivate* in packages/engine/layer3/Executive.cpp (e.g. packages/engine/layer3/Executive.cpp:15019, :15069, :783). It also exports rep_setting_lists consumed by packages/engine/modules/pymol/viewing.py:1965.
So: menu bar = packages/engine/modules/pymol/_gui.py; internal-GUI right-click menus = packages/engine/modules/pymol/menu.py (a different agent’s area). Both must be cloned, but they are separate data sources with different serialization needs.

1. Window layout, docking and splitters

There are no QSplitters in the main window. Layout is QMainWindow + dock widgets:

In the port

apps/web/src/shell/AppShell.tsx: CSS-grid/flex root, <Viewport> (three.js canvas) as the “central widget”, and a dockable/resizable ExternalGuiPanel (react-mosaic / dockview / custom). Dock state (area, floating, visible) is pure client state; only viewport size changes must be echoed to the backend (cmd.reshape / cmd.viewport, see §12).

2. The “External GUI” concept

Historically PyMOL had two OS windows: the OpenGL “internal” window and a Tk “external” window with the menu bar, command line and output. In Qt they are merged: the “External GUI” is just the QDockWidget described above (pymol_qt_gui.py:184-193) containing, left-to-right (pymol_qt_gui.py:159-169):
  1. a QVBoxLayout with the feedback QPlainTextEdit (browser) and the PyMOL> label + CommandLineEdit row;
  2. a quickbuttonslayout QVBoxLayout with 4 rows of quick buttons plus the progress-bar row.
Behaviours:
  • If options.external_gui is truthy the dock’s title bar is replaced with an empty QWidget (i.e. undecorated, non-draggable); otherwise the dock is hidden (pymol_qt_gui.py:188-191). -x sets external_gui = 0 (invocation.py:382), -q/-c paths at invocation.py:492-493.
  • Double-clicking the frame calls toggle_ext_window_dockable(True) (pymol_qt_gui.py:172-173).
  • toggle_ext_window_dockable swaps between “has title bar / floating” and “no title bar / docked” (pymol_qt_gui.py:457-470).
  • Menu entries Display ▸ External GUI ▸ Toggle dockable (shortcut Ctrl+E) and ▸ Visible (the dock’s own toggleViewAction, re-labelled “Visible”) are appended imperatively after the data-driven menu build (pymol_qt_gui.py:377-384).
  • pymol.gui.ext_hide / ext_show explicitly no-op with a printed “ignoring gui.ext_hide” when a Qt window exists (packages/engine/modules/pymol/gui.py:44-66).

In the port

apps/web/src/shell/extGuiDock.ts drives a dock with modes (docked-top \| docked-side \| floating \| hidden), persisted in client state, plus Ctrl+E binding. ext_hide/ext_show stay no-ops; the bridge should still surface them so scripts don’t error.

3. Command entry line (CommandLineEdit)

Widget: CommandLineEdit(QLineEdit) at pymol_qt_gui.py:1087, instantiated at pymol_qt_gui.py:120-121 with objectName="command_line"; prefixed by a QLabel("PyMOL>") with objectName="command_label" (pymol_qt_gui.py:141-143). Tooltip text, reproduced verbatim, pymol_qt_gui.py:145-157:

3.1 Key handling (lineeditKeyPressEventFilter, pymol_qt_gui.py:421-438)

eventFilter (pymol_qt_gui.py:440-455) additionally swallows all Tab KeyRelease events, and, when the GL widget has focus, routes Tab KeyPress into keyPressEventpymol.button (pymol_qt_gui.py:50-54).

3.2 History model (packages/engine/modules/pymol/_gui.py:895-941)

  • self.history = [''], history_cur = 0 (_gui.py:896-897). Slot 0 is always a scratch buffer holding the currently typed text.
  • doTypedCommand (_gui.py:906): dedupes against the previous entry, inserts a new blank at index 0, caps the list at 255 entries, resets history_cur, then self.cmd.do(cmmd).
  • back() saves current text into slot 0 on first press then steps history_cur + 1.
  • back_search(set0=False) scans forward from history_cur+1 for the first entry that startswith(history[0]).
  • _jump_history(i) clamps to len(history)-1, sets text and moves the cursor to end.
  • Accessors the mixin requires from the toolkit: command_get (pymol_qt_gui.py:922), command_set (:925), command_set_cursor (:928).

3.3 Tab completion

complete() (_gui.py:899-904) calls self.cmd._parser.complete(self.command_get()). The implementation is Parser._complete at packages/engine/modules/pymol/parser.py:524-593:
  • no space/@ in the string ⇒ complete against cmd.kwhash command keywords (parser.py:534);
  • otherwise resolve the command via cmd.kwhash.interpret, count commas to pick the argument index, and complete from cmd.auto_arg[count][command] (parser.py:540-558);
  • fallback: filesystem glob completion on the trailing token, including $ENVVAR completion (parser.py:561-593). Ambiguous matches are printed to feedback (“ parser: matching files:”) and the common prefix is inserted.
  • Returns a full replacement string; the GUI replaces the whole line and puts the cursor at the end.
The alternative QCompleter(cmd.kwhash.keywords) path exists but is commented out (pymol_qt_gui.py:212-216).

3.4 Submit path

doPrompt (pymol_qt_gui.py:960-964): doTypedCommand(text)pymolwidget._pymolProcess() (pump PyMOL idle/redisplay, pymol_gl_widget.py:245) → lineedit.clear()feedback_timer.start(0) (immediate feedback flush).

3.5 Drag-and-drop into the command line

CommandLineEdit implements a live preview drop (pymol_qt_gui.py:1087-1124):
  • dragEnterEvent: if the mime data has text, immediately insert it at the cursor and select it; if the first URL is a local file, insert url.toLocalFile() instead of the raw text. Saves _saved_pos/_saved_text.
  • dragLeaveEvent: restores the saved text and cursor if a preview was inserted.
  • dropEvent: accepts the proposed action (the text is already there).
  • dragMoveEvent: overridden to a no-op.

In the port

apps/web/src/features/console/CommandLine.tsx with useCommandHistory.ts mirroring _gui.py:895-942 exactly (255 cap, slot-0 scratch, prefix search), and completion proxied to cmd._parser.complete server-side, because it needs kwhash, auto_arg and the local filesystem. HTML5 drag events reproduce the preview-insert/restore semantics (dragPreview.ts).

4. Feedback / output text area

  • self.browser = QPlainTextEdit(), objectName="feedback_browser", read-only (pymol_qt_gui.py:122-124).
  • Focus proxy trick: browser’s focus proxy is the command line so clicking output focuses input; the proxy is cleared while a selection exists so Ctrl+C works (pymol_qt_gui.py:126-134, copyAvailable handler).
  • Monospace font via getMonospaceFont() (pymol_qt_gui.py:137; impl packages/engine/modules/pymol/Qt/utils.py:249-266 — Monaco/Consolas/Monospace by platform, size 9, +3 on macOS).
  • connectFontContextMenu(self.browser) (pymol_qt_gui.py:138) adds a “Select Font…” entry to the standard context menu, opening QFontDialog (Qt/utils.py:204-226).
  • Polling loop: feedback_timer is a single-shot QTimer, first fired after 100 ms (pymol_qt_gui.py:391-394), then re-armed at 500 ms at the end of every update_feedback (pymol_qt_gui.py:958).
  • update_feedback (pymol_qt_gui.py:941-958):
    1. update_progress();
    2. feedback = self.cmd._get_feedback() (packages/engine/modules/pymol/internal.py:593) — drains the C++ feedback queue, returns a list of lines;
    3. colorprinting.text2html(...) (packages/engine/modules/pymol/colorprinting.py:17 — escapes &<>, converts spaces to &nbsp; and newlines to <br>) then browser.appendHtml(html);
    4. auto-scroll to verticalScrollBar().maximum();
    5. for setting in self.cmd.get_setting_updates() (packages/engine/modules/pymol/setting.py:440) → for each registered index, read cmd.get_setting_tuple(setting)[1][0] and invoke every callback in self.setting_callbacks[setting]. This is the entire mechanism that keeps checkable/radio menu items and the window title in sync with the backend.
Note: colorprinting.text2html in this build does not translate PyMOL’s \933-style color escapes (see packages/engine/modules/pymol/menu.py:21-23 for those codes); error/warning/suggest/parrot are plain print aliases (colorprinting.py:28-31).

In the port

Polling is replaced by push: the bridge drains cmd._get_feedback() on its status tick and publishes the feedback topic, and publishes settings from the cmd.get_setting_updates() tap (see docs/settings-colors.md D3). apps/web/src/features/console/FeedbackLog.tsx renders it — monospace, auto-scroll-on-bottom, selectable, with the font size held client-side.

5. Quick buttons + progress bar (the “toolbar”/status area)

There is no QToolBar and no QStatusBar. The button grid is built at pymol_qt_gui.py:222-271 — 4 rows, each a QHBoxLayout with spacing 2; every button gets setProperty("quickbutton", True) (drives the stylesheet rule) and WA_LayoutUsesWidgetRect (macOS workaround, pymol_qt_gui.py:261). Progress row (pymol_qt_gui.py:273-284): QProgressBar + a red Abort QPushButton (background: #FF0000; color: #FFFFFF) wired to cmd.interrupt (packages/engine/modules/pymol/locking.py:88). update_progress (pymol_qt_gui.py:931-939) computes int(cmd.get_progress() * 100) (packages/engine/modules/pymol/monitoring.py:5) and shows/hides both widgets when the value is >= 0 / < 0. A stretch is appended after the rows and its index is remembered so the dock-orientation handler can add/remove it (pymol_qt_gui.py:286-287, used at :200 and :203-204).

6. Menu bar — full enumeration

Built at pymol_qt_gui.py:289-357. self.menubar = self.menuBar(); self.menudict = {'': menubar} (:350) is the registry legacy plugins mutate. Every top-level menu and submenu is setTearOffEnabled(True) and gets setWindowTitle(title) “needed for Windows” (pymol_qt_gui.py:297-298).

6.1 The _addmenu item grammar (pymol_qt_gui.py:295-344)

SettingAction (pymol_qt_gui.py:1041-1082): resolves the setting index via cmd.setting._get_index(name), reads cmd.get_setting_tuple(index), makes the action checkable for setting types 1/2/3/5/6 (bool/int/float/color/str) else prints TODO <type> <name>; on trigger issues cmd.set(index, true_value if checked else false_value, log=1, quiet=0); registers setting_callbacks[index] → action.setChecked(v != false_value). Gotcha for the port: because radio QActionGroups are keyed only by setting name, two visually separate radio blocks for the same setting (e.g. surface_cavity_mode appearing both as the “Cavities and Pockets…” block and the “Exterior (Normal)” item in Setting ▸ Surface) share one exclusive group. Also, a radio’s initial checked state is taken from values[0] == value where values was captured when the group was first created (pymol_qt_gui.py:326-327, 339-340). macOS hack: Edit is temporarily renamed to Edit_ and restored after 10 ms via QTimer.singleShot to suppress “Start Dictation” (pymol_qt_gui.py:359-364, QTBUG-43217).

6.2 Filepackages/engine/modules/pymol/_gui.py:80-134

6.3 Edit_gui.py:135-138

6.4 Build_gui.py:139-233

Build ▸ Fragment (all cmd.do(...) of literal strings): Acetylene [Alt-J] editor.attach_fragment('pk1','acetylene',2,0) · Amide N->C [Alt-1] ...'formamide',3,1 · Amide C->N [Alt-2] ...'formamide',5,0 · Bromine [Ctrl-Shift-B] replace Br,1,1 · Carbon [Ctrl-Shift-C] replace C,4,4 · Carbonyl [Alt-0] ...'formaldehyde',2,0 · Chlorine [Ctrl-Shift-L] replace Cl,1,1 · Cyclobutyl [Alt-4] ...'cyclobutane',4,0 · Cyclopentyl [Alt-5] ...'cyclopentane',5,0 · Cyclopentadiene [Alt-8] ...'cyclopentadiene',5,0 · Cyclohexyl [Alt-6] ...'cyclohexane',7,0 · Cycloheptyl [Alt-7] ...'cycloheptane',8,0 · Fluorine [Ctrl-Shift-F] replace F,1,1 · Iodine [Ctrl-Shift-I] replace I,1,1 · Methane [Ctrl-Shift-M] ...'methane',1,0 · Nitrogen [Ctrl-Shift-N] replace N,4,3 · Oxygen [Ctrl-Shift-O] replace O,4,2 · Sulfer [Ctrl-Shift-S] (sic) replace S,2,2 · Sulfonyl [Alt-3] ...'sulfone',3,1 · Phosphorus [Ctrl-Shift-P] replace P,4,3. (_gui.py:140-161; backend packages/engine/modules/pymol/editor.py:51 attach_fragment.) Build ▸ Residue (_gui.py:162-195) — 23 commands, each cmd.editor.attach_amino_acid('pk1', <code>) (packages/engine/modules/pymol/editor.py:98): Acetyl [Alt-B]=ace, Alanine [Alt-A]=ala, Amine=nhh, Aspartate [Alt-D]=asp, Asparagine [Alt-N]=asn, Arginine [Alt-R]=arg, Cysteine [Alt-C]=cys, Glutamate [Alt-E]=glu, Glutamine [Alt-Q]=gln, Glycine [Alt-G]=gly, Histidine [Alt-H]=his, Isoleucine [Alt-I]=ile, Leucine [Alt-L]=leu, Lysine [Alt-K]=lys, Methionine [Alt-M]=met, N-Methyl [Alt-Z]=nme, Phenylalanine [Alt-F]=phe, Proline [Alt-P]=pro, Serine [Alt-S]=ser, Threonine [Alt-T]=thr, Tryptophan [Alt-W]=trp, Tyrosine [Alt-Y]=tyr, Valine [Alt-V]=val. Then a separator and 3 radios on secondary_structure: Helix=1, Antiparallel Beta Sheet=2, Parallel Beta Sheet=3. Build ▸ Sculpting (_gui.py:197-221): checks Auto-Sculpting(auto_sculpt), Sculpting(sculpting); commands Activate=sculpt_activate all, Deactivate=sculpt_deactivate all, Clear Memory=cmd.sculpt_purge; radios on sculpting_cycles = 1,3,10,33,100,333,1000 (labels 1 Cycle per Update, N Cycles per Update); radios on sculpt_field_mask: Bonds Only=0x01, Bonds and Angles Only=0x03, Local Geometry Only=0x1F, All Except VDW=~(0x20|0x40), All Except 1-4 VDW and Torsions=~(0x40|0x80), All Terms=0xFF. Build root commands (_gui.py:223-232): Cycle Bond Valence [Ctrl-Shift-W]=cycle_valence; Fill Hydrogens on (pk1) [Ctrl-Shift-R]=h_fill; Invert (pk2)-(pk1)-(pk3) [Ctrl-Shift-E]=invert; Create Bond (pk1)-(pk2) [Ctrl-Shift-T]=bond; Remove (pk1) [Ctrl-Shift-D]=remove pk1; Make (pk1) Positive [Ctrl-Shift-K]=alter pk1, formal_charge=1; Make (pk1) Negative [Ctrl-Shift-J]=alter pk1, formal_charge=-1; Make (pk1) Neutral [Ctrl-Shift-U]=alter pk1, formal_charge=0.

6.5 Movie_gui.py:234-376

  • Append ▸ {0.25, 0.5, 1, 2, 3, 4, 6, 8, 12, 18, 24, 30, 48, 60} secondcmd.movie.add_blank(i) (_gui.py:235-238; packages/engine/modules/pymol/movie.py:268).
  • Program ▸ Camera Loop ▸ Nutate — 15°/{4,8,12}s, 30°/{4,8,12,16}s, 60°/{8,16,24,32}s → self.mvprg("movie.add_nutate(<sec>,<deg>,start=%d)") (_gui.py:241-256).
  • Program ▸ Camera Loop ▸ X-Rock — 30°/{2,4,8}, 60°/{4,8,16}, 90°/{6,12,24}, 120°/{8,16,32}, 180°/{12,24,48} (angle written as 179.99) → movie.add_rock(sec,deg,axis='x',start=%d) (_gui.py:258-278).
  • Program ▸ Camera Loop ▸ X-Roll — 4/8/16/32 seconds → movie.add_roll(<s>.0,axis='x',start=%d) (_gui.py:279-284).
  • Program ▸ Camera Loop ▸ Y-Rock — same grid, axis='y' (_gui.py:286-306).
  • Program ▸ Camera Loop ▸ Y-Roll — 4/8/16/32 seconds, axis='y' (_gui.py:307-312).
  • Program ▸ Scene Loop ▸ {Nutate|X-Rock|Y-Rock} (rock=4/2/1) → for angle∈{30,60,90,120} × the matching seconds tuple ((2,4,8),(4,8,16),(6,12,24),(8,16,32)), item label "<angle> deg. over <sec> sec." firing set sweep_angle,<angle>;cmd.movie.add_scenes(None, <sec>, rock=<rock>, start=%d) (_gui.py:315-327).
  • Program ▸ Scene Loop ▸ Steady ▸ {1,2,4,8,12,16,24} seconds eachmovie.add_scenes(None,<v>.0,rock=0,start=%d) (_gui.py:329-333).
  • Program ▸ State Loop / State Sweep{Full Speed \| 1/2 \| 1/3 \| 1/4 \| 1/8 \| 1/16 Speed}{no pause \| 1 \| 2 \| 4 second pause}movie.add_state_loop(<speed>, <pause>, start=%d) / movie.add_state_sweep(...) (_gui.py:336-349). That is 2 × 6 × 4 = 48 leaf items.
  • Update Last Programself.mvprg() re-runs self.movie_command (_gui.py:958-969).
  • Remove Last Programself.mvprg_remove_last()cmd.mdelete(-1, self.movie_start) (_gui.py:950-956).
  • Resetmset;rewind.
  • Frame Rate ▸ radios on movie_fps: 30/15/5/1/0.3 FPS; check Show Frame Rate(show_frame_rate); command Reset Meter=cmd.meter_reset.
  • Root checks: Auto Interpolate(movie_auto_interpolate), Show Panel(movie_panel), Loop Frames(movie_loop), Draw Frames(draw_frames), Ray Trace Frames(ray_trace_frames), Cache Frame Images(cache_frames); command Clear Image Cache=cmd.mclear; checks Static Singletons(static_singletons,1), Show All States(all_states,1).
mvprg state (_gui.py:947-969): movie_start = cmd.get_movie_length() + 1, then movie_command = command % movie_start, then cmd.do(movie_command). This is client-side stateful and must be replicated in the React store.

6.6 Display_gui.py:377-491

  • check Sequence (seq_view,1).
  • Sequence Mode ▸ radios seq_view_format: Residue Codes0, Residue Names1, Chain Identifiers3, Atom Names2, States4; sep; radios seq_view_label_mode: All Residue Numbers2, Top Sequence Only1, Object Names Only0, No Labels3; sep; radios seq_view_gap_mode: No Gaps0, All Gaps1, Single Gap2.
  • checks Internal GUI(internal_gui,1), Internal Prompt(internal_prompt,1).
  • Internal Feedback ▸ radios internal_feedback = 0,1,3,5.
  • Overlay ▸ radios overlay = 0,1,3,5.
  • check Stereo(stereo,1).
  • Stereo Mode ▸ commands: Anaglyph Stereo=stereo anaglyph, Cross-Eye Stereo=stereo crosseye, Wall-Eye Stereo=stereo walleye, Quad-Buffered Stereo=stereo quadbuffer, Zalman Stereo=stereo byrow, OpenVR=stereo openvr, sep, Swap Sides=stereo swap, sep, Chromadepth=stereo chromadepth, off=stereo off.
  • Zoom ▸ {4,6,8,12,20} Angstrom Spherecmd.zoom('center', i, animate=-1); All=zoom animate=-1; Complete=zoom animate=-1, complete=1.
  • Clip ▸ Nothing=clip atoms, 5, all; {8,12,16,20,30} Angstrom Slabcmd.clip('slab', i).
  • Background ▸ checks Opaque(opaque_background,1), Alpha Checker(show_alpha_checker,1); radios bg_rgb: White0, Light Grey134, Grey104, Black1.
  • Color Space ▸ CMYK (for publications)=space cmyk, PyMOL (for video + web)=space pymol, RGB (default)=space rgb.
  • Quality ▸ Maximum Performance=util.performance(100), Reasonable Performance=util.performance(66), Reasonable Quality=util.performance(33), Maximum Quality=util.performance(0) (packages/engine/modules/pymol/util.py:571).
  • Grid ▸ radios grid_mode: By Object1, By State2, By Object-State3, Disable0.
  • Root checks: Orthoscopic View(orthoscopic,1), Show Valences(valence,1), Smooth Lines(line_smooth,1), Depth Cue (Fogging)(depth_cue,1), Two Sided Lighting(two_sided_lighting,1), Specular Reflections(specular,1.0), Animation(animation,1), Roving Detail(roving_detail,1).
  • Appended imperatively (not in _gui.py): separator + External GUI ▸ Toggle dockable (Ctrl+E) and ▸ Visiblepymol_qt_gui.py:377-384.

6.7 Setting_gui.py:492-774

Top: Edit All...settings_edit_all_dialog (pymol_qt_gui.py:878-883, pmg_qt/advanced_settings_gui.py:PyMOLAdvancedSettings); Keyboard Shortcuts...shortcut_menu_edit_dialog (:885-889, pmg_qt/shortcut_menu_gui.py:PyMOLShortcutMenu); Colors...edit_colors_dialog (:547-611).
  • Label ▸ Size ▸ radios label_size = 10/14/18/24/36/48/72 “Point”; sep; -0.3/-0.5/-1/-2/-4 shown as “N Angstrom”.
  • Label ▸ Font ▸ radios label_font_id: Sans5, Sans Oblique6, Sans Bold7, Sans Bold Oblique8, Serif9, Serif Oblique17, Serif Bold10, Serif Bold Oblique18, Mono11, Mono Oblique12, Mono Bold13, Mono Bold Oblique14, Gentium Roman15, Gentium Italic16.
  • Label ▸ Color ▸ radios label_color: Front-6, Back-7.
  • Label ▸ Show Connectors check (label_connector).
  • Label ▸ Background Color ▸ radios label_bg_color: None-1, Back-7, Front-6.
  • Lines & Sticks ▸ check Ball and Stick(stick_ball,1); Ball and Stick Ratio ▸ radios stick_ball_ratio 1.0/1.5/VDW(-1); Zero Order Bonds ▸ radios valence_zero_mode Hide0/Dashed1/Solid2; Zero Order Stick Scale ▸ radios valence_zero_scale 0.1/0.2/0.3/1.0; Stick Radius ▸ radios stick_radius .1/.2/.25; Stick Hydrogen Scale ▸ radios stick_h_scale .4/1.; Line Width ▸ radios line_width 1.0/1.49/3.0; check Lines As Cylinders(line_as_cylinders,1).
  • Cartoon ▸ Rings and Bases ▸ radios cartoon_ring_mode: Filled Rings (Round Edges)1, (Flat Edges)2, (with Border)3, Spheres4, Base Ladders0; radios cartoon_ring_finder: Bases and Sugars1, Bases Only2, Non-protein Rings3, All Rings4; radios cartoon_ring_transparency: Transparent Rings0.5, Default-1.
  • Cartoon ▸ checks: Side Chain Helper(cartoon_side_chain_helper), Round Helices, Fancy Helices, Cylindrical Helices, Flat Sheets, Fancy Sheets, Smooth Loops, Discrete Colors, Highlight Color(cartoon_highlight_color, true=104, false=-1); Sampling ▸ radios cartoon_sampling: Atom count dependent-1, 2, 7, 14; Gap Cutoff ▸ radios cartoon_gap_cutoff 0/5/10/20.
  • Ribbon ▸ checks Side Chain Helper(ribbon_side_chain_helper), Trace Atoms(ribbon_trace_atoms); radios ribbon_as_cylinders: As Lines0, As Cylinders1; Cylinder Radius ▸ radios ribbon_radius: Match Line Width0., 0.2/0.5/1.0 Angstrom.
  • Surface ▸ Color ▸ radios surface_color: White0, Light Gray4236, Gray25, Default (Atomic)-1; radios surface_type: Dot1, Wireframe2, Solid0; radios surface_cavity_mode: Cavities and Pockets Only1, Cavities and Pockets (Culled)2, Exterior (Normal)0; Cavity Detection Radius ▸ radios surface_cavity_radius: 7 Angstrom, and -3/-4/-5/-6/-8/-10/-20 labelled “N Solvent Radii”; Cavity Detection Cutoff ▸ radios surface_cavity_cutoff -1..-5 “N Solvent Radii”; check Solvent Accessible(surface_solvent); checks Smooth Edges(surface_smooth_edges), Edge Proximity(surface_proximity); radios surface_mode: Ignore None1, Ignore HETATMs0, Ignore Hydrogens2, Ignore Unsurfaced3.
  • Volume ▸ check Pre-integrated Rendering(volume_mode); Number of Layers ▸ radios volume_layers 100/256/500/1000.
  • Transparency ▸ {Surface|Sphere|Cartoon|Stick} → each is transparency_menu(setting) = radios Off0.0, 20%0.2, 40%0.4, 50%0.5, 60%0.6, 80%0.8 on transparency/sphere_transparency/cartoon_transparency/stick_transparency (_gui.py:68-72). Then 4 composite commands that set three settings at once (_gui.py:681-690): Uni-Layer(transparency_mode2, backface_cull1, two_sided_lighting0), Multi-Layer(1,0,1), Multi-Layer (Real-time OIT)(3,0,-1), Fast and Ugly(0,1,0). Then check Angle-dependent(ray_transparency_oblique,1.0).
  • Rendering ▸ check OpenGL 2.0 Shaders(use_shaders); check Antialias (Ray Tracing)(antialias); Antialias (Real Time) ▸ radios antialias_shader off0/FXAA1/SMAA2; command Modernizecmd.util.modernize_rendering(1, cmd) (packages/engine/modules/pymol/util.py:553); Shadows ▸ commands None/Light/Medium/Heavy/Black then Matte/Soft/Occlusion/Occlusion2cmd.util.ray_shadows(v) (util.py:821); Texture ▸ radios ray_texture: None0, Matte 1=1, Matte 2=4, Swirl 1=2, Swirl 2=3, Fiber5; Interior Texture ▸ same values on ray_interior_texture; Memory ▸ radios hash_max: Use Less (slower)70, Use Standard Amount100, Use More (faster)170, Use Even More230, Use Most300; checks Cull Backfaces(backface_cull), Opaque Interiors(ray_interior_color, true=74, false=-1).
  • PDB File Loading ▸ check Ignore PDB Segment Identifier(ignore_pdb_segi).
  • mmCIF File Loading ▸ checks Use "auth" Identifiers(cif_use_auth), Load Assembly (Biological Unit)(assembly, true "1", false "" — a string setting), Bonding by "Chemical Component Dictionary"(connect_mode, true 4, false 0).
  • Map File Loading ▸ checks Normalize CCP4 Maps(normalize_ccp4_maps), Normalize O Maps(normalize_o_maps).
  • Auto-Show ... ▸ check Cartoon/Sticks/Spheres by Classification(auto_show_classified, true -1, false 0); checks Auto-Show Lines, Auto-Show Spheres, Auto-Show Nonbonded, Auto-Show New Selections(auto_show_selections), Auto-Hide Selections(auto_hide_selections).
  • Root checks: Auto-Zoom New Objects(auto_zoom), Auto-Remove Hydrogens(auto_remove_hydrogens), Show Text (Esc)(text), Overlay Text(overlay).

6.8 Scene_gui.py:775-805

Scenes...scene_panel_menu_dialog (pymol_qt_gui.py:891-897, pmg_qt/scene_bin_gui.py:ScenePanel) · Next [PgDn]=cmd.scene('', 'next') · Previous [PgUp]=cmd.scene('', 'previous') · Append=scene new, store · Append... ▸ Camera=scene new, store, color=0, rep=0, Color=scene new, store, view=0, rep=0, Reps=scene new, store, view=0, color=0, Reps + Color=scene new, store, view=0 · Insert Before=cmd.scene('', 'insert_before') · Insert After=cmd.scene('','insert_after') · Update=cmd.scene('auto','update') · Delete=cmd.scene('auto','clear') · Recall ▸ F1..F12=cmd.scene(k,'recall'), Store ▸ F1..F12, Clear ▸ F1..F12 (generated by F_scene_menu, _gui.py:61-66) · check Buttons(scene_buttons,1) · Cache ▸ Enable/Optimize/Read Only/Disablecmd.cache("enable"|"optimize"|"read_only"|"disable").

6.9 Mouse_gui.py:806-833

Selection Mode ▸ radios mouse_selection_mode: Atoms0, Residues1, Chains2, Segments3, Objects4, Molecules5, C-alphas6. Commands: 3 Button Motions=cmd.config_mouse('three_button_motions'), 3 Button Editing=config_mouse('three_button_editing'), 3 Button Viewing=cmd.mouse('three_button_viewing'), 3 Button Lights=cmd.mouse('three_button_lights'), 3 Button All Modes=config_mouse('three_button_all_modes'), 2 Button Editing=config_mouse('two_button_editing'), 2 Button Viewing=config_mouse('two_button'), 1 Button Viewing Mode=cmd.mouse('one_button_viewing'), Emulate Maestro=cmd.mouse('three_button_maestro'). (cmd.config_mouse at packages/engine/modules/pymol/controlling.py:168; cmd.mouse is marked INTERNAL at controlling.py:609.) Checks: Virtual Trackball(virtual_trackball), Show Mouse Grid(mouse_grid), Roving Origin(roving_origin).

6.10 Wizard_gui.py:834-865

Appearance=wizard appearance · Measurement=wizard measurement · Mutagenesis ▸ Protein=wizard mutagenesis, ▸ Nucleic Acids=wizard nucmutagenesis · Pair Fitting=wizard pair_fit · Density=wizard density · Filter=wizard filter · Sculpting=wizard sculpting · Label=wizard label · Charge=wizard charge · Demo ▸ Representations/Cartoon Ribbons/Roving Detail/Roving Density/Transparency/Ray Tracing/Sculpting/Scripted Animation/Electrostatics/Compiled Graphics Objects/Molscript/Raster3D Inputcmd.wizard('demo', 'reps'|'cartoon'|'roving'|'roving_density'|'trans'|'ray'|'sculpt'|'anime'|'elec'|'cgo'|'raster3d'), sep, End Demonstrationcmd.replace_wizard('demo', 'finish').

6.11 Plugin_gui.py:866 is an empty list

At startup, exactly one item is added imperatively: Initialize Plugin Systemself.initializePlugins (pymol_qt_gui.py:397-398). See §8.

6.12 Help_gui.py:867-888

All webbrowser.open(...) except About PyMOL: PyMOL Home Page http://www.pymol.org · PyMOL Product Page https://www.schrodinger.com/platform/products/pymol/ · PyMOL Community Wiki http://www.pymolwiki.org · PyMOL Command Reference http://pymol.org/pymol-command-ref.html · PyMOL 3 Documentation https://learn.schrodinger.com/public/pymol/current/Content/pymol/pymol_home.htm · Legacy Online Documentation http://pymol.org/d/ · Topics ▸ Selection Algebra \| Settings \| Timeline Python API (pymolwiki pages) · PyMOL Mailing List https://lists.sourceforge.net/lists/listinfo/pymol-users · About PyMOLself.show_about · Sponsorship Information http://pymol.org/funding.html · How to Cite PyMOL http://pymol.org/citing.

6.13 Menu items whose callable is None

PyMOLDesktopGUI declares 22 class attributes as None (_gui.py:12-40). PyMOLQtGUI supplies all of them (imports at pymol_qt_gui.py:36-45 + methods), but if a subclass didn’t, _addmenu would print warning: skipping <item> and silently omit the entry (pymol_qt_gui.py:306-307). Note _gui.py:39-40 declares shortcut_menu_edit_dialog and scene_panel_dialog, while the menu data actually references self.scene_panel_menu_dialog (_gui.py:776) — the declared-but-unused name is scene_panel_dialog; the real method is scene_panel_menu_dialog (pymol_qt_gui.py:891).

6.14 Open Recent... — the one genuinely dynamic submenu

Created by the ('open_recent_menu',) marker (pymol_qt_gui.py:341-342), then populated on every aboutToShow (pymol_qt_gui.py:366-374): clear(), then one action per self.recent_filenames, label truncated to '...' + fname[-120:] when len >= 128, action calls self.load_dialog(fname). Backing store — sqlite, packages/engine/modules/pymol/_gui.py:975-1032:
  • DB at ~/.pymol/recent.db, table recent (filename text unique, timestamp integer) (_gui.py:986-998).
  • Failure to connect prints " Warning: failed to connect to recent DB: <e>" and disables the feature (_gui.py:1000-1003).
  • recent_filenames = SELECT filename FROM recent ORDER BY timestamp DESC (_gui.py:1013-1014).
  • recent_filenames_add = REPLACE INTO recent VALUES (?, datetime('now')); when count > 20, deletes rows older than the 16th newest (LIMIT 1 OFFSET 15) (_gui.py:1016-1032).
  • Only session_save_as calls recent_filenames_add in this file (pymol_qt_gui.py:671); load_dialog in file_dialogs.py is the other likely caller (verify in the file-dialogs area).

7. Keyboard shortcuts owned by the main window

keymapping.py translation tables (needed 1:1 in the browser):
  • keyMap (keymapping.py:10-17): Escape→27, Tab→9, Backspace→8, Return/Enter→13, Delete→127.
  • specialMap (keymapping.py:19-41): Left100, Up101, Right102, Down103, PageUp104, PageDown105, Home106, End107, Insert108, F1..F12 → 1..12. Special keys use state = -2 (PyMOL_Special), ordinary keys state = -1 (PyMOL_Key).
  • Modifier mask (keymapping.py:44-58): Shift=0x1, Meta or Ctrl=0x2, Alt=0x4.
  • Ctrl-<key> without text ⇒ k = key - 64; Alt-<key> ⇒ k = key; keys outside 0..255 are dropped (keymapping.py:84-96).
Saved user shortcuts: pymol.save_shortcut.load_and_set(self.cmd) at window construction (pymol_qt_gui.py:419), which reads ~/.pymol/shortcuts_save.json and calls cmd.set_key(key, value[2]) per entry (packages/engine/modules/pymol/save_shortcut.py:6, :65-72). The returned dict is stored as self.saved_shortcuts and handed to PyMOLShortcutMenu (pymol_qt_gui.py:888).

8. Plugin menu machinery (legacy Tk shim)

initializePlugins (pymol_qt_gui.py:970-988), decorated with @PopupOnException.decorator:
  1. self.menudict['Plugin'].clear();
  2. app = plugins.get_pmgapp() (→ pymol/gui.py:20-26createlegacypmgapp, overridden at pymol_qt_gui.py:1240 to window.createlegacypmgapp);
  3. plugins.legacysupport.addPluginManagerMenuItem() → adds Plugin Manager (packages/engine/modules/pymol/plugins/legacysupport.py:72-87), which opens the Qt PluginManager when pymol.gui.get_qtwindow() returns a window;
  4. Re-points the registry: menudict['PluginQt'] = menudict['Plugin'], then menudict['Plugin'] = menudict['PluginQt'].addMenu('Legacy Plugins') — so Qt plugins land under Plugin, Tk plugins under Plugin ▸ Legacy Plugins;
  5. plugins.HAVE_QT = True; plugins.initialize(app) (packages/engine/modules/pymol/plugins/__init__.py:408).
createlegacypmgapp (pymol_qt_gui.py:990-994) builds mimic_pmg_tk.PMGApp() and sets pmgapp.menuBar = mimic_pmg_tk.PmwMenuBar(self.menudict). PmwMenuBar (mimic_pmg_tk.py:29-91) is the Pmw-compatible façade plugins call through pymol.plugins.addmenuitem (packages/engine/modules/pymol/plugins/__init__.py:112-129) and addmenuitemqt (:101-109):
  • addmenu(name)addcascademenu('', name);
  • deletemenuitems(menuName, start, end) → removes menu.actions()[start-1:(end or start)] (1-based, inclusive — off-by-one-prone);
  • addmenuitem(menuName, 'separator'|'command', label=..., command=...) — the command is wrapped so exceptions print a colored traceback and pop a QMessageBox.critical “Error” (“PyMOL would crash if an exception is not caught”, mimic_pmg_tk.py:65-77);
  • addcascademenu(parent, name, label=...) — raises ValueError if the menu name already exists; sets TearOffEnabled;
  • unknown menu names print Error: no such menu: <name> and return None.
  • Label paths use | as the separator (plugins/__init__.py:116-129), and a leaf label of '-' means “separator”.
PMGApp (mimic_pmg_tk.py:132-174) lazily creates a real hidden tkinter.Tk() root on first .root access, wraps root.tk in tkapp_proxy and pumps root.update() from a 50 ms QTimer, pausing while update/tkwait/vwait are in flight (mimic_pmg_tk.py:108-129, 150-163). Pmw.initialise(root) is called at the end. mimic_tk.py injects Qt replacements into sys.modules for tkMessageBox and tkFileDialog (mimic_tk.py:99-100) and installs a MimicTkImporter meta-path finder mapping tkinter.messagebox / tkinter.filedialog (mimic_tk.py:106-128). _qtMessageBox maps askyesno/askquestion/askokcancel/askretrycancel/showinfo/showerror/showwarning onto QMessageBox (mimic_tk.py:13-33); _qtFileDialog maps askopenfilename(s)/askopenfile(s)/asksaveasfilename/asksaveasfile/askdirectory and converts Tk filetypes tuples into Qt filter strings (mimic_tk.py:36-91). Detection of this shim elsewhere: 'pmg_qt.mimic_tk' in sys.modules at packages/engine/modules/pymol/plugins/installation.py:155 and legacysupport.py:43, 135, 153.

In the port

Legacy Tk plugins cannot run in a browser, so the plugin code stays headless in Python and plugins register menu descriptors (label path, id) over the bridge; the client renders them and calls back on click (apps/web/src/features/plugin-manager/legacyMenu.ts, LegacyPlugins.tsx). That preserves addmenuitem('A|B|C', fn) semantics as a JSON tree.

9. Dialogs owned by the main window

9.1 load_form — the .ui loader (pymol_qt_gui.py:512-545)

Tries importlib.import_module('.forms.<name>', 'pmg_qt') (pre-generated Ui_Form/Ui_Dialog), else falls back to pymol.Qt.utils.loadUi(<dir>/forms/<name>.ui, widget) (packages/engine/modules/pymol/Qt/utils.py:269-320). dialog='floating' wraps the widget in a floating QDockWidget. Available forms (packages/engine/modules/pmg_qt/forms/): askpartial, change_confirm, colors, create_shortcut, fetch, help_shortcut, load_aln, load_mae, load_map, load_mtz, load_traj, movieexport, pluginitem, pluginmanager, png, props, render, save_molecule, save_object, shortcut_menu.

9.2 Setting ▸ Colors... (edit_colors_dialog, pymol_qt_gui.py:547-611, form forms/colors.ui)

Widgets: list_colors (QListWidget, sorting enabled, filled from cmd.get_color_indices()), input_name (QLineEdit), input_R/G/B (QDoubleSpinBox, max 1.0, step per .ui), slider_R/G/B (QSlider, max 100), frame_color (QFrame preview via stylesheet background-color: rgb(...)), button_apply. Wiring: sliders → spinboxes (v/100), spinboxes → sliders (round(v*100)) + preview, input_name.textChangedload_color (cmd.get_color_index(name), -1 ⇒ ignore; then cmd.get_color_tuple(index)), list selection → name field. button_apply runs cmd.do("set_color %s, [%.2f, %.2f, %.2f]\nrecolor") and inserts+selects the name if new. A spinbox_lock flag prevents feedback loops (pymol_qt_gui.py:566-583).

9.3 Draw/Ray render dialog (render_dialog, pymol_qt_gui.py:673-790, form forms/render.ui)

Two-page QStackedWidget (stack). Page 1 fields: input_width/input_height (QSpinBox, px), input_width_units/input_height_units (QDoubleSpinBox, suffix follows input_units), input_units (QComboBox: inch / cm — factor 1.0 vs 2.54), input_dpi (editable QComboBox with QIntValidator, pre-filled from cmd.get_setting_int('image_dots_per_inch') when > 0), button_current (“Use current viewport size” → cmd.get_viewport()), button_lock (QCheckBox, aspect-ratio lock; computes form.aspectratio = w/h, unchecks itself on ZeroDivisionError), input_transparent (checkbox), button_draw, button_ray. Actions: run_drawcmd.do('draw %d, %d'); run_raycmd.set('opaque_background', not input_transparent.isChecked()) then cmd.do('ray %d, %d, async=1'); both then switch to page 2. Page 2: button_save (“Save Image to File”) → getSaveFileNameWithExt(filter='PNG File (*.png)') then cmd.png(fname, prior=1, dpi=<dpi>); button_clip (“Copy Image to Clipboard”) → _copy_image(cmd, False, dpi) inside PopupOnException; button_back (”< Back”) → page 1. Circular-update protection uses UpdateLock([ZeroDivisionError]) with @lock.skipIfCircular (packages/engine/modules/pymol/Qt/utils.py:5-62). _copy_image (pymol_qt_gui.py:1170-1185): renders to a tempfile.mktemp('.png'), loads a QImage, QApplication.clipboard().setImage(...), unlinks, prints " Image copied to clipboard" when not quiet. It is installed as pymol.cmd._copy_image at pymol_qt_gui.py:1242.

9.4 Lazily-constructed panels held by the window (pymol_qt_gui.py:103-108)

dialog_png, advanced_settings_dialog, props_panel, builder, shortcut_menu_filter_dialog, scene_panel_dialog — each created on first use in settings_edit_all_dialog/open_props_dialog/open_builder_panel/shortcut_menu_edit_dialog/scene_panel_menu_dialog.

9.5 About PyMOL (show_about, pymol_qt_gui.py:899-916)

QMessageBox.about(self, "About PyMOL", text) where text is exactly:
(the 'Open-Source Build' line is unconditional in this build, pymol_qt_gui.py:908).

9.6 Splash

There is no Qt splash screen. The splash is drawn inside the OpenGL viewport: options.show_splash (packages/engine/modules/pymol/invocation.py:142) causes cmd.splash(1) to be inserted as the first deferred command (invocation.py:529-530); the command is packages/engine/modules/pymol/commanding.py:297, and the C++ side consumes it via OrthoInit(G, G->Option->show_splash) (packages/engine/layer5/PyMOL.cpp:1954). -q clears it (invocation.py:488). Web implication: the splash is part of the rendered ortho layer, so it either arrives as CGO/image data over the wire or must be re-implemented as a React overlay. Flag this to the viewport/ortho agent.

10. Drag-and-drop of files

Two independent drop targets:
  1. GL viewport (pymol_gl_widget.py:118-119 enables it; handlers at :256-270): dragEnterEvent accepts anything with URLs; dropEvent iterates event.mimeData().urls(), converting local files with toLocalFile() else toString() (so remote URLs are passed through), and calls self.gui.load_dialog(url) for each.
  2. Command line — text preview insert, see §3.5.
  3. App icon / FinderPyMOLApplication (pymol_qt_gui.py:1127-1166): handle_file_open ignores events until the first ApplicationActivate (so sys.argv isn’t hijacked by Qt), then handles QEvent.FileOpen:
    • if not options.reuse_helper and cmd.get_names()window.new_window([ev.file()]) (new process);
    • if options.auto_reinitialize (pymol -I -U) ⇒ cmd.reinitialize();
    • if the file ends in .pswcmd.set('presentation'), cmd.set('internal_gui', 0), cmd.set('internal_feedback', 0), cmd.full_screen('on');
    • then window.load_dialog(ev.file()).

In the port

Browser drops give File objects, not paths. The backend is local with full filesystem access, so there are two paths: a drop carrying a real path (text/uri-list from a native app) sends the path and calls load_dialog; otherwise the bytes are uploaded to /upload, written to a temp dir and loaded from there. apps/web/src/features/files/globalDrop.ts and FileDropTarget.tsx implement it. The .psw presentation branch and the “open in a new instance” branch are process-level and are not reproduced.

11. Application startup and shutdown flow

Startup (execapp, pymol_qt_gui.py:1193-1267)

Entered from pymol.launch (packages/engine/modules/pymol/__init__.py:405-425): if options.gui == 'pmg_qt' and not no_gui/testing, from pmg_qt import pymol_qt_gui; return pymol_qt_gui.execapp(); on ImportError it prints Qt not available (...), using GLUT/Tk interface and falls back to pmg_tk. no_gui goes to _launch_no_gui() (__init__.py:386-403), which spins p.draw() until idle conditions clear. execapp steps, in order:
  1. sys.excepthook = traceback.print_exception — “don’t let exceptions stop PyMOL” (:1200-1202);
  2. Windows: AA_UseDesktopOpenGL (:1204-1206);
  3. AA_EnableHighDpiScaling unless QT_SCALE_FACTOR/QT_SCREEN_SCALE_FACTORS set (:1208-1212);
  4. Windows taskbar icon via SetCurrentProcessExplicitAppUserModelID('com.schrodinger.pymol') (:1214-1218);
  5. app = PyMOLApplication(['PyMOL']); app.setWindowIcon(make_pymol_qicon())$PYMOL_DATA/pymol/icons/icon2.svg (:1188-1190, :1220-1221);
  6. window = PyMOLQtGUI(); setWindowTitle("PyMOL"); app.setDesktopFileName("org.pymol.PyMOL") (gnome/wayland wmclass fix) (:1223-1227);
  7. Command overloads via commandoverloaddecorator (:1033-1038 — copies the docstring from pymol.cmd.<name>, setattrs onto pymol.cmd, then pymol.cmd.extend):
    • viewport(w,h)window.viewportsignal.emit(int(w), int(h)) (thread-safe hop into pymolviewport, :1229-1231),
    • full_screen(toggle) → resolves pymol.viewing.toggle_dict/toggle_sc then window.toggle_fullscreen(toggle) (:1233-1237);
  8. pymol.gui.createlegacypmgapp = window.createlegacypmgapp (:1239-1240);
  9. pymol.cmd._copy_image = _copy_image; pymol.cmd._call_in_gui_thread = MainThreadCaller() (:1242-1243; MainThreadCaller at Qt/utils.py:147);
  10. pymol.cmd._call_with_opengl_context = lambda func: _call_in_gui_thread(lambda: with window.pymolwidget: func()) (:1245-1252) — the backend calls back into the GUI to get a current GL context;
  11. window.show(); window.raise_();
  12. if options.win_xy_set: viewport(fb_scale * win_x, fb_scale * win_y) (:1257-1261);
  13. if options.plugins: window.initializePlugins() (:1263-1265);
  14. app.exec().
Window-construction side effects worth noting: the PyMOLGLWidget constructor starts the PyMOL engine (self.pymol = PyMOL(); self.pymol.start(), pymol_gl_widget.py:99-101) and installs pcatch._install() to capture Python stdout into the feedback queue (pymol_gl_widget.py:104-105).

Shutdown

closeEventself.cmd.quit() (pymol_qt_gui.py:56-57). File ▸ QuitQApplication.instance().quit() (:875-876). No unsaved-session prompt exists.

window_cmd — the cmd.window backend hook (pymol_qt_gui.py:996-1030)

Reached from cmd.window(action, x, y, w, h) (packages/engine/modules/pymol/viewing.py:1430-1457) which resolves the action name through window_dict (packages/engine/modules/pymol/constants.py:150-152): hide0, show1, position2, size3, box4, maximize5, fit6, focus7, defocus8. fit clamps the frame geometry into screen().availableGeometry() and no-ops when Maximized/FullScreen.

viewport / pymolviewport (pymol_qt_gui.py:59-81)

viewportsignal = QtCore.Signal(int, int) connected to pymolviewport (:100) so viewport resizes issued from worker threads land on the GUI thread. pymolviewport maintains aspect ratio when only one dimension is given, and resizes the window by QSize(w-cw, h-ch)/fb_scale, i.e. it grows the window to make the GL area match the requested pixel size.

12. Backend contract summary for the bridge

Commands/APIs this area calls (all confirmed present): cmd.do, cmd.set(name_or_index, value, log=1, quiet=0), cmd.get, cmd.get_setting_tuple, cmd.get_setting_int, cmd.setting._get_index, cmd.get_setting_updates (setting.py:440), cmd._get_feedback (internal.py:593), cmd.get_progress (monitoring.py:5), cmd.interrupt (locking.py:88), cmd.get_viewport, cmd.get_view, cmd.set_view, cmd.reset, cmd.zoom, cmd.orient, cmd.unpick, cmd.deselect, cmd.rock, cmd.rewind, cmd.backward, cmd.forward, cmd.ending, cmd.mplay, cmd.mstop, cmd.mclear, cmd.rebuild, cmd.reinitialize, cmd.save, cmd.png, cmd.run, cmd.cd, cmd.system, cmd.log_open/log_close/resume (commanding.py:107, 206, 52), cmd.quit, cmd.undo, cmd.redo, cmd.scene, cmd.cache (exporting.py:48), cmd.clip, cmd.wizard, cmd.replace_wizard (wizarding.py:94), cmd.config_mouse (controlling.py:168), cmd.mouse (controlling.py:609), cmd.meter_reset (viewing.py:1800), cmd.sculpt_purge (editing.py:104), cmd.mdelete (moving.py:591), cmd.get_movie_length (querying.py:730), cmd.get_version (querying.py:603), cmd.get_color_indices/get_color_index/get_color_tuple (querying.py:843, 858, 825), cmd.get_names, cmd.as_pathstr (cmd.py:116), cmd.exp_path, cmd.set_key, cmd.extend, cmd._parser.complete (parser.py:524), cmd.editor.attach_amino_acid / attach_fragment (editor.py:98, 51), cmd.movie.add_blank/add_roll/add_rock/add_nutate/add_scenes/add_state_loop/add_state_sweep (movie.py:268, 296, 346, 433, 562, 409, 384), cmd.util.performance/ray_shadows/modernize_rendering (util.py:571, 821, 553). Engine-loop APIs (from pymol_gl_widget.py): pymol.start(), pymol.idle(), pymol.getRedisplay(), pymol.draw(), pymol.reshape(w,h,True), pymol.button(...), pymol.drag(...). Bridge hooks the backend expects the front end to provide (currently monkey-patched in execapp): pymol.cmd._copy_image, pymol.cmd._call_in_gui_thread, pymol.cmd._call_with_opengl_context, pymol.gui.createlegacypmgapp, and the overloaded cmd.viewport / cmd.full_screen. pymol.gui.get_qtwindow() (packages/engine/modules/pymol/gui.py:28-36) is queried by cmd.window (viewing.py:1450), pymol.gui.save_as/save_image (gui.py:70-92) and plugins/legacysupport.py:76. These are the exact seams the bridge fillspackages/bridge/tenmol_bridge/shims.py.

13. Constraints this area lives under

  1. pymol.cmd._call_with_opengl_context (pymol_qt_gui.py:1245-1252) makes the backend depend on a live, current OpenGL context in the GUI process. With rendering moved to the browser there is no such context in Python. Anything routed through it (shader-dependent operations, ray/draw/png in some paths) must either get a headless/offscreen GL context in the Python process or fail loudly.
  2. _copy_image writes a PNG to a temp file and pushes it to the OS clipboard (pymol_qt_gui.py:1170-1185). In the browser this becomes a base64 data transfer + navigator.clipboard.write(ClipboardItem), which requires a user gesture and HTTPS/localhost.
  3. Feedback and setting sync are polled at 500 ms (pymol_qt_gui.py:958). Naively porting the poll over WebSocket is wasteful; push is the right model, but cmd.get_setting_updates() is a drain (consume-once) API — only one consumer may call it, so the bridge must own it and fan out.
  4. cmd.get_setting_updates() being consume-once also means the internal GUI (packages/engine/layer2/3 ortho widgets) and the web client compete for the same queue. Verify with the internal-GUI agent.
  5. _addmenu silently drops items whose command is None (pymol_qt_gui.py:306) and prints error: for unknown types (:343). A serialized menu tree must define what None means (disabled vs omitted).
  6. QActionGroups are keyed by setting name only (pymol_qt_gui.py:322-327); a naive React port that scopes radio groups per submenu will behave differently (arguably better) than PyMOL. Decide and document.
  7. Tear-off menus (setTearOffEnabled(True), pymol_qt_gui.py:297, mimic_pmg_tk.py:90) have no browser equivalent. Either drop them or implement detachable palettes.
  8. File ▸ New PyMOL Window spawns a whole new OS process (_gui.py:42-53) — contradicts “one process, one browser client”. Needs a product decision (disable, or spawn a second backend + open a new tab on a new port).
  9. Legacy Tk plugins (mimic_pmg_tk.PMGApp creating a real hidden Tk() root, mimic_pmg_tk.py:140-168) cannot run in the browser. mimic_tk.py also installs a global sys.meta_path hook (mimic_tk.py:128) that will still fire in the headless backend and hand plugins Qt dialogs that no one can see.
  10. Filesystem completion in the command line (parser.py:561-593) prints candidate lists into the feedback stream instead of returning them — the web UI cannot render a proper completion popup without either re-implementing or extending _complete (which we may not do: source is read-only).
  11. confirm_quit (pymol_qt_gui.py:875) does not actually confirm; closeEvent calls cmd.quit() unconditionally (:56-57). A browser tab close cannot reliably run this — the bridge needs a heartbeat/lifecycle policy.
  12. The stylesheet packages/engine/data/pmg_qt/styles/pymol.sty is nearly empty; almost all visual identity comes from the native Qt theme. The React shell has no design source of truth to copy — expect a design pass.
  13. pymolviewport resizes the window to satisfy cmd.viewport w, h (pymol_qt_gui.py:62-81). A browser cannot resize its own window reliably; cmd.viewport must instead resize the canvas element and report the achieved size back.
  14. Drag-and-drop of files from the OS gives no path in a plain browser (§10).

14. Decisions this map fed

  • file_dialogs.load_dialog does not add to the recent list. Only session_save_as calls recent_filenames_add in pymol_qt_gui.py. The bridge does not reimplement the recent-files store: it binds PyMOL’s own unbound _recent_filenames_lazy_init / recent_filenames / recent_filenames_add (packages/engine/modules/pymol/_gui.py:975-1032) onto its own carrier, so ~/.pymol/recent.db keeps the same schema and prune behaviour (packages/bridge/tenmol_bridge/panels/files.py).
  • Open Recent stays server-side. ~/.pymol/recent.db (_gui.py:986) is the store; nothing is mirrored into browser storage.
  • Display ▸ Internal GUI / Internal Prompt / Internal Feedback / Overlay are still in the menu (internal_gui, internal_prompt, internal_feedback, overlay, _gui.py:409-418) but the bridge holds internal_gui at 0 deliberately, because get_viewport() must equal the requested size. apps/web/src/shell/orthoPanel.ts records the measured effect of each value.
  • Stereo modes (_gui.py:420-433) are kept live and annotated rather than pruned, because the menu tree is harvested rather than hand-written. apps/web/src/features/menubar/stereo.ts records which ones the engine actually honours, including the two that answer ok and then report an error only as a feedback line (quadbuffer, openvr), and the two whose labels lie upstream (Chromadepth turns stereo off; Swap Sides touches neither stereo nor stereo_mode).
  • Setting ▸ Keyboard Shortcuts... persistence stays server-side. ~/.pymol/shortcuts_save.json is cmd.shortcut_dict, written verbatim by save_shortcut.save_shortcuts (packages/engine/modules/pymol/save_shortcut.py), so the editor calls that function rather than inventing a format (apps/web/src/features/shortcuts/ShortcutEditor.tsx). Reading the live bindings needed a bridge module: cmd.key_mappings is a plain dict attribute and the dispatcher resolves callables only, so packages/bridge/tenmol_bridge/panels/shortcuts.py reduces each binding to {kind, command, callable} — a (function, args, kwargs) value cannot cross the wire.
  • Legacy Tk plugins register menu descriptors over the bridge and the client renders them (apps/web/src/features/plugin-manager/LegacyPlugins.tsx, legacyMenu.ts), preserving addmenuitem('A|B|C', fn) semantics as a JSON tree.