This document provides an overview of the User Interface (UI) structure and development practices for the Grazr application. It's intended for contributors working on UI features, custom widgets, or styling. Grazr's UI is built using PySide6 (the official Python bindings for Qt 6).
- Overview of UI Architecture
- Main Window (
main_window.py) - Page Widgets (
grazr/ui/*.py) - Dialogs (
grazr/ui/*.py) - Custom Widgets (
grazr/ui/widgets/*.py) - Styling with QSS (
style.qss) - Icon and Asset Management (
resources.qrc) - Signal and Slot Mechanism
- Error Handling and User Feedback in UI
- Contributing to the UI
Grazr's UI is built using PySide6. The main interaction point is the MainWindow, which hosts several pages in a QStackedWidget. Each page is responsible for a specific domain (Services, Sites, PHP, Node.js). Dialogs are used for specific input tasks like adding services or configuring PHP. Custom widgets are used to create reusable UI elements, especially in lists.
Key principles:
- Responsiveness: Long-running operations are delegated to a
Workerthread to keep the UI from freezing. - Modularity: Pages and managers are designed to handle specific aspects of the application.
- Signal/Slot Mechanism: Qt's signal and slot mechanism is used extensively for communication between UI components, the
MainWindow, and theWorker.
The grazr/ui/main_window.py defines the MainWindow class, which is the central hub of the application's user interface.
- Initializes the main application layout, including the sidebar for navigation and the
QStackedWidgetfor displaying different pages. - Instantiates and manages the page widgets (
ServicesPage,SitesPage,PhpPage,NodePage). - Handles top-level UI actions like switching pages, showing/hiding the log area, and system tray interactions.
- Serves as the primary recipient of signals emitted from page widgets indicating user actions (e.g., "start service," "add site").
- Delegates tasks to the
Workerthread by emitting thetriggerWorkersignal. - Receives results from the
Workervia thehandleWorkerResultslot and updates the UI or relevant pages accordingly. - Manages global UI elements like the page title and header action buttons.
- The
self.stacked_widgetholds instances ofServicesPage,SitesPage,PhpPage, andNodePage. - The
self.sidebar(aQListWidget) controls which page is currently displayed in theQStackedWidgetvia thechange_pageslot. - The
change_pageslot updates the page title, clears and adds page-specific header action buttons, and callsrefresh_current_page. refresh_current_pagecalls therefresh_data()method of the currently visible page to ensure its content is up-to-date.
MainWindow connects to various signals emitted by the page widgets. For example:
self.services_page.serviceActionTriggered.connect(self.on_service_action_triggered)self.sites_page.linkDirectoryClicked.connect(self.add_site_dialog)self.php_page.managePhpFpmClicked.connect(self.on_manage_php_fpm_triggered)These handler slots inMainWindowtypically prepare data and then emitself.triggerWorkerto offload the actual work.
MainWindowowns theQThreadand theWorkerobject (self.thread,self.worker).- The
triggerWorker = Signal(str, dict)signal is used to send tasks to theWorker.str:task_name(e.g., "start_internal_nginx", "add_site").dict:data_dictcontaining parameters for the task.
The handleWorkerResult(self, task_name, context_data, success, message) slot receives the outcome of background tasks.
- It logs the result.
- It determines which page or UI element might need updating based on
task_nameandcontext_data. - It calls appropriate refresh methods for pages or specific services (e.g.,
self.sites_page.refresh_data(),self._refresh_specific_service_on_page(service_id)), often usingQTimer.singleShotto allow the event loop to process before intensive UI updates. - It re-enables controls on the relevant page that might have been disabled while the task was running.
MainWindowhas aQTextEdit(self.log_text_area) that can be toggled visible.- The
log_message(self, message)method appends messages to this text area and also logs them using theloggingmodule.
- If a system tray is available,
main.pycreates aQSystemTrayIcon. MainWindowprovides methods liketoggle_visibility()to show/hide the main window, and the tray icon can trigger these.- The tray menu can also have actions to quit the application or perform global actions like "Start All Services."
Each "page" in Grazr is a QWidget (or a subclass) that occupies the main content area of the MainWindow.
- Purpose: Lists all manageable services (Nginx, and user-configured instances of MySQL, PostgreSQL, Redis, MinIO). Also displays status for system Dnsmasq.
- Structure: Uses a
QListWidget(self.service_list_widget) where each item is a customServiceItemWidget. It also has a details pane (QStackedWidgetnamedself.details_stack) to show logs and environment variables for a selected service. refresh_data():- Loads configured services from
services_config_manager.load_configured_services(). - Always includes an entry for the internal Nginx.
- Populates
services_by_categoryafter determining thewidget_key(unique ID for the widget, which isconfig_idfor user-added services or the fixedprocess_idfor Nginx) andprocess_id_for_pm(ID used byprocess_manager). - Clears and repopulates
self.service_list_widget, creating or reusingServiceItemWidgetinstances. Stores these widgets inself.service_widgetskeyed by theirwidget_key. - Calls
_trigger_single_refresh(widget_key)for each service to initiate status updates viaMainWindow.
- Loads configured services from
- Interaction: Emits
serviceActionTriggered(widget_key, action)for start/stop,settingsClicked(widget_key)to show details, andremoveServiceRequested(config_id)for user-added services.
- Purpose: Manages local project sites. Lists linked sites and provides a detail view for configuration.
- Structure: Uses a
QListWidget(self.site_list_widget) withSiteListItemWidgetfor each site. A details pane shows settings for the selected site (PHP version, Node version, domain, SSL, etc.). refresh_site_list(): Loads sites fromsite_manager.load_sites(), clears and repopulates the list withSiteListItemWidgetinstances.- Interaction: Emits signals for actions like
linkDirectoryClicked,unlinkSiteClicked,saveSiteDomainClicked,setSitePhpVersionClicked,enableSiteSslClicked, etc. These signals carrysite_infoorsite_id.
- Purpose: Manages PHP versions and their FPM services, and provides access to INI/extension configuration.
- Structure: Typically lists available bundled PHP versions. For each version, it might show FPM status and provide buttons to start/stop FPM, and a button to open the
PhpConfigurationDialog. refresh_data(): Callsphp_manager.detect_bundled_php_versions()andphp_manager.get_php_fpm_status()for each version to update the display.- Interaction: Emits
managePhpFpmClicked(version, action)andconfigurePhpVersionClicked(version).
- Purpose: Manages Node.js versions via the bundled NVM.
- Structure: Lists available remote Node.js versions (especially LTS) and currently installed versions. Provides buttons to install/uninstall.
refresh_data(): Callsnode_manager.list_installed_node_versions()and potentiallynode_manager.list_remote_lts_versions()to update its lists.- Interaction: Emits
installNodeRequested(version_string)anduninstallNodeRequested(version_string).
refresh_data(): Most pages have this method, called byMainWindow.refresh_current_page()when the page becomes active, or byMainWindow.handleWorkerResult()after a relevant task completes. This method is responsible for fetching the latest data from managers and updating the page's UI elements.set_controls_enabled(bool): Disables interactive elements while a background task related to the page is running, and re-enables them when the task is done. Called byMainWindow.handleWorkerResult().- Signal Emission: Pages emit signals to
MainWindowto request actions, passing necessary data.
Dialogs are used for focused user input.
- Purpose: Allows users to add new instances of services like MySQL, PostgreSQL (selecting a major version), Redis, MinIO.
- Structure: Uses
QComboBoxfor selecting service category and then service type (e.g., "PostgreSQL 16").QLineEditfor display name,QSpinBoxfor port,QCheckBoxfor autostart. - Logic:
- Populates service type combo based on
config.AVAILABLE_BUNDLED_SERVICES. - When a service type is selected, it pre-fills the display name and default port.
get_service_data()returns a dictionary withservice_type,name,port,autostart.MainWindowuses this to callservices_config_manager.add_configured_service().
- Populates service type combo based on
- Purpose: Allows users to edit common
php.inisettings and manage extensions for a specific PHP version. - Structure: Likely uses
QTabWidgetfor "INI Settings" and "Extensions".- INI Settings Tab: Displays
QLineEdits for settings likememory_limit,upload_max_filesize, etc. Reads initial values usingphp_manager.get_ini_value(). - Extensions Tab: Lists available extensions (from
php_manager.list_available_extensions()) and shows their enabled status (fromphp_manager.list_enabled_extensions()) usingQCheckBoxes or aQListWidgetwith checkable items.
- INI Settings Tab: Displays
- Interaction: Emits signals like
saveIniSettingsRequested(version, settings_dict),toggleExtensionRequested(version, ext_name, enable_state),configureInstalledExtensionRequested(version, ext_name)toMainWindow.
- Dialogs are typically modal (
dialog.exec()). - They return
QDialog.AcceptedorQDialog.Rejected. - Data is retrieved from the dialog's input fields if accepted.
Reusable UI elements to maintain consistency and encapsulate logic.
- Purpose: Displays a single service in the
ServicesPagelist. - Structure:
QHBoxLayoutcontaining:StatusIndicatorwidget.QVBoxLayoutfor name (QLabel) and detail (QLabelfor version/port).QHBoxLayoutfor action buttons (Start/Stop, Settings, Remove).
- Properties: Stores
service_id(the widget_key:config_idfor instanced services,process_idfor Nginx) andprocess_id_for_pm. - Slots:
update_status(status_str),update_details(detail_str),set_controls_enabled(bool). - Signals:
actionClicked(service_id, action),removeClicked(config_id),settingsClicked(service_id). - Styling: Uses object names for QSS styling (e.g.,
ActionButton,SettingsButton).
- Purpose: Displays a single site in the
SitesPagelist. - Structure:
QHBoxLayoutcontaining:- Favorite button (
QPushButtonwith star icon). - HTTPS shield icon (
QLabelwith pixmap). - Site domain name (
QLabel, clickable to open in browser). - (Previously had path label, settings, and open folder buttons, but these were removed to simplify the list item view. This functionality is now in the main site details pane).
- Favorite button (
update_data(new_site_info): Refreshes the widget's display based on new site data.- Signals:
toggleFavoriteClicked(site_id),domainClicked(domain_str).settingsClickedandopenFolderClickedwere removed along with their buttons.
- A simple
QWidgetthat paints a colored circle (green for running, red for stopped, yellow for unknown/checking). - Has a
set_color(QColor)method and overridespaintEvent().
- Subclass
QWidget. - Define necessary signals for interaction.
- Implement slots to update the widget's appearance or state.
- Ensure clear separation of UI and logic.
Grazr uses a Qt Stylesheet (style.qss) file for a consistent look and feel.
- Loading:
main.pyloadsgrazr/ui/style.qssand applies it to theQApplication. - Selectors: QSS uses selectors similar to CSS to target widgets:
- Type selectors:
QPushButton,QLabel - Object name selectors:
QPushButton#PrimaryButton,QWidget#SidebarArea - Property selectors:
QPushButton[selected="true"] - Class selectors (less common in direct QSS, more for custom widget internal styling logic).
- Type selectors:
- Common Properties:
background-color,color,border,border-radius,padding,margin,font-size,font-weight. - Icons: While QSS can set
imageorbackground-image, icons onQPushButtonare typically set viasetIcon()in Python code for better control and resource management.
Set widget.setObjectName("MyUniqueObjectName") in Python, then style it in QSS:
#MyUniqueObjectName {
background-color: blue;
}
Set dynamic properties on widgets in Python: widget.setProperty("selected", True).
Then style based on the property in QSS:
QPushButton[selected="true"] {
background-color: #cce5ff;
border-left: 3px solid #007bff;
}
Refer to the Registering_Icons_Assets.md document for full details. In summary:
- Icons (SVG preferred, PNG for fallback/tray) are stored in
grazr/assets/icons/. grazr/ui/resources.qrclists these assets with aliases (e.g.,:/icons/my_icon.svg).pyside6-rcc grazr/ui/resources.qrc -o grazr/ui/resources_rc.pycompiles this into a Python module.import grazr.ui.resources_rcinmain.py(ormain_window.py) makes resources available.- Use
QIcon(":/icons/alias.svg")in code.
Qt's signal and slot mechanism is fundamental for communication between objects in Grazr, especially UI components.
- Signals: Defined in a class using
mySignal = Signal(arg_type1, arg_type2, ...). Emitted usingself.mySignal.emit(value1, value2). - Slots: Methods decorated with
@Slot(arg_type1, ...)that can be connected to signals. The argument types in the@Slotdecorator must match the signal's argument types. - Connections:
sender_object.someSignal.connect(receiver_object.someSlot).
- Keep signal payloads minimal and focused.
- Disconnect signals when objects are being destroyed if there's a risk of dangling connections, though Qt's parent-child ownership usually handles this for widgets.
- Use
QTimer.singleShot(0, slot_to_call)to defer execution of a slot to the next event loop iteration, which can resolve some UI update or state issues.
- Direct Connection:
button.clicked.connect(self.on_button_clicked) - Lambda for Extra Args:
button.clicked.connect(lambda: self.handle_action("specific_action", item_id)) - Connection to Worker:
main_window.triggerWorker.connect(worker_instance.doWork)
- Background Task Errors: The
Workercatches exceptions and returnssuccess=Falseand an errormessageviaresultReady.MainWindow.handleWorkerResultlogs this and can display it to the user (e.g., viaQMessageBox.warningorQMessageBox.critical). - UI Input Validation: Dialogs should validate user input before accepting.
- Informative Messages: Use
QMessageBoxfor important errors, warnings, or confirmations. UseMainWindow.log_message()for status updates in the log area. - Disabling Controls: Disable buttons or input fields during long operations (handled by
set_controls_enabled(False)on pages, triggered byMainWindowbefore emitting to worker).
- New Features: Discuss UI design and workflow in an issue before implementation.
- Consistency: Try to follow existing UI patterns and styling.
- Responsiveness: Ensure any new operations that might block are offloaded to the
Worker. - Accessibility: Keep accessibility in mind (e.g., keyboard navigation, sufficient contrast, tooltips).
- Testing: Manually test UI changes thoroughly.
- QSS: If adding new widgets that need specific styling, consider if existing QSS selectors can be used or if new object names/properties are needed.
This guide should help contributors understand and effectively work on Grazr's user interface.