The desktop main window is a sidebar, a pane area that can be split, and a chat panel side by side. Each pane holds tabs, and each tab holds one screen. This page explains widgets/main_window.py, widgets/sidebar.py, widgets/pane_container.py, widgets/pane_widget.py, core/tab_manager.py, and core/pane_controller.py under src/cluster_erp/desktop/. The screens inside the tabs are described on generic widgets and module screens.
In short¶
MainWindowputsSidebar,PaneContainer, andChatWidgetin one horizontal splitter, withStatusBarWidgetbelow (src/cluster_erp/desktop/widgets/main_window.py:69-137).- A sidebar click opens a section tab. If the same section is already open in any pane, that tab is activated instead (
src/cluster_erp/desktop/widgets/main_window.py:314-343). TabManagerkeeps oneTabDescriptorper tab, refuses duplicates of the same module, section, and record, and asks before closing a tab with unsaved changes (src/cluster_erp/desktop/core/tab_manager.py:17-24,src/cluster_erp/desktop/core/tab_manager.py:87-161).PaneContainersplits panes in nestedQSplitterwidgets and saves the layout, with the open section tabs, in the local settingpanes/layout(src/cluster_erp/desktop/widgets/pane_container.py:28,src/cluster_erp/desktop/widgets/pane_container.py:261-272).PaneControlleroffers the same pane and tab operations through string ids and plain dictionaries (src/cluster_erp/desktop/core/pane_controller.py:23-30).
How it works¶
Main window¶
_setup_ui builds the window from left to right (src/cluster_erp/desktop/widgets/main_window.py:69-137):
Sidebar, created with the set of custom section keys this client can build (src/cluster_erp/desktop/widgets/main_window.py:79-86).PaneContainerwith one pane, which receives a welcome tab under the module nameappand sectionwelcome(src/cluster_erp/desktop/widgets/main_window.py:88-101).ChatWidget, which can be shown and hidden (src/cluster_erp/desktop/widgets/main_window.py:103-112,src/cluster_erp/desktop/widgets/main_window.py:217-229).StatusBarWidgetandPaneController(src/cluster_erp/desktop/widgets/main_window.py:126-137).
_on_navigate handles four reserved sections of the module app: settings opens the preferences dialog, accessibility opens the accessibility dialog, chat toggles the chat panel, and logout signs out after a confirmation. Every other request opens a module tab (src/cluster_erp/desktop/widgets/main_window.py:188-201, src/cluster_erp/desktop/widgets/main_window.py:231-242).
Building a section screen¶
_create_module_widget finds the section in the module's sidebar metadata and chooses the screen by view_type (src/cluster_erp/desktop/widgets/main_window.py:347-381):
view_type |
Screen | Source |
|---|---|---|
list |
GenericListView for the section's entity, with an optional list_endpoint |
src/cluster_erp/desktop/widgets/main_window.py:399-429 |
tree |
GenericTreeView |
src/cluster_erp/desktop/widgets/main_window.py:431-454 |
settings |
GenericSettingsView with one tab per entity key in tabs |
src/cluster_erp/desktop/widgets/main_window.py:479-499 |
tabbed_list |
GenericTabbedListView built from entity_tabs |
src/cluster_erp/desktop/widgets/main_window.py:456-477 |
custom |
A class from _CUSTOM_WIDGET_REGISTRY, looked up as <module>.<custom_widget> and imported on first use |
src/cluster_erp/desktop/widgets/main_window.py:501-553 |
A section that the metadata does not declare gives a placeholder with the text of app.section_missing. A declared section without a buildable screen gives the text of app.not_implemented (src/cluster_erp/desktop/widgets/main_window.py:353-357, src/cluster_erp/desktop/widgets/main_window.py:555-576). When the section metadata has a description, one line with it is inserted at the top of the screen (src/cluster_erp/desktop/widgets/main_window.py:383-397). The tab title is the section label in the current language (src/cluster_erp/desktop/widgets/main_window.py:595-604).
Sidebar¶
Sidebar is 200 pixels wide (src/cluster_erp/desktop/widgets/sidebar.py:54). It lists one button per accessible section of every visible module, in module order, with a separator between groups (src/cluster_erp/desktop/widgets/sidebar.py:159-215). A section is hidden in three cases (src/cluster_erp/desktop/widgets/sidebar.py:217-240):
- It is a
customsection that this client cannot build. - It names a
required_permissionthat the user does not hold. - It is a
listsection and the user lacks<entity>.read, or asettingssection and the user lacks<tab>.readfor every tab.
When the permission list is empty, every section is shown (src/cluster_erp/desktop/widgets/sidebar.py:224-227). Modules hidden by the app scope never reach the sidebar; see metadata store and foreign-key cache. A fixed bottom area holds the chat, Accessibility, Preferences (key sidebar.settings), and Logout (key auth.logout) buttons (src/cluster_erp/desktop/widgets/sidebar.py:112-151).
Tab lifecycle¶
The state diagram shows one tab from opening to closing.
stateDiagram-v2 [*] --> Built: caller builds widget Built --> Activated: find_existing finds same module, section, entity_id Activated --> [*]: new widget deleted with deleteLater Built --> Open: addTab, TabDescriptor stored Open --> Dirty: mark_dirty(True) Dirty --> Open: mark_dirty(False) after save Open --> Closed: close_tab Dirty --> Closed: close_tab and user confirms Dirty --> Dirty: close_tab and user declines Closed --> [*]: removeTab, deleteLater, descriptor removed
In words:
- The caller builds the screen widget and calls
open_tabwith module, section, title, and an optional record id (src/cluster_erp/desktop/core/tab_manager.py:87-94). find_existinglooks for a tab with the same module, section, and record id. If one exists, it is activated and the new widget is deleted, so its signal subscriptions and workers end (src/cluster_erp/desktop/core/tab_manager.py:95-108,src/cluster_erp/desktop/core/tab_manager.py:173-179).- Otherwise a
TabDescriptoris stored, the tab is added and activated, the widget gets the propertytab_id, and a custom close button is placed on the tab (src/cluster_erp/desktop/core/tab_manager.py:110-125). - A screen with unsaved edits calls
mark_dirty, which prefixes the tab text with a bullet (src/cluster_erp/desktop/core/tab_manager.py:181-189).GenericDetailViewdoes this when its form changes and clears it after a save (src/cluster_erp/desktop/widgets/generic/generic_detail_view.py:571-588). close_tabon a dirty tab asks withcommon.unsaved_changes. A forced close skips the question (src/cluster_erp/desktop/core/tab_manager.py:139-161).close_all_tabsasks once withtabs.close_all_unsaved(src/cluster_erp/desktop/core/tab_manager.py:195-211).
The right-click menu of a tab bar offers Close All Tabs (key tabs.close_all) (src/cluster_erp/desktop/widgets/pane_widget.py:211-220).
Record tabs¶
Record cards open as tabs whose section is the entity type and whose record id is set. A list view opens one on a double-click (src/cluster_erp/desktop/widgets/generic/generic_list_view.py:395-431). MainWindow._on_open_entity opens one in the active pane for the open_entity signal, using the title the sender passes or the entity label (src/cluster_erp/desktop/widgets/main_window.py:270-312). _on_navigate_to opens a section and passes the parameters to the screen's apply_navigation method when it has one (src/cluster_erp/desktop/widgets/main_window.py:203-215).
Panes¶
Each PaneWidget has a 2-pixel active indicator, a PaneBar with split, maximise, and close buttons, and a QTabWidget with movable tabs managed by its own TabManager (src/cluster_erp/desktop/widgets/pane_widget.py:105-167). A mouse press in an inactive pane makes it active (src/cluster_erp/desktop/widgets/pane_widget.py:206-225). Focus moving into a pane also makes it active (src/cluster_erp/desktop/widgets/pane_container.py:369-380).
split_pane adds the new pane next to the old one in the same splitter when the orientation matches, wraps the old pane in a new splitter when it does not, and creates a root splitter for the first split. The two panes share the old size equally (src/cluster_erp/desktop/widgets/pane_container.py:88-152). close_pane refuses to close the last pane, asks when the pane has unsaved tabs, and removes splitters left with one child (src/cluster_erp/desktop/widgets/pane_container.py:154-188, src/cluster_erp/desktop/widgets/pane_container.py:323-346).
| Shortcut | Action | Source |
|---|---|---|
Alt+[ |
Split the active pane left and right | src/cluster_erp/desktop/widgets/pane_container.py:387-391 |
Alt+] |
Split the active pane top and bottom | src/cluster_erp/desktop/widgets/pane_container.py:393-397 |
Alt+Z |
Close the active pane | src/cluster_erp/desktop/widgets/pane_container.py:399-401 |
Layout persistence¶
MainWindow.closeEvent asks once when any pane has unsaved tabs. It then saves the pane layout, the window geometry, and the window state, and closes the API client (src/cluster_erp/desktop/widgets/main_window.py:684-701). _serialize writes panes and splitters as nested dictionaries with orientation and sizes. It keeps only section tabs; record tabs are skipped (src/cluster_erp/desktop/widgets/pane_container.py:417-447). At start-up restore_pane_layout rebuilds the tree and reopens each saved section through the same widget factory (src/cluster_erp/desktop/widgets/main_window.py:669-682, src/cluster_erp/desktop/widgets/pane_container.py:449-509). A missing or broken saved layout leaves one empty pane (src/cluster_erp/desktop/widgets/pane_container.py:274-285, src/cluster_erp/desktop/widgets/pane_container.py:466-472).
Pane controller¶
PaneController wraps the container with string ids. It lists panes and tabs, splits, closes, maximises, restores, and activates panes, opens, closes, and activates tabs, and saves the layout (src/cluster_erp/desktop/core/pane_controller.py:54-230). The docstring states that an assistant or any other automation layer can drive the workspace this way without widget references (src/cluster_erp/desktop/core/pane_controller.py:24-30). MainWindow creates it and exposes it as the property pane_controller (src/cluster_erp/desktop/widgets/main_window.py:134-137, src/cluster_erp/desktop/widgets/main_window.py:182-184). No other code in src/cluster_erp/desktop/ calls it.
Why it is built this way¶
- Duplicate detection deletes the new widget: the comment in
src/cluster_erp/desktop/core/tab_manager.py:100-105states that the orphan would otherwise keep its signal registration and workers alive as an invisible view. - Missing section metadata reads as a fault: the comment in
src/cluster_erp/desktop/widgets/main_window.py:355-356states that a section the server did not send is a fault, not an unbuilt feature. - Custom sections are filtered in the sidebar: the comment in
src/cluster_erp/desktop/widgets/sidebar.py:41-43states that the sidebar never leads to a placeholder. open_entityis handled by the window: the docstring insrc/cluster_erp/desktop/widgets/main_window.py:273-280states that a custom widget has no tab manager, and the window owns the panes.
Rules and boundaries¶
- The project's written engineering rules require duplicate detection, dirty-state warnings, and a descriptor with
tab_id,module,section,entity_id,title,widget, andis_dirty.TabDescriptorhas exactly these fields (src/cluster_erp/desktop/core/tab_manager.py:16-24). - A pane always exists:
active_panecreates one when the list is empty (src/cluster_erp/desktop/widgets/pane_container.py:75-83).
Interaction with other parts¶
- Screens reach the tab system only through the
TabManagerthey receive or through thenavigate_toandopen_entitysignals; see signal bus and watch_entity. - The web client's equivalent of section tabs is described on module apps and app scope. Its server-side layout store is described on user UI state.
Limits and open points¶
- Record tabs are not saved, so they do not reopen after a restart (
src/cluster_erp/desktop/widgets/pane_container.py:421-422). _retranslate_uirenames section tabs after a language change and leaves record tabs unchanged (src/cluster_erp/desktop/widgets/main_window.py:636-648).PaneWidgetsets its tab widget closable, andTabManager, created right after, sets it not closable and adds its own button (src/cluster_erp/desktop/widgets/pane_widget.py:154,src/cluster_erp/desktop/core/tab_manager.py:84,src/cluster_erp/desktop/core/tab_manager.py:122-123).- The header button of the sidebar opens a credits dialog (
src/cluster_erp/desktop/widgets/sidebar.py:70-74).
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| tab descriptor | TabDescriptor |
The record that the tab manager keeps for one open tab. | sekme tanımlayıcı | src/cluster_erp/desktop/core/tab_manager.py:17 |
| tab manager | TabManager |
The object that opens, deduplicates, marks, and closes the tabs of one pane. | sekme yöneticisi | src/cluster_erp/desktop/core/tab_manager.py:73 |
| pane | PaneWidget |
One area of the workspace with its own tab bar and tab manager. | bölme | src/cluster_erp/desktop/widgets/pane_widget.py:99 |
| pane controller | PaneController |
The string-id interface for pane and tab operations. | bölme denetleyicisi | src/cluster_erp/desktop/core/pane_controller.py:23 |
| custom widget registry | _CUSTOM_WIDGET_REGISTRY |
The map from module.custom_widget keys to hand-written screen classes. |
özel bileşen kaydı | src/cluster_erp/desktop/widgets/main_window.py:501 |
Related pages¶
Verified against commit 92f8051 on 2026-11-13.