Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Desktop client

Explanation

Tabs, panes, and layout

How the desktop main window, sidebar, split panes, tab manager, and pane controller open, track, close, and persist screens.

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

  • MainWindow puts Sidebar, PaneContainer, and ChatWidget in one horizontal splitter, with StatusBarWidget below (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).
  • TabManager keeps one TabDescriptor per 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).
  • PaneContainer splits panes in nested QSplitter widgets and saves the layout, with the open section tabs, in the local setting panes/layout (src/cluster_erp/desktop/widgets/pane_container.py:28, src/cluster_erp/desktop/widgets/pane_container.py:261-272).
  • PaneController offers 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):

  1. Sidebar, created with the set of custom section keys this client can build (src/cluster_erp/desktop/widgets/main_window.py:79-86).
  2. PaneContainer with one pane, which receives a welcome tab under the module name app and section welcome (src/cluster_erp/desktop/widgets/main_window.py:88-101).
  3. 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).
  4. StatusBarWidget and PaneController (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 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 custom section that this client cannot build.
  • It names a required_permission that the user does not hold.
  • It is a list section and the user lacks <entity>.read, or a settings section and the user lacks <tab>.read for 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:

  1. The caller builds the screen widget and calls open_tab with module, section, title, and an optional record id (src/cluster_erp/desktop/core/tab_manager.py:87-94).
  2. find_existing looks 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).
  3. Otherwise a TabDescriptor is stored, the tab is added and activated, the widget gets the property tab_id, and a custom close button is placed on the tab (src/cluster_erp/desktop/core/tab_manager.py:110-125).
  4. 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). GenericDetailView does this when its form changes and clears it after a save (src/cluster_erp/desktop/widgets/generic/generic_detail_view.py:571-588).
  5. close_tab on a dirty tab asks with common.unsaved_changes. A forced close skips the question (src/cluster_erp/desktop/core/tab_manager.py:139-161). close_all_tabs asks once with tabs.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-105 states 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-356 states 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-43 states that the sidebar never leads to a placeholder.
  • open_entity is handled by the window: the docstring in src/cluster_erp/desktop/widgets/main_window.py:273-280 states 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, and is_dirty. TabDescriptor has exactly these fields (src/cluster_erp/desktop/core/tab_manager.py:16-24).
  • A pane always exists: active_pane creates one when the list is empty (src/cluster_erp/desktop/widgets/pane_container.py:75-83).

Interaction with other parts

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_ui renames section tabs after a language change and leaves record tabs unchanged (src/cluster_erp/desktop/widgets/main_window.py:636-648).
  • PaneWidget sets its tab widget closable, and TabManager, 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

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13