pycontrol-gui 3.0.0b2__tar.gz → 3.0.0b4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/PKG-INFO +2 -2
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/pyproject.toml +2 -2
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/session_controller.py +13 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/experiment_run_view.py +0 -21
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/log_view.py +45 -27
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/run_task_tab.py +1 -16
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/settings_dialog.py +124 -90
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/subject_run_panel.py +13 -17
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/task_status.py +52 -1
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_log_view.py +81 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_session_controller.py +10 -4
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_settings_dialog.py +7 -10
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_task_status_widget.py +27 -1
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/uv.lock +1 -1
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/.gitignore +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/.python-version +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/CONTRIBUTING.md +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/LICENSE.txt +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/README.md +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/docs/addons.md +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/docs/architecture.md +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/docs/changelog.md +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/docs/maintainability.md +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/docs/plot-addons.md +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/docs/task-controls.md +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/docs/task-sharing.md +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/__init__.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/__main__.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/app.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/app_settings.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/bus_adapter.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/error_logging.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/experiment_handlers.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/gui_gc.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/main_window.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/persistent_variables.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/plotting/__init__.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/plotting/base.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/plotting/default_plotter.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/plotting/host.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/plotting/loader.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/plotting/profiling.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/plotting/widgets/__init__.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/plotting/widgets/analog_plot.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/plotting/widgets/events_plot.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/plotting/widgets/states_plot.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/__init__.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/add.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/bar-graph.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/book.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/circle-arrow-up_white.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/close.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/close_white.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/collapse.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/connect.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/delete.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/detach.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/disconnect.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/down.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/edit.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/enable.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/expand.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/filter.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/folder.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/funnel.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/github.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/help.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/info.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/keyboard.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/left.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/logo.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/mount.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/pause.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/play.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/play_white.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/record_white.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/refresh.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/remove.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/right.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/run_white.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/save.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/save_as.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/settings.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/share.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/stop_white.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/trash.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/up.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/warning.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/resources/icons/wrench.svg +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/sandbox_session.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/session_helpers.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/session_runtime.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/setup_maintenance.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/task_variables.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/update_check.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/__init__.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/about_dialog.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/controls_dialog.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/controls_dialog_editor.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/controls_host.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/controls_mount_controller.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/controls_spec.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/error_log_dialog.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/experiment_editor_rows.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/experiment_editor_state.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/experiment_editor_table.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/experiment_run_actions.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/experiment_run_models.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/experiment_run_outcomes.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/experiment_run_plots.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/experiment_run_preflight.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/experiment_run_summary.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/experiment_subject_table.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/experiment_variable_values.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/experiment_variables_table.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/experiments_tab.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/file_tree.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/hwdef_selector.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/keyboard_shortcuts_dialog.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/log_filter_controller.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/sandbox_dialog.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/session_controls_shell.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/session_event_ui.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/session_panel.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/session_shutdown_ui.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/session_task_binding.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/setup_variables_dialog.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/setups_tab.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/setups_table.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/share_dialog.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/style.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/task_selector_widgets.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/welcome_dialog.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/workspace_model.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/workspace_snapshot.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/CLASSIFICATION.md +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/__init__.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/conftest.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/fixtures/controls/LED_controls.json +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/fixtures/controls/LED_controlz.json +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/fixtures/controls/blinker_gui.json +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/fixtures/controls/wheel_controls.json +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/support/__init__.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/support/core_events.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_app.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_bus_adapter.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_controls_dialog.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_controls_editor_round_trip.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_core_import_surface.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_error_log_dialog.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_error_logging.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_experiment_editor_state.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_experiment_run_actions.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_experiment_run_end_to_end.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_experiment_run_plots.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_experiment_run_preflight.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_experiment_run_view.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_experiment_subject_hwdef.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_experiment_variable_values.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_experiments_tab.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_gui_gc.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_main_window_shortcuts.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_model_view_widgets.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_persistent_variables.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_plotting_loader.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_plotting_widgets.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_run_task_tab.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_session_event_ui.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_session_helpers.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_session_runtime.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_setup_maintenance.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_setups_tab.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_share_dialog.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_smoke.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_task_variables.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_update_check.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_welcome_dialog.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_workspace_model.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tests/test_workspace_snapshot.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tools/check_wheels.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tools/run_tests.py +0 -0
- {pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/tools/smoke_installed.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pycontrol-gui
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0b4
|
|
4
4
|
Summary: Qt desktop frontend for pycontrol-core.
|
|
5
5
|
Author: Karpova Lab contributors
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
13
13
|
Classifier: Topic :: Scientific/Engineering
|
|
14
14
|
Requires-Python: >=3.12
|
|
15
15
|
Requires-Dist: packaging>=24
|
|
16
|
-
Requires-Dist: pycontrol-core<3.1,>=3.0.
|
|
16
|
+
Requires-Dist: pycontrol-core<3.1,>=3.0.0b3
|
|
17
17
|
Requires-Dist: pyqtgraph>=0.13.7
|
|
18
18
|
Requires-Dist: pyside6>=6.8
|
|
19
19
|
Provides-Extra: test
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pycontrol-gui"
|
|
7
|
-
version = "3.0.
|
|
7
|
+
version = "3.0.0b4"
|
|
8
8
|
description = "Qt desktop frontend for pycontrol-core."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -20,7 +20,7 @@ classifiers = [
|
|
|
20
20
|
]
|
|
21
21
|
dependencies = [
|
|
22
22
|
"packaging>=24",
|
|
23
|
-
"pycontrol-core>=3.0.
|
|
23
|
+
"pycontrol-core>=3.0.0b3,<3.1",
|
|
24
24
|
"PySide6>=6.8",
|
|
25
25
|
"pyqtgraph>=0.13.7",
|
|
26
26
|
]
|
|
@@ -520,6 +520,7 @@ class SessionController(QObject):
|
|
|
520
520
|
self._pending_end = None
|
|
521
521
|
self._end_poll.stop()
|
|
522
522
|
self._handled_session_end_signature = signature
|
|
523
|
+
self._log_run_error(ended)
|
|
523
524
|
self._finish_active_run(ended)
|
|
524
525
|
self.state_changed.emit(session.state)
|
|
525
526
|
self.run_stopped.emit(ended)
|
|
@@ -641,6 +642,18 @@ class SessionController(QObject):
|
|
|
641
642
|
if session.state in (SessionState.CONNECTED, SessionState.READY):
|
|
642
643
|
self._pending_notes.append((note, 0))
|
|
643
644
|
|
|
645
|
+
def _log_run_error(self, ended: SessionEnded) -> None:
|
|
646
|
+
"""Persist a board traceback or lost connection to the error log, not just the run's log view."""
|
|
647
|
+
if not ended.error_message:
|
|
648
|
+
return
|
|
649
|
+
run = self._active_run
|
|
650
|
+
if run is not None:
|
|
651
|
+
where = f"setup {run.plan.setup_name}, subject {run.plan.subject_id}, port {run.plan.port}"
|
|
652
|
+
else:
|
|
653
|
+
where = f"port {self._connected_port}"
|
|
654
|
+
message = ended.error_message.replace("\r\n", "\n").strip()
|
|
655
|
+
logger.error("Run on %s (task %s) ended with an error:\n%s", where, self._last_task_name, message)
|
|
656
|
+
|
|
644
657
|
def _finish_active_run(self, ended: SessionEnded | None = None) -> None:
|
|
645
658
|
"""Close and package the run in flight, if any.
|
|
646
659
|
|
{pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/experiment_run_view.py
RENAMED
|
@@ -176,7 +176,6 @@ class ExperimentRunView(QWidget):
|
|
|
176
176
|
self._persistent_variables_path: Path | None = None
|
|
177
177
|
self._last_summary_dialog: SummaryVariablesDialog | None = None
|
|
178
178
|
self._log_stretch_item: QLayoutItem | None = None
|
|
179
|
-
self._log_live_updates_enabled = True
|
|
180
179
|
self._experiment_plots_mounted = False
|
|
181
180
|
self._syncing_panel_splitters = False
|
|
182
181
|
self._session_splitter_layout: tuple[str, list[int]] | None = None
|
|
@@ -374,7 +373,6 @@ class ExperimentRunView(QWidget):
|
|
|
374
373
|
self.hardware_test_text.setText(hardware_test_name or "")
|
|
375
374
|
self._fit_header_text_fields()
|
|
376
375
|
self._set_hardware_test_header_visible(hardware_test_name is not None)
|
|
377
|
-
self._set_log_live_updates_enabled(self._log_live_updates_enabled)
|
|
378
376
|
self._hooks = ExperimentHookSequencer(
|
|
379
377
|
self._hook_dispatcher,
|
|
380
378
|
experiment_name=experiment.name,
|
|
@@ -566,10 +564,6 @@ class ExperimentRunView(QWidget):
|
|
|
566
564
|
)
|
|
567
565
|
self.log_filters_button = self._log_filter_controller.button
|
|
568
566
|
self._log_filter_name_checkboxes = self._log_filter_controller.name_checkboxes
|
|
569
|
-
self.log_autoscroll_checkbox = QCheckBox("Auto-scroll")
|
|
570
|
-
self.log_autoscroll_checkbox.setChecked(True)
|
|
571
|
-
self.log_autoscroll_checkbox.setToolTip("When enabled, all experiment logs follow new entries to the bottom.")
|
|
572
|
-
|
|
573
567
|
self.logs_visible_checkbox = QCheckBox("Logs")
|
|
574
568
|
self.controls_visible_checkbox = QCheckBox("Controls")
|
|
575
569
|
self.plots_visible_checkbox = QCheckBox("Plots")
|
|
@@ -607,7 +601,6 @@ class ExperimentRunView(QWidget):
|
|
|
607
601
|
logs_layout.setContentsMargins(12, 8, 12, 10)
|
|
608
602
|
logs_layout.setSpacing(8)
|
|
609
603
|
logs_layout.addWidget(self.log_filters_button)
|
|
610
|
-
logs_layout.addWidget(self.log_autoscroll_checkbox)
|
|
611
604
|
logs_layout.addStretch(1)
|
|
612
605
|
|
|
613
606
|
self.plot_window_group = QGroupBox("Plot window")
|
|
@@ -679,7 +672,6 @@ class ExperimentRunView(QWidget):
|
|
|
679
672
|
self.cancel_experiment_button.clicked.connect(self._cancel_experiment)
|
|
680
673
|
self.startstopclose_all_button.clicked.connect(self._start_stop_close_all)
|
|
681
674
|
self.hook_message.connect(self._log_hook_message)
|
|
682
|
-
self.log_autoscroll_checkbox.toggled.connect(self._on_log_live_updates_toggled)
|
|
683
675
|
for name, checkbox in self._pane_visibility_checkboxes.items():
|
|
684
676
|
checkbox.toggled.connect(lambda visible, name=name: self._set_all_session_panes_visible(name, visible))
|
|
685
677
|
|
|
@@ -1040,24 +1032,11 @@ class ExperimentRunView(QWidget):
|
|
|
1040
1032
|
)
|
|
1041
1033
|
self._apply_panel_height_policy()
|
|
1042
1034
|
|
|
1043
|
-
def _on_log_live_updates_toggled(self, enabled: bool) -> None:
|
|
1044
|
-
self._set_log_live_updates_enabled(enabled)
|
|
1045
|
-
|
|
1046
|
-
def _set_log_live_updates_enabled(self, enabled: bool) -> None:
|
|
1047
|
-
self._log_live_updates_enabled = enabled
|
|
1048
|
-
self.log_autoscroll_checkbox.blockSignals(True)
|
|
1049
|
-
self.log_autoscroll_checkbox.setChecked(enabled)
|
|
1050
|
-
self.log_autoscroll_checkbox.blockSignals(False)
|
|
1051
|
-
for panel in self._panels.values():
|
|
1052
|
-
panel.set_log_live_updates_enabled(enabled)
|
|
1053
|
-
|
|
1054
1035
|
def _apply_log_controls_to_panel(self, panel: SubjectRunPanel) -> None:
|
|
1055
1036
|
self._log_filter_controller.apply_to_log_view(panel.log_view)
|
|
1056
|
-
panel.set_log_live_updates_enabled(self._log_live_updates_enabled)
|
|
1057
1037
|
|
|
1058
1038
|
def _set_log_filter_controls_enabled(self, enabled: bool) -> None:
|
|
1059
1039
|
self._log_filter_controller.set_enabled(enabled)
|
|
1060
|
-
self.log_autoscroll_checkbox.setEnabled(enabled)
|
|
1061
1040
|
self.expand_one_at_a_time_checkbox.setEnabled(enabled)
|
|
1062
1041
|
|
|
1063
1042
|
def _add_log_stretch(self) -> None:
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections import deque
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from contextlib import contextmanager
|
|
6
8
|
from dataclasses import dataclass
|
|
7
9
|
|
|
8
10
|
from PySide6.QtGui import QColor, QPalette, QTextCharFormat, QTextCursor
|
|
@@ -68,7 +70,15 @@ class LogView(QPlainTextEdit):
|
|
|
68
70
|
self._entries: deque[LogEntry] = deque(maxlen=max_entries)
|
|
69
71
|
self._visible_types: set[str] = set(self._FILTERABLE_KINDS)
|
|
70
72
|
self._hidden_names: dict[str, set[str]] = {kind: set() for kind in self._FILTERABLE_KINDS}
|
|
71
|
-
|
|
73
|
+
# Whether the view follows new rows: true while the reader is at the bottom.
|
|
74
|
+
# Only the reader's own scrolling changes it, so a layout change that
|
|
75
|
+
# shrinks the view (the plots and status row appearing at run start) or
|
|
76
|
+
# a deferred scroll-range update cannot leave a following view stranded.
|
|
77
|
+
self._following = True
|
|
78
|
+
self._adjusting_scroll = False
|
|
79
|
+
vbar = self.verticalScrollBar()
|
|
80
|
+
vbar.valueChanged.connect(self._on_scrolled)
|
|
81
|
+
vbar.rangeChanged.connect(self._on_scroll_range_changed)
|
|
72
82
|
|
|
73
83
|
@property
|
|
74
84
|
def entries(self) -> tuple[LogEntry, ...]:
|
|
@@ -119,12 +129,11 @@ class LogView(QPlainTextEdit):
|
|
|
119
129
|
self._hidden_names.setdefault(kind, set()).clear()
|
|
120
130
|
self._rerender_visible_entries()
|
|
121
131
|
|
|
122
|
-
def
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
self.moveCursor(QTextCursor.MoveOperation.End)
|
|
132
|
+
def follow_new_rows(self) -> None:
|
|
133
|
+
"""Jump to the bottom and follow new rows until the reader scrolls up."""
|
|
134
|
+
self._following = True
|
|
135
|
+
vbar = self.verticalScrollBar()
|
|
136
|
+
vbar.setValue(vbar.maximum())
|
|
128
137
|
|
|
129
138
|
def append_line(self, text: str) -> None:
|
|
130
139
|
self.append_entry(LogEntry(timestamp_ms=None, kind="info", content=text))
|
|
@@ -156,22 +165,37 @@ class LogView(QPlainTextEdit):
|
|
|
156
165
|
fmt.setForeground(self._row_color(entry.kind))
|
|
157
166
|
source_fmt = QTextCharFormat()
|
|
158
167
|
source_fmt.setForeground(self._source_color())
|
|
159
|
-
|
|
160
|
-
|
|
168
|
+
# Writing through a separate cursor leaves the reader's selection alone.
|
|
169
|
+
with self._keeping_scroll_position():
|
|
170
|
+
cursor = QTextCursor(self.document())
|
|
161
171
|
cursor.movePosition(QTextCursor.MoveOperation.End)
|
|
162
172
|
self._insert_row(cursor, entry, row, fmt, source_fmt)
|
|
163
|
-
|
|
164
|
-
|
|
173
|
+
|
|
174
|
+
@contextmanager
|
|
175
|
+
def _keeping_scroll_position(self) -> Iterator[None]:
|
|
176
|
+
"""Restore the scroll position after an edit, or stay at the bottom while following."""
|
|
177
|
+
if self._adjusting_scroll: # already inside an outer edit, which restores the position
|
|
178
|
+
yield
|
|
165
179
|
return
|
|
166
180
|
vbar = self.verticalScrollBar()
|
|
167
181
|
hbar = self.horizontalScrollBar()
|
|
168
182
|
vpos = vbar.value()
|
|
169
183
|
hpos = hbar.value()
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
184
|
+
self._adjusting_scroll = True
|
|
185
|
+
try:
|
|
186
|
+
yield
|
|
187
|
+
finally:
|
|
188
|
+
vbar.setValue(vbar.maximum() if self._following else vpos)
|
|
189
|
+
hbar.setValue(hpos)
|
|
190
|
+
self._adjusting_scroll = False
|
|
191
|
+
|
|
192
|
+
def _on_scrolled(self, value: int) -> None:
|
|
193
|
+
if not self._adjusting_scroll:
|
|
194
|
+
self._following = value >= self.verticalScrollBar().maximum()
|
|
195
|
+
|
|
196
|
+
def _on_scroll_range_changed(self, _minimum: int, maximum: int) -> None:
|
|
197
|
+
if self._following and not self._adjusting_scroll:
|
|
198
|
+
self.verticalScrollBar().setValue(maximum)
|
|
175
199
|
|
|
176
200
|
@staticmethod
|
|
177
201
|
def _insert_row(
|
|
@@ -184,17 +208,11 @@ class LogView(QPlainTextEdit):
|
|
|
184
208
|
cursor.insertText("\n", fmt)
|
|
185
209
|
|
|
186
210
|
def _rerender_visible_entries(self) -> None:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
for entry in self._entries:
|
|
193
|
-
if self._is_entry_visible(entry):
|
|
194
|
-
self._append_rendered_entry(entry)
|
|
195
|
-
if not self._live_updates_enabled:
|
|
196
|
-
vbar.setValue(vpos)
|
|
197
|
-
hbar.setValue(hpos)
|
|
211
|
+
with self._keeping_scroll_position():
|
|
212
|
+
super().clear()
|
|
213
|
+
for entry in self._entries:
|
|
214
|
+
if self._is_entry_visible(entry):
|
|
215
|
+
self._append_rendered_entry(entry)
|
|
198
216
|
|
|
199
217
|
def _row_color(self, kind: str) -> QColor:
|
|
200
218
|
text_color = self.palette().color(QPalette.ColorRole.Text)
|
|
@@ -18,7 +18,6 @@ from PySide6.QtCore import QSize, Qt, QTimer, Signal
|
|
|
18
18
|
from PySide6.QtGui import QAction, QColor, QFont, QKeySequence, QPalette, QShortcut
|
|
19
19
|
from PySide6.QtWidgets import (
|
|
20
20
|
QAbstractSpinBox,
|
|
21
|
-
QCheckBox,
|
|
22
21
|
QComboBox,
|
|
23
22
|
QDialog,
|
|
24
23
|
QFileDialog,
|
|
@@ -293,13 +292,9 @@ class RunTaskTab(QWidget):
|
|
|
293
292
|
filters_layout = QHBoxLayout(self.log_filters_bar)
|
|
294
293
|
filters_layout.setContentsMargins(0, 0, 0, 0)
|
|
295
294
|
filters_layout.setSpacing(10)
|
|
296
|
-
self.log_autoscroll_checkbox = QCheckBox("Auto-scroll")
|
|
297
|
-
self.log_autoscroll_checkbox.setChecked(True)
|
|
298
|
-
self.log_autoscroll_checkbox.setToolTip("When enabled, the log view follows new entries to the bottom.")
|
|
299
295
|
self.clear_log_button = QPushButton("Clear log")
|
|
300
296
|
self.clear_log_button.setToolTip("Remove all entries from the log.")
|
|
301
297
|
filters_layout.addWidget(self.log_filters_button)
|
|
302
|
-
filters_layout.addWidget(self.log_autoscroll_checkbox)
|
|
303
298
|
filters_layout.addStretch(1)
|
|
304
299
|
filters_layout.addWidget(self.clear_log_button)
|
|
305
300
|
|
|
@@ -394,7 +389,6 @@ class RunTaskTab(QWidget):
|
|
|
394
389
|
self._upload_task_shortcut = QShortcut(QKeySequence(Qt.Key.Key_U), self)
|
|
395
390
|
self._upload_task_shortcut.setContext(Qt.ShortcutContext.WindowShortcut)
|
|
396
391
|
self._upload_task_shortcut.activated.connect(self._on_upload_task_shortcut)
|
|
397
|
-
self._set_log_live_updates_enabled(True)
|
|
398
392
|
self._set_plot_live_updates_enabled(True)
|
|
399
393
|
|
|
400
394
|
def _wire_ui(self) -> None:
|
|
@@ -414,7 +408,6 @@ class RunTaskTab(QWidget):
|
|
|
414
408
|
self.view_board_files_action.triggered.connect(self.view_board_files_requested.emit)
|
|
415
409
|
self.unmount_controls_action.triggered.connect(self._unmount_controls_dialog)
|
|
416
410
|
self.mount_controls_action.triggered.connect(self._mount_controls_dialog)
|
|
417
|
-
self.log_autoscroll_checkbox.toggled.connect(self._on_log_live_updates_toggled)
|
|
418
411
|
self.clear_log_button.clicked.connect(self.log_view.clear)
|
|
419
412
|
self.plot_live_button.toggled.connect(self._on_plot_live_updates_toggled)
|
|
420
413
|
self.task_plot.plotter_changed.connect(self._refresh_plot_live_button_visibility)
|
|
@@ -585,6 +578,7 @@ class RunTaskTab(QWidget):
|
|
|
585
578
|
self._refresh_controls()
|
|
586
579
|
|
|
587
580
|
def on_session_started(self, ev: SessionStarted) -> None:
|
|
581
|
+
self.log_view.follow_new_rows()
|
|
588
582
|
self.task_plot.run_start()
|
|
589
583
|
if self.task_status.mode == "default":
|
|
590
584
|
# A task handler's own status stands until it publishes the run's first.
|
|
@@ -802,18 +796,9 @@ class RunTaskTab(QWidget):
|
|
|
802
796
|
self._append_log_entry(kind="info", content=self._RUN_STOP_SEPARATOR)
|
|
803
797
|
self._append_log_entry(kind="info", content="")
|
|
804
798
|
|
|
805
|
-
def _on_log_live_updates_toggled(self, enabled: bool) -> None:
|
|
806
|
-
self._set_log_live_updates_enabled(enabled)
|
|
807
|
-
|
|
808
799
|
def _on_plot_live_updates_toggled(self, paused: bool) -> None:
|
|
809
800
|
self._set_plot_live_updates_enabled(not paused)
|
|
810
801
|
|
|
811
|
-
def _set_log_live_updates_enabled(self, enabled: bool) -> None:
|
|
812
|
-
self.log_view.set_live_updates_enabled(enabled)
|
|
813
|
-
self.log_autoscroll_checkbox.blockSignals(True)
|
|
814
|
-
self.log_autoscroll_checkbox.setChecked(enabled)
|
|
815
|
-
self.log_autoscroll_checkbox.blockSignals(False)
|
|
816
|
-
|
|
817
802
|
def _set_plot_live_updates_enabled(self, enabled: bool) -> None:
|
|
818
803
|
self._plot_live_updates_enabled = enabled
|
|
819
804
|
self.plot_live_button.blockSignals(True)
|
{pycontrol_gui-3.0.0b2 → pycontrol_gui-3.0.0b4}/src/pycontrol_gui/widgets/settings_dialog.py
RENAMED
|
@@ -42,6 +42,8 @@ from pycontrol_gui.resources import icon
|
|
|
42
42
|
|
|
43
43
|
logger = logging.getLogger(__name__)
|
|
44
44
|
|
|
45
|
+
_STACK_INDEX_ROLE = Qt.ItemDataRole.UserRole + 1
|
|
46
|
+
|
|
45
47
|
|
|
46
48
|
@dataclass(frozen=True, slots=True)
|
|
47
49
|
class _BoundSettingSpec:
|
|
@@ -53,18 +55,27 @@ class _BoundSettingSpec:
|
|
|
53
55
|
@dataclass(frozen=True, slots=True)
|
|
54
56
|
class _AddonRow:
|
|
55
57
|
group: QGroupBox
|
|
56
|
-
|
|
58
|
+
list_widgets: dict[str, QListWidget]
|
|
57
59
|
stack: QStackedWidget
|
|
58
60
|
go_to_file_button: QPushButton
|
|
59
61
|
|
|
60
62
|
|
|
61
63
|
@dataclass(frozen=True, slots=True)
|
|
62
64
|
class _AddonSettingsError:
|
|
65
|
+
category: str
|
|
63
66
|
label: str
|
|
64
67
|
file_path: Path
|
|
65
68
|
message: str
|
|
66
69
|
|
|
67
70
|
|
|
71
|
+
_AddonGroup = tuple[str, tuple[str, ...], tuple[AddonSettingSpec, ...], Path | None]
|
|
72
|
+
|
|
73
|
+
_PLOTTERS = "Plotters"
|
|
74
|
+
_TASK_HANDLERS = "Task Handlers"
|
|
75
|
+
_EXPERIMENT_HANDLERS = "Experiment Handlers"
|
|
76
|
+
_ADDON_CATEGORIES = (_PLOTTERS, _TASK_HANDLERS, _EXPERIMENT_HANDLERS)
|
|
77
|
+
|
|
78
|
+
|
|
68
79
|
class SettingsDialog(QDialog):
|
|
69
80
|
"""Edit settings stored in the active workspace."""
|
|
70
81
|
|
|
@@ -289,59 +300,109 @@ class SettingsDialog(QDialog):
|
|
|
289
300
|
layout.setColumnStretch(2, 1)
|
|
290
301
|
return group
|
|
291
302
|
|
|
292
|
-
def _build_addon_row(
|
|
293
|
-
self, title: str, groups: tuple[tuple[str, tuple[str, ...], tuple[AddonSettingSpec, ...], Path | None], ...]
|
|
294
|
-
) -> QGroupBox:
|
|
303
|
+
def _build_addon_row(self, title: str, groups: dict[str, list[_AddonGroup]]) -> QGroupBox:
|
|
295
304
|
group = QGroupBox(title)
|
|
296
305
|
layout = QHBoxLayout(group)
|
|
297
306
|
layout.setSpacing(12)
|
|
298
307
|
|
|
299
308
|
list_panel = QWidget()
|
|
309
|
+
list_panel.setMinimumWidth(300)
|
|
310
|
+
list_panel.setMaximumWidth(420)
|
|
300
311
|
list_layout = QVBoxLayout(list_panel)
|
|
301
312
|
list_layout.setContentsMargins(0, 0, 0, 0)
|
|
302
|
-
|
|
303
|
-
list_widget.setMinimumWidth(300)
|
|
304
|
-
list_widget.setMaximumWidth(420)
|
|
305
|
-
list_widget.setMinimumHeight(100)
|
|
306
|
-
list_widget.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
|
|
313
|
+
stack = QStackedWidget()
|
|
307
314
|
go_to_file_button = QPushButton("Go to file")
|
|
308
315
|
go_to_file_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
|
309
|
-
go_to_file_button.
|
|
310
|
-
|
|
316
|
+
go_to_file_button.setEnabled(False)
|
|
317
|
+
list_widgets: dict[str, QListWidget] = {}
|
|
311
318
|
|
|
312
|
-
def
|
|
313
|
-
item
|
|
314
|
-
|
|
319
|
+
def select_item(source: QListWidget, item: QListWidgetItem | None) -> None:
|
|
320
|
+
if item is None:
|
|
321
|
+
return
|
|
322
|
+
for other in list_widgets.values():
|
|
323
|
+
if other is not source:
|
|
324
|
+
other.blockSignals(True)
|
|
325
|
+
other.setCurrentRow(-1)
|
|
326
|
+
other.clearSelection()
|
|
327
|
+
other.blockSignals(False)
|
|
328
|
+
stack.setCurrentIndex(int(item.data(_STACK_INDEX_ROLE)))
|
|
329
|
+
go_to_file_button.setEnabled(bool(item.data(Qt.ItemDataRole.UserRole)))
|
|
330
|
+
|
|
331
|
+
def selected_item() -> QListWidgetItem | None:
|
|
332
|
+
return next(
|
|
333
|
+
(widget.currentItem() for widget in list_widgets.values() if widget.currentItem() is not None), None
|
|
334
|
+
)
|
|
315
335
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
336
|
+
go_to_file_button.clicked.connect(lambda _checked=False: self._reveal_addon_file(selected_item()))
|
|
337
|
+
|
|
338
|
+
first_list: QListWidget | None = None
|
|
339
|
+
for category in _ADDON_CATEGORIES:
|
|
340
|
+
heading = QLabel(category)
|
|
341
|
+
font = heading.font()
|
|
342
|
+
font.setBold(True)
|
|
343
|
+
heading.setFont(font)
|
|
344
|
+
list_widget = QListWidget()
|
|
345
|
+
list_widgets[category] = list_widget
|
|
346
|
+
|
|
347
|
+
for group_title, path, specs, file_path in groups.get(category, []):
|
|
348
|
+
self._add_addon_item(
|
|
349
|
+
list_widget,
|
|
350
|
+
stack,
|
|
351
|
+
self._addon_item_label(path),
|
|
352
|
+
file_path,
|
|
353
|
+
self._build_group(group_title, path, specs, show_title=False),
|
|
354
|
+
)
|
|
322
355
|
for error in self._addon_settings_errors:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
356
|
+
if error.category == category:
|
|
357
|
+
item = self._add_addon_item(
|
|
358
|
+
list_widget,
|
|
359
|
+
stack,
|
|
360
|
+
f"{error.label} (error)",
|
|
361
|
+
error.file_path,
|
|
362
|
+
self._build_addon_error_widget(error),
|
|
363
|
+
)
|
|
364
|
+
item.setToolTip(error.message)
|
|
365
|
+
|
|
366
|
+
if list_widget.count():
|
|
367
|
+
list_widget.currentItemChanged.connect(
|
|
368
|
+
lambda item, _previous, source=list_widget: select_item(source, item)
|
|
369
|
+
)
|
|
370
|
+
first_list = first_list or list_widget
|
|
371
|
+
else:
|
|
372
|
+
placeholder = QListWidgetItem("No configurable files")
|
|
373
|
+
placeholder.setFlags(Qt.ItemFlag.NoItemFlags)
|
|
374
|
+
list_widget.addItem(placeholder)
|
|
375
|
+
list_widget.setFixedHeight(self._list_height(list_widget, min(max(list_widget.count(), 3), 8)))
|
|
376
|
+
|
|
377
|
+
list_layout.addWidget(heading)
|
|
378
|
+
list_layout.addWidget(list_widget)
|
|
379
|
+
|
|
380
|
+
if first_list is None:
|
|
334
381
|
placeholder = QLabel("No settings files found.")
|
|
335
382
|
placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
336
383
|
stack.addWidget(placeholder)
|
|
384
|
+
else:
|
|
385
|
+
first_list.setCurrentRow(0)
|
|
337
386
|
|
|
338
|
-
list_layout.addWidget(list_widget)
|
|
339
387
|
list_layout.addWidget(go_to_file_button)
|
|
340
388
|
layout.addWidget(list_panel)
|
|
341
389
|
layout.addWidget(stack, 1)
|
|
342
|
-
self._addon_rows[title] = _AddonRow(group,
|
|
390
|
+
self._addon_rows[title] = _AddonRow(group, list_widgets, stack, go_to_file_button)
|
|
343
391
|
return group
|
|
344
392
|
|
|
393
|
+
def _add_addon_item(
|
|
394
|
+
self, list_widget: QListWidget, stack: QStackedWidget, label: str, file_path: Path | None, page: QWidget
|
|
395
|
+
) -> QListWidgetItem:
|
|
396
|
+
item = QListWidgetItem(label)
|
|
397
|
+
item.setData(Qt.ItemDataRole.UserRole, "" if file_path is None else str(file_path))
|
|
398
|
+
item.setData(_STACK_INDEX_ROLE, stack.addWidget(page))
|
|
399
|
+
list_widget.addItem(item)
|
|
400
|
+
return item
|
|
401
|
+
|
|
402
|
+
def _list_height(self, list_widget: QListWidget, rows: int) -> int:
|
|
403
|
+
row_height = list_widget.sizeHintForRow(0) if list_widget.count() else self.fontMetrics().height() + 4
|
|
404
|
+
return rows * row_height + 2 * list_widget.frameWidth() + 4
|
|
405
|
+
|
|
345
406
|
def _build_addon_error_widget(self, error: _AddonSettingsError) -> QLabel:
|
|
346
407
|
label = QLabel(
|
|
347
408
|
f"Settings could not be loaded from {error.file_path.name}.\n\n"
|
|
@@ -356,13 +417,10 @@ class SettingsDialog(QDialog):
|
|
|
356
417
|
|
|
357
418
|
def _addon_item_label(self, path: tuple[str, ...]) -> str:
|
|
358
419
|
if path == ("addons", "plotters", "DefaultPlotter"):
|
|
359
|
-
return "pycontrol-gui
|
|
360
|
-
if len(path) >= 3 and path[0] == "addons":
|
|
361
|
-
return f"{path[1]}/{path[2]}"
|
|
420
|
+
return "DefaultPlotter (pycontrol-gui)"
|
|
362
421
|
return path[-1]
|
|
363
422
|
|
|
364
|
-
def
|
|
365
|
-
item = list_widget.currentItem()
|
|
423
|
+
def _reveal_addon_file(self, item: QListWidgetItem | None) -> None:
|
|
366
424
|
if item is None:
|
|
367
425
|
return
|
|
368
426
|
raw_path = item.data(Qt.ItemDataRole.UserRole)
|
|
@@ -405,64 +463,31 @@ class SettingsDialog(QDialog):
|
|
|
405
463
|
self._group_widgets.append(addon_row)
|
|
406
464
|
self.set_locked(self._locked)
|
|
407
465
|
|
|
408
|
-
def _dynamic_addon_groups(
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
"pycontrol-gui / DefaultPlotter",
|
|
415
|
-
("addons", "plotters", "DefaultPlotter"),
|
|
416
|
-
DefaultPlotter.SETTINGS,
|
|
417
|
-
None,
|
|
418
|
-
)
|
|
466
|
+
def _dynamic_addon_groups(self) -> dict[str, list[_AddonGroup]]:
|
|
467
|
+
default_plotter: _AddonGroup = (
|
|
468
|
+
"pycontrol-gui / DefaultPlotter",
|
|
469
|
+
("addons", "plotters", "DefaultPlotter"),
|
|
470
|
+
DefaultPlotter.SETTINGS,
|
|
471
|
+
None,
|
|
419
472
|
)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
return []
|
|
428
|
-
groups: list[tuple[str, tuple[str, ...], tuple[AddonSettingSpec, ...], Path]] = []
|
|
429
|
-
for path in sorted(self._experiment_handlers_dir.glob("*.py")):
|
|
430
|
-
if path.name.startswith("_"):
|
|
431
|
-
continue
|
|
432
|
-
specs = self._settings_specs_from_module(path)
|
|
433
|
-
if specs:
|
|
434
|
-
groups.append(
|
|
435
|
-
(
|
|
436
|
-
f"Experiment Handlers / {path.stem}",
|
|
437
|
-
("addons", "experiment_handlers", path.stem),
|
|
438
|
-
specs,
|
|
439
|
-
path,
|
|
440
|
-
)
|
|
441
|
-
)
|
|
442
|
-
return groups
|
|
443
|
-
|
|
444
|
-
def _plotter_groups(self) -> list[tuple[str, tuple[str, ...], tuple[AddonSettingSpec, ...], Path]]:
|
|
445
|
-
if self._plotters_dir is None or not self._plotters_dir.is_dir():
|
|
446
|
-
return []
|
|
447
|
-
groups: list[tuple[str, tuple[str, ...], tuple[AddonSettingSpec, ...], Path]] = []
|
|
448
|
-
for path in sorted(self._plotters_dir.glob("*.py")):
|
|
449
|
-
if path.name.startswith("_"):
|
|
450
|
-
continue
|
|
451
|
-
specs = self._settings_specs_from_module(path)
|
|
452
|
-
if specs:
|
|
453
|
-
groups.append((f"Task Plotters / {path.stem}", ("addons", "plotters", path.stem), specs, path))
|
|
454
|
-
return groups
|
|
473
|
+
return {
|
|
474
|
+
_PLOTTERS: [default_plotter, *self._addon_file_groups(self._plotters_dir, "plotters", "Task Plotters")],
|
|
475
|
+
_TASK_HANDLERS: self._addon_file_groups(self._task_handlers_dir, "task_handlers", "Task Handlers"),
|
|
476
|
+
_EXPERIMENT_HANDLERS: self._addon_file_groups(
|
|
477
|
+
self._experiment_handlers_dir, "experiment_handlers", "Experiment Handlers"
|
|
478
|
+
),
|
|
479
|
+
}
|
|
455
480
|
|
|
456
|
-
def
|
|
457
|
-
if
|
|
481
|
+
def _addon_file_groups(self, directory: Path | None, key: str, title_prefix: str) -> list[_AddonGroup]:
|
|
482
|
+
if directory is None or not directory.is_dir():
|
|
458
483
|
return []
|
|
459
|
-
groups: list[
|
|
460
|
-
for path in sorted(
|
|
484
|
+
groups: list[_AddonGroup] = []
|
|
485
|
+
for path in sorted(directory.glob("*.py")):
|
|
461
486
|
if path.name.startswith("_"):
|
|
462
487
|
continue
|
|
463
488
|
specs = self._settings_specs_from_module(path)
|
|
464
489
|
if specs:
|
|
465
|
-
groups.append((f"
|
|
490
|
+
groups.append((f"{title_prefix} / {path.stem}", ("addons", key, path.stem), specs, path))
|
|
466
491
|
return groups
|
|
467
492
|
|
|
468
493
|
def _settings_specs_from_module(self, path: Path) -> tuple[AddonSettingSpec, ...]:
|
|
@@ -504,9 +529,18 @@ class SettingsDialog(QDialog):
|
|
|
504
529
|
|
|
505
530
|
def _record_addon_settings_error(self, path: Path, message: str) -> None:
|
|
506
531
|
self._addon_settings_errors.append(
|
|
507
|
-
_AddonSettingsError(
|
|
532
|
+
_AddonSettingsError(
|
|
533
|
+
category=self._addon_category_for_path(path), label=path.stem, file_path=path, message=message
|
|
534
|
+
)
|
|
508
535
|
)
|
|
509
536
|
|
|
537
|
+
def _addon_category_for_path(self, path: Path) -> str:
|
|
538
|
+
if self._plotters_dir is not None and path.parent == self._plotters_dir:
|
|
539
|
+
return _PLOTTERS
|
|
540
|
+
if self._task_handlers_dir is not None and path.parent == self._task_handlers_dir:
|
|
541
|
+
return _TASK_HANDLERS
|
|
542
|
+
return _EXPERIMENT_HANDLERS
|
|
543
|
+
|
|
510
544
|
def _minimum_label_width(self, specs: tuple[AddonSettingSpec, ...]) -> int:
|
|
511
545
|
metrics = self.fontMetrics()
|
|
512
546
|
return max(metrics.horizontalAdvance(spec.title) for spec in specs) + 8
|