1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-23 00:32:42 +02:00

Compare commits

..

1382 Commits

Author SHA1 Message Date
wyzula_j 24b08554f7 build(qt): update qt dependencies to latest 2026-03-17 15:09:16 +01:00
semantic-release 008c3a223a 3.2.3
Automatically generated by python-semantic-release
2026-03-16 15:07:09 +00:00
perl_d b9145d762c fix: check adding parent for filesystemmodel 2026-03-16 16:06:22 +01:00
perl_d 37a5dc2e9e fix: refactor client mock with global fakeredis 2026-03-16 16:06:22 +01:00
wakonig_k 1351fcd47b ci: fix path for uploading logs on failure 2026-03-16 15:49:22 +01:00
semantic-release 14a6b04b11 3.2.2
Automatically generated by python-semantic-release
2026-03-16 14:28:24 +00:00
wyzula_j 4c9d7fddce fix(image): disconnecting of 2d monitor 2026-03-16 15:26:40 +01:00
semantic-release 39ecb89196 3.2.1
Automatically generated by python-semantic-release
2026-03-16 14:08:42 +00:00
wyzula_j 974f25997d fix(e2e): bec shell excluded from e2e testing 2026-03-16 15:07:51 +01:00
wyzula_j e061fa31a9 fix(e2e): bec dock rpc fixed synchronization 2026-03-16 15:07:51 +01:00
wyzula_j 718f99527c fix(e2e): timeout for maybe_remove_dock_area 2026-03-16 15:07:51 +01:00
semantic-release bd5aafc052 3.2.0
Automatically generated by python-semantic-release
2026-03-11 20:52:57 +00:00
wyzula_j b4f6f5aa8b feat(waveform): composite DAP with multiple models 2026-03-11 21:52:10 +01:00
wyzula_j 14d51b8016 feat(curve, waveform): add dap_parameters for lmfit customization in DAP requests 2026-03-11 21:52:10 +01:00
semantic-release e94554b471 3.1.4
Automatically generated by python-semantic-release
2026-03-11 11:58:34 +00:00
wyzula_j 7e0e391888 build: increased minimal version of bec and bec qthemes 2026-03-11 12:57:40 +01:00
wyzula_j 53e5ec42b8 fix(profile_utils): renamed to fetch widgets settings 2026-03-11 12:57:40 +01:00
semantic-release 0e49828a23 3.1.3
Automatically generated by python-semantic-release
2026-03-09 08:46:29 +00:00
wyzula_j 278d8de058 fix(monaco_dock): optimization, removal of QTimer, eventFilter replaced by signal/slot 2026-03-09 09:45:40 +01:00
semantic-release cb4c2beed4 3.1.2
Automatically generated by python-semantic-release
2026-03-06 15:34:15 +00:00
wyzula_j 4382d5c9b1 fix(dock_area): remove old AdvancedDockArea references 2026-03-06 16:33:23 +01:00
wakonig_k 8463b32792 build(deps): update isort requirement 2026-03-06 12:02:48 +01:00
semantic-release 5aff336446 3.1.1
Automatically generated by python-semantic-release
2026-03-06 10:47:17 +00:00
wakonig_k e2daf2e89c build: update min bec dependency to 3.106 2026-03-06 11:46:29 +01:00
wakonig_k ef1233163c test: fix import of bec_lib json extended 2026-03-06 11:46:29 +01:00
wakonig_k 419c01bdd4 fix(positioner box): include username in scan queue request 2026-03-06 11:46:29 +01:00
wyzula_j d4e037f338 refactor(black): black 26 applied 2026-03-06 11:25:37 +01:00
wyzula_j e157f0d7c9 build(deps): upgrade to black 26 2026-03-06 11:25:37 +01:00
semantic-release 2136699806 3.1.0
Automatically generated by python-semantic-release
2026-03-06 10:00:11 +00:00
wakonig_k 75697f5b1f test: adjust metadata assertions to new schema defaults 2026-03-06 10:59:24 +01:00
wakonig_k 2697496515 test: adjust metadata assertions to new schema defaults 2026-03-06 10:59:24 +01:00
wakonig_k 56f16b6352 feat(bec_queue): add tooltip support for user metadata in queue display 2026-03-06 10:59:24 +01:00
wakonig_k ab3efdbd0a feat(scan queue): add scan name to queue 2026-03-06 10:59:24 +01:00
perl_d 0b94ee1485 tests: fix tests to match bec core changes 2026-03-06 10:59:24 +01:00
wakonig_k 72e66cf57f fix(scan metadata): set scan_name to current scan if empty in form data 2026-03-06 10:59:24 +01:00
wakonig_k e6b41b4e92 feat(scan control): wrap metadata form in a group box for better organization 2026-03-06 10:59:24 +01:00
wakonig_k ac824f6b83 feat(StrFormItem): set placeholder text from spec description 2026-03-06 10:59:24 +01:00
wakonig_k 5e34c8a351 fix(forms): use FieldInfo title for label text in _add_griditem method 2026-03-06 10:59:24 +01:00
semantic-release 29ff45a24b 3.0.0
Automatically generated by python-semantic-release
2026-03-06 08:57:30 +00:00
wyzula_j bfc9f19472 fix(main_app): temporarily disable IDE view
Will remain disabled until we have migrated the IDE view to use the procedures
2026-03-06 09:13:29 +01:00
wyzula_j 5bcf440be7 fix(main_app): rpc access refined 2026-03-05 18:12:33 +01:00
wyzula_j 758956be09 feat(generate_cli): RPC API from content widget can be merged with the RPC API of the container widget statically 2026-03-05 18:12:33 +01:00
wyzula_j 010373fd5b feat(becconnector): added rpc_passthrough_children flag in addition to rpc_exposed 2026-03-05 18:12:33 +01:00
wyzula_j 0eabd0f72b fix(rpc_server): add check for rpc_exposed to serialize_object 2026-03-05 18:12:33 +01:00
wyzula_j 963941a788 fix(dock_area): profile management with empty profile, applied across the whole repo 2026-03-05 18:12:33 +01:00
wyzula_j f8be43741a fix(main_window): safeguard of fetching the launcher from the main window if GUIServer is not running 2026-03-05 18:12:33 +01:00
wyzula_j 4d41be61b5 fix(client_utils): safeguard for accessing gui.new and launcher if GUIServer not running 2026-03-05 18:12:33 +01:00
wyzula_j 04b448e183 fix(main_window): scan progress bar rpc not exposed 2026-03-05 18:12:33 +01:00
wyzula_j 654aeb7116 refactor(main_app): simpler id and object name management 2026-03-05 18:12:33 +01:00
wyzula_j 3d049d67a9 fix(view): based on BECWidgets 2026-03-05 18:12:33 +01:00
wyzula_j de6c6284ad feat(becconnector): exposed rpc flag added to the BECConnector 2026-03-05 18:12:33 +01:00
appel_c 2826919c5a refactor: add extra tour steps, add enter button 2026-03-05 18:12:33 +01:00
appel_c a1a400f540 fix: address copilot review 2026-03-05 18:12:33 +01:00
appel_c fcb43066e4 feat: add guided tour docs to device-manager-view 2026-03-05 18:12:33 +01:00
appel_c 83489b7519 refactor(guided-tour): add support for QTableWidgetItem 2026-03-05 18:12:33 +01:00
appel_c 7bcdc31f11 fix(guided-tour): fix skip past invalid step for 'prev' step 2026-03-05 18:12:33 +01:00
wakonig_k 3ffdf11c3e feat: guided tour for main app 2026-03-05 18:12:33 +01:00
wyzula_j 0d05839e9e fix(server): gui server can reach shutdown, logic moved to becconnector 2026-03-05 18:12:33 +01:00
wyzula_j a632f35c40 fix(dock_area): tabbed dock have correct parent 2026-03-05 18:12:33 +01:00
wyzula_j 3a5317be53 fix(dock_area): widget_map and widget_list by default returns only becconnector based widgets 2026-03-05 18:12:32 +01:00
copilot-swe-agent[bot] 041afc68b1 test(widget_io): add dedicated unit tests for iter_widget_tree and helper methods
Co-authored-by: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com>
2026-03-05 18:12:32 +01:00
wyzula_j 047ff2bef7 fix(rpc_server): removed unused get _get_becwidget_ancestor 2026-03-05 18:12:32 +01:00
wyzula_j 5f46fa0994 feat(widget_hierarchy_tree): widget displaying parent child hierarchy from the app widgets 2026-03-05 18:12:32 +01:00
wyzula_j 8b782ac302 feat(widget_highlighter): reusable separate widget highlighter 2026-03-05 18:12:32 +01:00
wyzula_j 00bf01c129 refactor(widget_io): hierarchy logic generalized 2026-03-05 18:12:32 +01:00
dependabot[bot] 4a44ede8fe build(deps): update bec-qthemes requirement
Updates the requirements on [bec-qthemes](https://github.com/bec-project/bec_qthemes) to permit the latest version.
- [Release notes](https://github.com/bec-project/bec_qthemes/releases)
- [Changelog](https://github.com/bec-project/bec_qthemes/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bec-project/bec_qthemes/compare/v0.7.0...v1.3.3)

---
updated-dependencies:
- dependency-name: bec-qthemes
  dependency-version: 1.3.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-05 18:12:32 +01:00
wyzula_j b93fbc5cd3 refactor: global refactoring to use device-signal pair names 2026-03-05 18:12:32 +01:00
wyzula_j c1d4758e4c feat(client_utils): theme can be changed from the CLI 2026-03-05 18:12:32 +01:00
wyzula_j 4fc252220d fix(dark_mode_button): RPC access disabled 2026-03-05 18:12:32 +01:00
wakonig_k 375d131109 fix(toggle): move toggle to theme colors 2026-03-05 18:12:32 +01:00
wyzula_j 5b15c75b88 fix(rpc): rpc flags adjustment for MainApp and DeveloperWidget 2026-03-05 18:12:32 +01:00
wyzula_j b29648e10b fix(cli): RPC API from any folder 2026-03-05 18:12:32 +01:00
wyzula_j 7507f27d68 fix(becconnector): sanitize the setObjectName from qobject inheritance 2026-03-05 18:12:32 +01:00
appel_c b798ea2340 feat(bec-login): Add login widget in material design style 2026-03-05 18:12:32 +01:00
wyzula_j ab9688d2b5 fix(main_app): the dock area view implemented as a viewBase 2026-03-05 18:12:32 +01:00
wyzula_j 92ae5fc7fb fix: removal of old BECDock import 2026-03-05 18:12:32 +01:00
wyzula_j cd9c7ab079 fix(colors): more benevolent fetching of colormap names, avoid hardcoded wrong colormap mapping from GradientWidget from pg 2026-03-05 18:12:32 +01:00
wyzula_j fb55e72713 fix(screen_utils): screen utilities added and fixed sizing for widgets from launch window and main app 2026-03-05 18:12:32 +01:00
wyzula_j 71ed2d353a refactor(dock_area): change name to BECDockArea 2026-03-05 18:12:32 +01:00
appel_c 338ff455cc fix(ophyd-validation): add device_manager_ds argument if available for ophyd validation 2026-03-05 18:12:32 +01:00
wyzula_j 48387c0ad9 fix(editors): VSCode widget removed 2026-03-05 18:12:32 +01:00
perl_d 37298c21c3 ci: cancel previous CI run for PR or branch 2026-03-05 18:12:32 +01:00
wyzula_j 80c0dfa4f2 feat(image): modernization of image widget 2026-03-05 18:12:32 +01:00
wyzula_j 6c73307bb4 fix(device_combobox): public flag for valid input 2026-03-05 18:12:32 +01:00
wyzula_j 940face118 fix(device_input_widgets): removed RPC access 2026-03-05 18:12:32 +01:00
wyzula_j fbddf4a284 feat(device_combobox): device filter added based on its signal classes 2026-03-05 18:12:32 +01:00
wyzula_j cfd6bde268 feat(signal_combobox): extended that can filter by signal class and dimension of the signal 2026-03-05 18:12:32 +01:00
wyzula_j dd69578b91 feat(plot_base): plot_base, image and heatmap widget adopted to property-toolbar sync 2026-03-05 18:12:32 +01:00
wyzula_j 4357d984c8 feat(toolbar): toolbar can be synced with the property_changed for toggle actions 2026-03-05 18:12:32 +01:00
wyzula_j 7cce3bd542 feat(SafeProperty): SafeProperty emits property_changed signal 2026-03-05 18:12:32 +01:00
wakonig_k 3f76ade628 test(e2e): raise with widget name 2026-03-05 18:12:32 +01:00
wakonig_k 84d6653d19 fix(rpc_server): use single shot instead of processEvents to avoid dead locks 2026-03-05 18:12:32 +01:00
wakonig_k 7fd7f67857 fix: adjust ring progress bar to ads 2026-03-05 18:12:32 +01:00
wakonig_k 2992939b0f fix(FakeDevice): add _info dict 2026-03-05 18:12:32 +01:00
wakonig_k a84b924162 feat(color): add relative luminance calculation 2026-03-05 18:12:32 +01:00
wakonig_k 5435fec68a feat: add export and load settings methods to BECConnector; add SafeProperty safe getter flag 2026-03-05 18:12:32 +01:00
wyzula_j 38eb2441cd fix(rpc_register): listing only valid connections 2026-03-05 18:12:32 +01:00
wyzula_j d9b728584f fix(launch_window): logic for showing launcher 2026-03-05 18:12:32 +01:00
wyzula_j 947bf63e03 fix(main_window): parent fixed for notification broker 2026-03-05 18:12:32 +01:00
appel_c 3fe6a00708 fix(device-progress-bar): remove stretch in content layout 2026-03-05 18:12:32 +01:00
perl_d 793779db68 style: wrap progress bar in widget to fix background 2026-03-05 18:12:32 +01:00
perl_d de835e81d8 test: fix test 2026-03-05 18:12:32 +01:00
perl_d fa56fc8802 fix: tooltip logic and disable button on running scan 2026-03-05 18:12:32 +01:00
perl_d c1443fa27a feat: attach config cancellation to closeEvent 2026-03-05 18:12:31 +01:00
perl_d 8bbd519559 fix: remove manual stylesheet deletion/override 2026-03-05 18:12:31 +01:00
perl_d 9c4a54493a fix: 'Any' type annotations 2026-03-05 18:12:31 +01:00
appel_c a9f92cf155 fix(_OverlayEventFilter): fix typo 2026-03-05 18:12:31 +01:00
appel_c 12b4d3a9e0 refactor(device-form-dialog): Use native QDialogButtonBox instead of GroupBox layout 2026-03-05 18:12:31 +01:00
appel_c 229da6244a refactor(busy-loager): Improve eventFilter to avoid crashs if target or overlay is None. 2026-03-05 18:12:31 +01:00
appel_c 4edc57158b test(device-manager-view): improve test coverage for device-manager-view 2026-03-05 18:12:31 +01:00
appel_c 72639e7e5f fix(signal-label): Fix signal label cleanup, missing parent in constructors 2026-03-05 18:12:31 +01:00
appel_c b67f3a14aa test cleanup add mocked client 2026-03-05 18:12:31 +01:00
appel_c fa49322d1f fix(device-manager-display-widget): fix error message popup on cancelling upload 2026-03-05 18:12:31 +01:00
appel_c 24701c2a27 test(config-communicator): add test for cancel action 2026-03-05 18:12:31 +01:00
appel_c caba3a55f3 fix(device-init-progress-bar): fix ui format for device init progressbar 2026-03-05 18:12:31 +01:00
appel_c 94faaba24d fix(busy-loader): adjust busy loader and tests 2026-03-05 18:12:31 +01:00
appel_c 332ca205c1 refactor(busy-loader): refactor busy loader to use custom widget 2026-03-05 18:12:31 +01:00
appel_c 5deafb9797 feat(device-initialization-progress-bar): add progress bar for device initialization 2026-03-05 18:12:31 +01:00
wyzula_j 1f363d9bd4 fix(colors): added logger to the apply theme 2026-03-05 18:12:31 +01:00
wyzula_j c61d00e761 fix(launch_window): processEvents removed 2026-03-05 18:12:31 +01:00
wyzula_j 3236dfb07f fix(advanced_dock_area): removed the singleShot for load_initial_profile 2026-03-05 18:12:31 +01:00
wyzula_j 853f16ad3a fix(view):removed splitter logic 2026-03-05 18:12:31 +01:00
wyzula_j 6cff8d7a41 fix(basic_dock_area): removed the singleShot usage 2026-03-05 18:12:31 +01:00
wyzula_j a56bd572a0 fix(widgets): processEvent removed from widgets using it 2026-03-05 18:12:31 +01:00
wyzula_j e26a90c62f fix: remove singleShots from BECConnector and adjustments of dock area logic 2026-03-05 18:12:31 +01:00
wyzula_j 6113debc6c fix(positioner_box): layout HV centered and size taken from the ui file 2026-03-05 18:12:31 +01:00
wyzula_j 56b1e6687f fix(bec_connector): use RPC register to fetch all connections 2026-03-05 18:12:31 +01:00
wyzula_j 168bb3cb77 feat(motor_map): motor selection adopted to splitter action 2026-03-05 18:12:31 +01:00
wyzula_j 0752f3d6a9 feat(toolbar): splitter action added 2026-03-05 18:12:31 +01:00
wakonig_k 08e19858ea fix(scatter waveform): fix tab order for settings panel 2026-03-05 18:12:31 +01:00
wyzula_j f6712e8bb8 fix(scatter_waveform): remove curve_json from the properties 2026-03-05 18:12:31 +01:00
wyzula_j 66a95102dd fix(signal_combo_box): get_signal_name added; remove duplicates from heatmap and scatter waveform settings; 2026-03-05 18:12:31 +01:00
wyzula_j dea73a97c9 fix(scatter_waveform): modernization of scatter waveform settings dialog 2026-03-05 18:12:31 +01:00
wyzula_j 7ab8e0c2ed fix(scatter_waveform): devices and entries saved as properties 2026-03-05 18:12:31 +01:00
wyzula_j 6baf1962fa fix(heatmap): devices are saved as SafeProperties 2026-03-05 18:12:31 +01:00
wakonig_k 6296055c66 test(script_tree): improve hover event handling with waitUntil 2026-03-05 18:12:31 +01:00
wakonig_k 5e111cfc54 feat(web console): add support for shared web console sessions 2026-03-05 18:12:31 +01:00
appel_c 836fedd50e test(device-manager): use mocked client for tests 2026-03-05 18:12:31 +01:00
appel_c 89d5c5abdb refactor(ophyd-validation): Allow option to keep device visible after successful validation 2026-03-05 18:12:31 +01:00
appel_c 1d654bd8bd fix(device-manager): fix minor icon synchronization bugs 2026-03-05 18:12:31 +01:00
appel_c 7805c7a191 fix(device-manager-display-widget): Remove devices from ophyd validation after upload to BEC 2026-03-05 18:12:31 +01:00
appel_c f827e77e87 test(device-form-dialog): adapt tests 2026-03-05 18:12:31 +01:00
appel_c 36be5292da fix(device-form-dialog): Adapt device-form-dialog ophyd validation test 2026-03-05 18:12:31 +01:00
appel_c 7c28364594 fix(device-form-dialog): Adapt DeviceFormDialog to run validation of config upon editing/adding a config, and forward validation results 2026-03-05 18:12:31 +01:00
wyzula_j b43b6e844b fix(CLI): change the default behavior of launching the profiles in CLI 2026-03-05 18:12:31 +01:00
wyzula_j eda30e3139 refactor(advanced_dock_area): change remove_widget to delete 2026-03-05 18:12:31 +01:00
wyzula_j aba67d3129 fix(advanced_dock_area): empty profile is always empty 2026-03-05 18:12:31 +01:00
wyzula_j 6883982bf6 fix(advanced_dock_area): CLI API adjustments docs + names 2026-03-05 18:12:31 +01:00
wyzula_j 013b916ca3 fix(advanced_dock_area): replace sanitize_namespace with slugify 2026-03-05 18:12:31 +01:00
wyzula_j be55bf20c1 fix(client_utils): delete is deleting window and its content 2026-03-05 18:12:31 +01:00
wyzula_j 9c66dd5991 fix(CLI): dock_area can be created from CLI with specific profile or empty 2026-03-05 18:12:31 +01:00
wyzula_j 8f44213ecc fix(advanced_dock_area): remove widget from dock area by object name 2026-03-05 18:12:31 +01:00
wyzula_j 22df7bb532 fix(advanced_dock_area): profile behaviour adjusted, cleanup of the codebase 2026-03-05 18:12:31 +01:00
wyzula_j beca23e14e fix: sanitize name space util for bec connector and ads 2026-03-05 18:12:31 +01:00
wyzula_j 06745e0511 fix(main_app): dock area from main app shares the workspace name with the CLI one to reuse the profiles created in the cli companion window 2026-03-05 18:12:30 +01:00
wyzula_j 6459281387 fix(launch_window): launch geometry for widgets launched from launcher to 80% of the primary screen as default 2026-03-05 18:12:30 +01:00
wyzula_j 3c16909a87 fix(launch_window): argument to start with the gui class 2026-03-05 18:12:30 +01:00
wyzula_j a6583ad53f fix(dock_area): the old BECDockArea(pg) removed and replaces by AdvancedDockArea(ADS) 2026-03-05 18:12:30 +01:00
wyzula_j 88b6e015bf fix(advanced_dock_area): removed non-functional dock_list and dock_map from RPC 2026-03-05 18:12:30 +01:00
wyzula_j 0d6b94aaec fix(advanced_dock_area): new profiles are saved with quickselect as default 2026-03-05 18:12:30 +01:00
wyzula_j 7d2760eab8 fix(advanced_dock_area): ensure the general profile exists when launched first time 2026-03-05 18:12:30 +01:00
wyzula_j b841cfbc5f fix(advanced_dock_area): remove all widgets when loading new profiles 2026-03-05 18:12:30 +01:00
wyzula_j 6b2b42f21a fix(basic_dock_area): delete_all will also delete floating docks 2026-03-05 18:12:30 +01:00
wyzula_j 522934f8cd fix(main_window): delete on close 2026-03-05 18:12:30 +01:00
wakonig_k 96a52a0cb0 fix(main_app): center the application window on the screen 2026-03-05 18:12:30 +01:00
wakonig_k 7ccfcc9f52 fix(main_app): refactor main function and update script entry point in pyproject.toml 2026-03-05 18:12:30 +01:00
wakonig_k c9a8e64217 ci: use shared issue sync action instead of local version 2026-03-05 18:12:30 +01:00
wakonig_k d085f65153 fix(developer widget): save before executing a scripts 2026-03-05 18:12:30 +01:00
wakonig_k c53d4c0ad7 fix(monaco widget): reset current_file 2026-03-05 18:12:30 +01:00
wakonig_k 3631fc2649 fix(monaco dock): update last focused editor when closing 2026-03-05 18:12:30 +01:00
wakonig_k 31364772bd fix(monaco_dock): update editor metadata handling and improve open_file method 2026-03-05 18:12:30 +01:00
wakonig_k 5e0c376774 refactor(developer_widget): enhance documentation and add missing imports 2026-03-05 18:12:30 +01:00
wakonig_k fa79179f89 feat(developer_widget): add signal connection for focused editor changes to disable run button for macro files 2026-03-05 18:12:30 +01:00
wyzula_j 7083f94f46 fix(client): client API regenerated 2026-03-05 18:12:30 +01:00
wyzula_j a6357af8ff feat(device-manager): Add DeviceManager Widget for BEC Widget main applications 2026-03-05 18:12:30 +01:00
wyzula_j 3ebac55e2d fix(general_app): old general app example removed 2026-03-05 18:12:30 +01:00
wyzula_j 6fc524c819 fix(heatmap): interpolation thread is killed only on exit, logger for dandling thread 2026-03-05 18:12:30 +01:00
wyzula_j f98a5de7e9 perf(heatmap): thread worker optimization 2026-03-05 18:12:30 +01:00
wyzula_j 323c8d5bc0 fix(heatmap): interpolation of the image moved to separate thread 2026-03-05 18:12:30 +01:00
wyzula_j 96060fca53 fix(motor_map): x/y motor are saved in properties 2026-03-05 18:12:30 +01:00
perl_d c1d0e435d5 fix: don't wait forever 2026-03-05 18:12:30 +01:00
wyzula_j 84c7360bb8 fix(widget_state_manager): PROPERTIES_TO_SKIP are not restored even if in ini file 2026-03-05 18:12:30 +01:00
wyzula_j 440cecddf7 feat(advanced_dock_area): floating docks restore with relative geometry 2026-03-05 18:12:30 +01:00
wakonig_k 19b7310433 refactor: improvements to enum access 2026-03-05 18:12:30 +01:00
wyzula_j bcaf013d2b feat(advanced_dock_area): instance lock for multiple ads in same session 2026-03-05 18:12:30 +01:00
wyzula_j b72bf4a0f9 fix(widgets): removed isVisible from all SafeProperties 2026-03-05 18:12:30 +01:00
wyzula_j f38cd3e3a0 fix(bec_widget): improved qt enums; grab safeguard 2026-03-05 18:12:30 +01:00
wyzula_j 2f9d6d59ee fix(qt_ads): pythons stubs match structure of PySide6QtAds 2026-03-05 18:12:30 +01:00
wyzula_j 7ea4352a09 fix(widget_state_manager): filtering of not wanted properties 2026-03-05 18:12:30 +01:00
wyzula_j ac850ec650 refactor(main_app): adapted for DockAreaWidget changes 2026-03-05 18:12:30 +01:00
wyzula_j 4d40918b7c refactor(developer_view): changed to use DockAreaWidget 2026-03-05 18:12:30 +01:00
wyzula_j ed0d34a60f refactor(monaco_dock): changed to use DockAreaWidget 2026-03-05 18:12:30 +01:00
wyzula_j 58b88efcb6 feat(advanced_dock_area): created DockAreaWidget base class; profile management through namespaces; dock area variants 2026-03-05 18:12:30 +01:00
wyzula_j cab422777c fix(main_window): removed general forced cleanup 2026-03-05 18:12:30 +01:00
wyzula_j 7305498475 feat(advanced_dock_area): UI/UX for profile management improved, saving directories logic adjusted 2026-03-05 18:12:30 +01:00
wyzula_j 06cb187d1a fix(main_window): cleanup adjusted with shiboken6 2026-03-05 18:12:30 +01:00
wyzula_j 1c18810e5f fix(dark_mode_button): skip settings added 2026-03-05 18:12:30 +01:00
wyzula_j 338b9e1aa7 fix(widget_state_manager): added shiboken check 2026-03-05 18:12:30 +01:00
wyzula_j ed2651a914 feat(bec_widget): save screenshot to bytes 2026-03-05 18:12:30 +01:00
wyzula_j 0b9e5c15af fix(becconnector): ophyd thread killer on exit + in conftest 2026-03-05 18:12:30 +01:00
wakonig_k 9b753c1f24 feat(guided_tour): add guided tour 2026-03-05 18:12:30 +01:00
wakonig_k 17e678b0ad fix: add metadata to scan control export 2026-03-05 18:12:30 +01:00
wyzula_j bdef594b58 feat(developer_view): add developer view 2026-03-05 18:12:29 +01:00
wyzula_j 66f3e517f0 feat(jupyter_console_window): adjustment for general usage 2026-03-05 18:12:29 +01:00
wakonig_k 4c4fc25a42 feat(ads): add pyi stub file to provide type hints for ads 2026-03-05 18:12:29 +01:00
appel_c 9e4be38c0b feat(dm-view): initial device manager view added 2026-03-05 18:12:29 +01:00
appel_c 5ac629de8c feat(help-inspector): add help inspector widget 2026-03-05 18:12:29 +01:00
wyzula_j 90ba505c10 fix(signal_label): dispatcher unsubscribed in the cleanup 2026-03-05 18:12:29 +01:00
wyzula_j c923f79293 fix(client): abort, reset, stop button removed from RPC access 2026-03-05 18:12:29 +01:00
wyzula_j b30e1e4c5e feat(main_app): main app with interactive app switcher 2026-03-05 18:12:29 +01:00
wyzula_j 9c3a6e1691 feat(actions): actions can be created with label text with beside or under alignment 2026-03-05 18:12:29 +01:00
wyzula_j 92c15a7f82 feat(busy_loader): busy loader added to bec widget base class 2026-03-05 18:12:29 +01:00
wakonig_k 4b5a45c320 feat: add SafeConnect 2026-03-05 18:12:29 +01:00
wyzula_j b0cd619d7d fix(bec_widgets): adapt to bec_qthemes 1.0; themes can be only applied on living Qt objects 2026-03-05 18:12:29 +01:00
wyzula_j d25314e6ee feat(advanced_dock_area): added ads based dock area with profiles 2026-03-05 18:12:29 +01:00
wyzula_j 55c8a57e71 fix(web_console): added startup kwarg 2026-03-05 18:12:29 +01:00
wyzula_j 37bfad7174 refactor(bec_main_window): main app theme renamed to View 2026-03-05 18:12:29 +01:00
wyzula_j 98e29792a2 fix(widget_state_manager): state manager can save all properties recursively to already existing settings 2026-03-05 18:12:29 +01:00
wyzula_j 82dbf31da5 feat(bec_widget): attach/detach method for all widgets + client regenerated 2026-03-05 18:12:29 +01:00
wyzula_j db83576346 feat(widget_io): widget hierarchy can grap all bec connectors from the widget recursively 2026-03-05 18:12:29 +01:00
wyzula_j 389a93f8d0 fix(bec_connector): widget_removed and name_established signals added 2026-03-05 18:12:29 +01:00
wakonig_k b6d70c34df ci: install ttyd 2026-03-05 18:12:29 +01:00
wakonig_k d301fdfeb2 ci: add artifact upload 2026-03-05 18:12:29 +01:00
wyzula_j 562001c08c build!: PySide6-QtAds; bec_qtheme V1; dependencies updated and adjusted 2026-03-05 18:12:28 +01:00
semantic-release ade65dd629 2.45.14
Automatically generated by python-semantic-release
2026-01-23 16:50:26 +00:00
wakonig_k 709ffd6927 fix(bec_status): adjust bec status widget to info and version signature 2026-01-23 17:49:41 +01:00
wakonig_k d577fac02f test(scan control): avoid strict length comparisons 2025-12-23 13:14:28 +01:00
wakonig_k 0349c87261 ci: use auth.token instead of login_or_token 2025-12-23 13:14:28 +01:00
wakonig_k de8fe3b5f5 test(device config): validate against pydantic 2025-12-18 14:02:10 +01:00
semantic-release 2b75d5600a 2.45.13
Automatically generated by python-semantic-release
2025-12-17 13:52:45 +00:00
wakonig_k 01c6e092b9 fix(scan queue): adjustments for changes to the pydantic model of the scan queue 2025-12-17 14:51:54 +01:00
semantic-release ca6f355aac 2.45.12
Automatically generated by python-semantic-release
2025-12-16 12:37:06 +00:00
wyzula_j d876ca72bc fix(heatmap): more robust logic for fast and slow axis in grid scan 2025-12-16 13:36:17 +01:00
wyzula_j e0fd97616d fix(heatmap): flush image if config changes during scan 2025-12-16 13:36:17 +01:00
wyzula_j 6af8a5cbfe fix(heatmap): grid scan image correctly map to scan positions 2025-12-16 13:36:17 +01:00
semantic-release 944e2cedf8 2.45.11
Automatically generated by python-semantic-release
2025-12-15 14:48:20 +00:00
wyzula_j cd11a6cce3 fix(waveform): support for AsyncMultiSignal 2025-12-15 15:47:26 +01:00
semantic-release c98106e594 2.45.10
Automatically generated by python-semantic-release
2025-12-10 11:21:40 +00:00
wakonig_k 04f1ff4fe7 fix(devices): minor fix to comply with new config helper in bec_lib 2025-12-10 12:20:48 +01:00
semantic-release 45ed92494c 2.45.9
Automatically generated by python-semantic-release
2025-12-09 14:21:27 +00:00
wakonig_k 5fc96bd299 fix(rpc): add expiration to GUI registry state updates 2025-12-09 15:20:42 +01:00
semantic-release 1ad5df57fe 2.45.8
Automatically generated by python-semantic-release
2025-12-08 14:36:13 +00:00
wyzula_j 440e778162 fix(notification_banner): backwards compatibility to push messages from Broker to Centre as dict 2025-12-08 15:35:23 +01:00
wyzula_j fdeb8fcb0f fix(notification_banner): formatted error messages fetched directly from BECMessage; do not repreat notifications ids 2025-12-08 15:35:23 +01:00
wyzula_j 5c90983dd4 fix(notification_banner): better contrast in light mode 2025-12-08 15:35:23 +01:00
wyzula_j 4171de1e45 fix(notification_banner): expired messages are hidden in notification center but still accessible 2025-12-08 15:35:23 +01:00
semantic-release f12339e6f9 2.45.7
Automatically generated by python-semantic-release
2025-12-08 14:04:02 +00:00
perl_d ce8e5f0bec fix: handle none in literal combobox 2025-12-08 15:03:17 +01:00
semantic-release 7ea9ab5175 2.45.6
Automatically generated by python-semantic-release
2025-11-27 09:43:46 +00:00
wyzula_j b72f0dc6e8 fix(curve): update dap curves if data are set manually 2025-11-27 10:42:58 +01:00
semantic-release cb9d429884 2.45.5
Automatically generated by python-semantic-release
2025-11-26 13:25:28 +00:00
perl_d 0a80bd0a92 fix: remove ghost widgets in scan metadata 2025-11-26 14:24:36 +01:00
semantic-release 9bc9d355e2 2.45.4
Automatically generated by python-semantic-release
2025-11-24 13:30:46 +00:00
wyzula_j 7d5e702a11 fix(web_links): fixed link to bec widget issues from gitlab to github 2025-11-24 14:29:56 +01:00
wyzula_j 40cbf7fe4f fix(main_window): removed hiding scan progressbar animation 2025-11-24 14:29:56 +01:00
semantic-release 7b287c45f2 2.45.3
Automatically generated by python-semantic-release
2025-11-17 19:27:38 +00:00
wakonig_k c9455672b5 fix(fakeredis): add support for additional args 2025-11-17 20:24:44 +01:00
semantic-release 7f06375f9d 2.45.2
Automatically generated by python-semantic-release
2025-11-17 12:30:21 +00:00
wyzula_j d00d786399 fix(test): removed duplicate test in crosshair 2025-11-17 13:29:35 +01:00
wyzula_j a4c465dcaf build: pyqtgraph pin to 0.13.7 2025-11-17 13:29:35 +01:00
semantic-release d0e94d0da4 2.45.1
Automatically generated by python-semantic-release
2025-11-14 14:13:05 +00:00
wyzula_j bb3cea7fe8 fix(waveform): async_readback can accept 0D data 2025-11-14 15:12:14 +01:00
semantic-release 3c6aa8e138 2.45.0
Automatically generated by python-semantic-release
2025-11-10 19:28:18 +00:00
wyzula_j 198684c65d feat(waveform): dap curve can be attached to custom and history curves 2025-11-10 20:27:31 +01:00
wakonig_k 617f2df2af chore: add third-party license notice 2025-11-10 13:52:22 +01:00
semantic-release ef83287126 2.44.0
Automatically generated by python-semantic-release
2025-11-05 21:43:46 +00:00
wyzula_j d5e6f095fe refactor(plot_base): consolidated user access for the PlotBase 2025-11-05 22:42:57 +01:00
wyzula_j b10efc0f40 feat(plot_base): invert x/y axis 2025-11-05 22:42:57 +01:00
wyzula_j 44b1dbf911 docs: README rewritten 2025-11-03 14:59:57 +01:00
Klaus Wakonig e9d381a18a chore: Update stale issue and PR settings to 120 days 2025-11-03 14:46:03 +01:00
semantic-release b005542df3 2.43.0
Automatically generated by python-semantic-release
2025-10-30 07:58:54 +00:00
wakonig_k 13a9175ba5 feat: add pdf viewer widget 2025-10-30 08:58:11 +01:00
semantic-release 3f8e60a14f 2.42.1
Automatically generated by python-semantic-release
2025-10-28 14:48:23 +00:00
wyzula_j 6bc1c3c5f1 fix(rpc_server): raise window, even if minimized 2025-10-28 15:47:37 +01:00
semantic-release 9f91eb2e08 2.42.0
Automatically generated by python-semantic-release
2025-10-21 13:17:23 +00:00
wyzula_j 1e19092319 feat(positioner_box_2d): added properties to enable/disable vertical and horizontal controls 2025-10-21 15:16:24 +02:00
wyzula_j 96664c3923 feat(image_roi): enhance get_coordinates to include rectangle center and dimensions 2025-10-21 15:16:01 +02:00
semantic-release 741ca2fd8a 2.41.1
Automatically generated by python-semantic-release
2025-10-15 11:25:47 +00:00
wyzula_j 3941050883 fix(dependencies): bec lib versions fixed 2025-10-15 13:25:01 +02:00
semantic-release 1d746c6829 2.41.0
Automatically generated by python-semantic-release
2025-10-15 10:36:45 +00:00
wyzula_j ef27de40ce fix(image_roi): delete button added to compact version 2025-10-15 12:35:51 +02:00
wyzula_j 37df95ead8 fix(image_roi): rois can be removed with right click context menu 2025-10-15 12:35:51 +02:00
wyzula_j c87a6cfce9 feat(image_roi_tree): compact mode added 2025-10-15 12:35:51 +02:00
wakonig_k 3d807eaa63 refactor(serializer): upgrade to new serializer interface 2025-10-13 16:11:47 +02:00
wyzula_j 28ac9c5cc3 build(bec_lib): version bump to 3.69.3 2025-10-09 15:36:18 +02:00
appel_c 1dd20d5986 test(deviceconfig-form-update): Add onFailure default to test 2025-10-09 15:36:18 +02:00
semantic-release 13299aeeb3 2.40.0
Automatically generated by python-semantic-release
2025-10-08 11:41:33 +00:00
wyzula_j d681ba538b fix(waveform): cleanup of scan_history dialog if not closed manually before widget 2025-10-08 13:40:48 +02:00
wyzula_j 2bf489600e fix(waveform): safeguard for _scan_history_closed 2025-10-08 13:40:48 +02:00
wyzula_j 7e88a002b6 fix(waveform): safeguard for if scan_item is a list 2025-10-08 13:40:48 +02:00
wyzula_j 20a59af648 fix(curve_tree): scans are always fetched by scan ids 2025-10-08 13:40:48 +02:00
wyzula_j 540cfc37be fix(waveform): safeguard added to the fetching history data 2025-10-08 13:40:48 +02:00
wyzula_j e59f27a22d fix(waveform): if scan id and scan number is provided, the scan is fetched from the scan id 2025-10-08 13:40:48 +02:00
wyzula_j df8065ea40 fix(curve_tree): safeguard fetching scan numbers from BEC client 2025-10-08 13:40:48 +02:00
wyzula_j 2f3dc2ce6b build(bec_lib): bec_lib dependency raised to 3.68 2025-10-08 13:40:48 +02:00
wyzula_j a006f95f21 test(plotting_framework_e2e): fetching history curve 2025-10-08 13:40:48 +02:00
wyzula_j 8111a4a21b fix(curve_tree): fetching scan numbers directly from the bec client 2025-10-08 13:40:48 +02:00
wyzula_j 962ab774e6 fix(waveform): fetching scan number is not done from list but from .get_by_scan_number 2025-10-08 13:40:48 +02:00
wyzula_j 2f798be7b0 refactor(test_waveform): test waveform renamed 2025-10-08 13:40:48 +02:00
wyzula_j 5a5d32312b test(waveform,curve_tree): test extended to cover history curve behaviour 2025-10-08 13:40:48 +02:00
wyzula_j 0844a9e119 test(conftest): suppress_message_box for error popups fixture autouse True 2025-10-08 13:40:48 +02:00
wyzula_j db7dd4f8d4 fix(waveform): x_data checked with is scalar instead of len() 2025-10-08 13:40:48 +02:00
wyzula_j f083dff612 feat(waveform): new type of curve - history curve 2025-10-08 13:40:48 +02:00
wyzula_j 4be70580a6 refactor(waveform): separate method to fetch scan item from history 2025-10-08 13:40:48 +02:00
wyzula_j d19001c94e fix(waveform): update x suffix label with x property change, do not wait for next update cycle 2025-10-08 13:40:48 +02:00
wakonig_k f25f86522f chore: add dependabot config 2025-10-07 11:12:10 +02:00
semantic-release 948283bc13 2.39.1
Automatically generated by python-semantic-release
2025-10-07 09:00:10 +00:00
wakonig_k 50696bce4c fix: explicitly pass the cached readout flag 2025-10-07 10:59:22 +02:00
semantic-release 1d988a4c57 2.39.0
Automatically generated by python-semantic-release
2025-09-24 16:28:40 +00:00
wyzula_j 565c0bd1e7 feat(rpc_base): windows can be raised to front from CLI 2025-09-24 11:27:47 -05:00
wakonig_k 975404f483 fix(rpc): fix hide/show 2025-09-24 11:27:47 -05:00
semantic-release 165e5e7d84 2.38.4
Automatically generated by python-semantic-release
2025-09-23 15:05:34 +00:00
wakonig_k 108ddae6ca fix(image): add support for specifying preview signals through cli 2025-09-23 17:01:00 +02:00
semantic-release 9737acad58 2.38.3
Automatically generated by python-semantic-release
2025-09-23 14:19:21 +00:00
wakonig_k 65bc5f5421 fix(ringprogressbar): fix client signature 2025-09-23 16:18:33 +02:00
wakonig_k 475ca9f2d8 fix(connector): only flush pending events 2025-09-23 16:18:33 +02:00
wakonig_k bbb5fc6ce1 fix(ringprogressbar): various fixes and improvements 2025-09-23 16:18:33 +02:00
wakonig_k b1b6c5e6a5 test(ringprogressbar): extend e2e test 2025-09-23 16:18:33 +02:00
perl_d 3e339348dd chore: deprecate 3.10, add 3.13 2025-09-15 13:48:32 +02:00
semantic-release 4f075151d5 2.38.2
Automatically generated by python-semantic-release
2025-09-11 15:01:23 +00:00
wyzula_j 0a24ac2c40 fix(waveform):autorange on scan_status 2025-09-11 16:59:35 +02:00
wyzula_j 3a2ec9f1b7 test(crosshair): visibility test added with plotbase fixture 2025-09-11 16:59:35 +02:00
wyzula_j 4dc4ede1d2 fix(plot_base): crosshair items are excluded from visible curves and from auto_range 2025-09-11 16:59:35 +02:00
wyzula_j 556832fd48 fix(waveform): changing curve visibility refresh markers 2025-09-11 16:59:35 +02:00
wyzula_j 72b6f74252 fix(crosshair): ignore fetching data and markers from invisible items 2025-09-11 16:59:35 +02:00
wyzula_j b703b37bbd fix(plot_base): visible items injected into plot item 2025-09-11 16:59:35 +02:00
wakonig_k 18ef35f22a docs: move to autoapi 2025-09-10 15:05:54 +02:00
wakonig_k fe67a4f325 ci: fix stale issues job permissions; add workflow dispatch option 2025-08-31 09:59:16 +02:00
semantic-release f1c3d77a45 2.38.1
Automatically generated by python-semantic-release
2025-08-22 10:06:47 +00:00
perl_d ad7cdc60dd fix: move thefuzz dependency to prod 2025-08-22 12:06:01 +02:00
semantic-release ba047fd776 2.38.0
Automatically generated by python-semantic-release
2025-08-19 15:12:14 +00:00
appel_c 6e05157abb feat(device_manager): DeviceManager view of config session 2025-08-19 17:11:24 +02:00
semantic-release f4bc759e72 2.37.0
Automatically generated by python-semantic-release
2025-08-19 14:52:20 +00:00
wakonig_k 1bec9bd9b2 feat: add explorer widget 2025-08-19 16:51:38 +02:00
semantic-release 8b013d5dce 2.36.0
Automatically generated by python-semantic-release
2025-08-18 10:45:14 +00:00
wakonig_k f2e5a85e61 feat(scan control): add support for literals 2025-08-18 12:44:29 +02:00
semantic-release a2f8880459 2.35.0
Automatically generated by python-semantic-release
2025-08-14 07:16:53 +00:00
wyzula_j 926d722955 feat(property_manager): property manager widget 2025-08-14 09:16:04 +02:00
wyzula_j 44ba7201b4 build: PySide6 upgraded to 6.9.0 2025-08-12 19:56:29 +02:00
semantic-release 0717426db2 2.34.0
Automatically generated by python-semantic-release
2025-08-07 13:39:47 +00:00
perl_d f4af6ebc5f fix: use better source for plugin repo name 2025-08-07 15:39:07 +02:00
perl_d a923f12c97 feat: autoformat compiled file and add docs 2025-08-07 15:39:07 +02:00
perl_d a5a7607a83 tests: add tests for widget creator 2025-08-07 15:39:07 +02:00
perl_d 9de548446b fix: plugin widget import machinery
- lazy import client so plugin widgets can import BECWidgets which use
  it indirectly
- exclude classes originating from bec_widgets core from plugin
  discovery
- better errors
2025-08-07 15:39:07 +02:00
perl_d 49ac7decf7 feat(plugin manager): add cli commands 2025-08-07 15:39:07 +02:00
semantic-release 092bed38fa 2.33.3
Automatically generated by python-semantic-release
2025-07-31 11:10:38 +00:00
appel_c 50c84a766a refactor(scan-history): add spinner for loading time of history 2025-07-31 13:09:47 +02:00
appel_c d22a3317ba refactor: use client callback for scan history reload 2025-07-31 13:09:47 +02:00
appel_c 6df1d0c31f fix(scan-history-view): account for async loading of scan history 2025-07-31 13:09:47 +02:00
appel_c 946752a4b0 refactor(scan-history): fix insert logic; cleanup 2025-07-31 13:09:47 +02:00
appel_c c1f62ad6cb refactor: make ids a set, cleanup 2025-07-31 13:09:47 +02:00
appel_c a5adf3a97d refactor: improve scan history performance on loading full scan lists 2025-07-31 13:09:47 +02:00
semantic-release 76e3e0b60f 2.33.2
Automatically generated by python-semantic-release
2025-07-31 07:27:50 +00:00
perl_d f18eeb9c5d fix: don't warn on empty DeviceEdit init 2025-07-31 09:26:59 +02:00
perl_d 32ce8e2818 fix: remove config, directly set device+signal 2025-07-31 09:26:59 +02:00
perl_d 23413cffab fix: delete choice dialog on close 2025-07-31 09:26:59 +02:00
David Perl 4bbb8fa519 fix: display short lists in SignalDisplay 2025-07-31 09:26:59 +02:00
semantic-release a972369a72 2.33.1
Automatically generated by python-semantic-release
2025-07-31 06:50:30 +00:00
wakonig_k cd81e7f9ba fix(cli): ensure guis are not started twice 2025-07-31 08:49:48 +02:00
semantic-release e2b8118f67 2.33.0
Automatically generated by python-semantic-release
2025-07-29 13:24:20 +00:00
wakonig_k 5f925ba4e3 build: update bec and qtmonaco min dependencies 2025-07-29 15:23:36 +02:00
wakonig_k fc68d2cf2d feat(monaco): add insert, delete and lsp header 2025-07-29 15:23:36 +02:00
wakonig_k 627b49b33a feat(monaco): add vim mode 2025-07-29 15:23:36 +02:00
wakonig_k a51ef04cdf fix(monaco): forward text changed signal 2025-07-29 15:23:36 +02:00
wakonig_k 40f4bce285 test(web console): add tests for the web console 2025-07-29 15:23:36 +02:00
wakonig_k 2b9fe6c959 feat(web console): add signal to indicate when the js backend is initialized 2025-07-29 15:23:36 +02:00
wakonig_k c2e16429c9 feat(web console): add set_readonly method 2025-07-29 15:23:36 +02:00
semantic-release 85ce2aa136 2.32.0
Automatically generated by python-semantic-release
2025-07-29 13:09:07 +00:00
wakonig_k fd5af01842 feat(dock area): add screenshot toolbar action 2025-07-29 15:08:17 +02:00
wakonig_k 8a214c8978 feat(rpc_timeout): add decorator to override the rpc timeout 2025-07-29 15:08:17 +02:00
semantic-release f3214445f2 2.31.3
Automatically generated by python-semantic-release
2025-07-29 12:57:40 +00:00
wyzula_j 6bf84aea25 fix(waveform): fallback mechanism for auto mode to use index if scan_report_devices are not available 2025-07-29 14:56:54 +02:00
semantic-release aace071f11 2.31.2
Automatically generated by python-semantic-release
2025-07-29 12:05:13 +00:00
wakonig_k bf86a030a0 fix(bec widgets): always call cleanup of child widgets on cleanup 2025-07-29 14:04:24 +02:00
semantic-release 358c979bf2 2.31.1
Automatically generated by python-semantic-release
2025-07-29 09:19:55 +00:00
wakonig_k c1bdc506e8 fix(image_base): fix cleanup of uninitialized image layer 2025-07-29 11:19:07 +02:00
semantic-release 4febfb79df 2.31.0
Automatically generated by python-semantic-release
2025-07-29 07:02:55 +00:00
wyzula_j 0854175acb test(launch_window): MainWindow raise test removed, features is supported now 2025-07-29 09:01:01 +02:00
wyzula_j e090ac49b7 fix(launch_window): logic for custom main window apps adjusted 2025-07-29 09:01:01 +02:00
wyzula_j e4521d9528 feat(bec_main_window): plugin and rpc created 2025-07-29 09:01:01 +02:00
wyzula_j 1d0490fff4 fix(bec_main_window): main window have unified status bar on macOS 2025-07-29 09:01:01 +02:00
wyzula_j 10cbb9a05c refactor(widgets): all plugins regenerated 2025-07-29 09:01:01 +02:00
wyzula_j 7073e75adf fix(scan_progressbar): added kwargs to init 2025-07-29 09:01:01 +02:00
wyzula_j e42ffd7c01 fix(color_button_native): removed BECWidget inheritance 2025-07-29 09:01:01 +02:00
wyzula_j 2bd6d00899 fix(decimal_spinbox): removed BECWidget inheritance 2025-07-29 09:01:01 +02:00
wyzula_j c2a918ef4b fix(plugin_utils): plugins can be created from QWidgets, no need for BECWidget base class for plugin creation 2025-07-29 09:01:01 +02:00
wyzula_j 6bbf5126cf fix(widgets): added missing __init__ files 2025-07-29 09:01:01 +02:00
wyzula_j 728d4efd96 fix(utils): plugin template createWidget do not initialise widgets by default 2025-07-29 09:01:01 +02:00
semantic-release 7926969996 2.30.6
Automatically generated by python-semantic-release
2025-07-26 12:44:29 +00:00
wyzula_j 61e5bde15f fix(waveform): autorange is applied with 150ms delay after curve is added 2025-07-26 14:43:51 +02:00
semantic-release c8aa770de3 2.30.5
Automatically generated by python-semantic-release
2025-07-25 17:44:39 +00:00
appel_c 4d5df9608a refactor(positioner-box): cleanup, accept float precision 2025-07-25 19:43:52 +02:00
appel_c b718b438ba fix(positioner-box): Test to fix handling of none integer values for precision 2025-07-25 19:43:52 +02:00
semantic-release 2f978c93c4 2.30.4
Automatically generated by python-semantic-release
2025-07-25 10:18:28 +00:00
wakonig_k b4e0664011 fix(cli): remove stderr from cli output when not using rpc 2025-07-25 12:17:44 +02:00
semantic-release 45fbf4015d 2.30.3
Automatically generated by python-semantic-release
2025-07-23 08:01:36 +00:00
David Perl 0d81bdd4dd fix: cleanup subscriptions in device browser 2025-07-23 10:00:43 +02:00
semantic-release bb4c30ad80 2.30.2
Automatically generated by python-semantic-release
2025-07-23 06:57:35 +00:00
wyzula_j 3fd09fceef test(test_plotting_framework_e2e): added test for waveform with passing device from dev container 2025-07-23 08:56:52 +02:00
perl_d 8eb8225a7f fix: factor out device name function and add test 2025-07-23 08:56:52 +02:00
wyzula_j 491d04467c fix(rpc_base): rpc_call wrapper passes full_name for Devices indeed of name 2025-07-23 08:56:52 +02:00
semantic-release 3bcff75107 2.30.1
Automatically generated by python-semantic-release
2025-07-22 18:19:10 +00:00
perl_d 608590c542 fix: ignore KeyError in SignalLabel 2025-07-22 20:18:28 +02:00
semantic-release 012f7cf970 2.30.0
Automatically generated by python-semantic-release
2025-07-22 14:24:47 +00:00
perl_d cd17a4aad9 fix(signal_label): rewrite reading selection logic 2025-07-22 15:24:03 +01:00
perl_d f0dc992586 fix(signal_label): use read() instead of get() for init 2025-07-22 15:24:03 +01:00
perl_d fd1f9941e0 chore: update client.py 2025-07-22 15:24:03 +01:00
perl_d 3384ca02bd fix(device_browser): display signal for signals 2025-07-22 15:24:03 +01:00
perl_d 959cedbbd5 fix(signal_label): update signal from dialog correctly 2025-07-22 15:24:03 +01:00
perl_d ca4f97503b feat(signal_label): property to display array data or not 2025-07-22 15:24:03 +01:00
perl_d 22beadcad0 fix(signal_label): show all signals by default 2025-07-22 15:24:03 +01:00
perl_d b9af36a4f1 fix(device_signal_display): don't read omitted 2025-07-22 15:24:03 +01:00
semantic-release bdff736aa2 2.29.0
Automatically generated by python-semantic-release
2025-07-22 11:39:06 +00:00
wyzula_j 7cda2ed846 refactor(notification_banner): BECNotificationBroker done as singleton to sync all windows in the session 2025-07-22 13:38:23 +02:00
wyzula_j cd9d22d0b4 feat(notification_banner): notification centre for alarms implemented into BECMainWindow 2025-07-22 13:38:23 +02:00
semantic-release 37b80e16a0 2.28.0
Automatically generated by python-semantic-release
2025-07-21 12:23:48 +00:00
perl_d 7f0098f153 feat: save and load config from devicebrowser 2025-07-21 14:23:01 +02:00
perl_d 8489ef4a69 feat: remove and readd device for config changes 2025-07-21 14:23:01 +02:00
perl_d 13976557fb feat: disable editing while scan active 2025-07-21 14:23:01 +02:00
semantic-release 06ad87ce0a 2.27.1
Automatically generated by python-semantic-release
2025-07-17 13:22:03 +00:00
wyzula_j 00e3713181 fix(image_roi_tree): rois signals are disconnected when roi tree widget is closed 2025-07-17 15:21:11 +02:00
semantic-release 62020f9965 2.27.0
Automatically generated by python-semantic-release
2025-07-17 13:03:53 +00:00
wakonig_k 2373c7e996 feat: add monaco editor 2025-07-17 15:02:01 +02:00
semantic-release 1f3566c105 2.26.0
Automatically generated by python-semantic-release
2025-07-17 12:44:47 +00:00
wakonig_k b8ae7b2e96 fix(config label): reset offset when toggling the label action 2025-07-17 14:44:06 +02:00
wakonig_k 23674ccf59 fix(performance_bundle): fix performance bundle cleanup 2025-07-17 14:44:06 +02:00
wakonig_k 1d8069e391 feat(heatmap): add interpolation and oversampling UI components 2025-07-17 14:44:06 +02:00
wakonig_k 44cc06137c test(history): add history message helper methods to conftest 2025-07-17 14:44:06 +02:00
wakonig_k 46a91784d2 refactor(image_base): cleanup 2025-07-17 14:44:06 +02:00
wakonig_k debd347b64 feat(device combobox): add option to insert an empty element 2025-07-17 14:44:06 +02:00
semantic-release a13c3c44c8 2.25.0
Automatically generated by python-semantic-release
2025-07-17 09:27:51 +00:00
appel_c 25b2737aac refactor: cleanup, add compact popup view for scan_history_browser and update tests 2025-07-17 11:26:57 +02:00
appel_c cf97cc1805 refactor: add additional components for history metadata, device view and popup ui 2025-07-17 11:26:57 +02:00
appel_c 694a6c4960 fix(bec-progressbar): add flag for theme update 2025-07-17 11:26:57 +02:00
wyzula_j 9caae4cf40 feat(scan-history-browser): Add history browser and history metadata viewer 2025-07-17 11:26:57 +02:00
appel_c 2b06e34ecf ci(plugin): add plugin repository test to BW ci 2025-07-15 15:09:53 +02:00
appel_c a9c8995ac0 ci(bec): add child_repos test for bec (unit and e2e tests) 2025-07-15 15:09:53 +02:00
semantic-release 1262c66fd6 2.24.1
Automatically generated by python-semantic-release
2025-07-15 09:24:58 +00:00
perl_d bde523806f fix: update signal label for device_edit changes 2025-07-15 11:24:12 +02:00
semantic-release 16bca25d9c 2.24.0
Automatically generated by python-semantic-release
2025-07-15 08:30:13 +00:00
perl_d 130cc24b35 feat(device_browser): connect update to item refresh 2025-07-15 10:29:31 +02:00
perl_d 8b2d6052e8 fix(device_browser): un-nest exception 2025-07-15 10:29:31 +02:00
perl_d 530797a556 fix: hide validity LED, show message as tooltip 2025-07-15 10:29:31 +02:00
perl_d c660e5141f fix: validate some config data 2025-07-15 10:29:31 +02:00
perl_d 900153bc0b feat(#495): add validation against existing device names 2025-07-15 10:29:31 +02:00
perl_d 8dc72656ef feat(device_browser): device deletion from config 2025-07-15 10:29:31 +02:00
perl_d 170be0c7d3 feat: (#495) add devices through browser 2025-07-15 10:29:31 +02:00
perl_d 1925e6ac7f docs: docstring for config dialog 2025-07-15 10:29:31 +02:00
semantic-release b6cef2d27b 2.23.0
Automatically generated by python-semantic-release
2025-07-11 16:44:57 +00:00
wyzula_j a9fce175b7 feat(widget_finder): widget to fetch any other widget by class from currently running app 2025-07-11 18:44:08 +02:00
wyzula_j 783d042e8c feat(widget_io): utility function to find widget in the app by class 2025-07-11 18:44:08 +02:00
semantic-release 319a4206f2 2.22.2
Automatically generated by python-semantic-release
2025-07-11 12:43:39 +00:00
wyzula_j 76439866c1 fix(plot_base): autorange takes into account only visible curves 2025-07-11 14:42:54 +02:00
semantic-release ca600b057e 2.22.1
Automatically generated by python-semantic-release
2025-07-11 11:57:47 +00:00
wakonig_k 6c494258f8 fix(heatmap): fix pixel size calculation for arbitrary shapes 2025-07-11 13:57:01 +02:00
wakonig_k 63a8da680d fix(crosshair): crosshair mouse_moved can be set manually 2025-07-11 13:57:01 +02:00
semantic-release 0f2bde1a0a 2.22.0
Automatically generated by python-semantic-release
2025-07-10 12:23:05 +00:00
wakonig_k 0c76b0c495 feat: add heatmap widget 2025-07-10 14:22:15 +02:00
wakonig_k e594de3ca3 fix(image): reset crosshair on new scan 2025-07-10 14:22:15 +02:00
wakonig_k adaad4f4d5 fix(crosshair): add slot to reset mouse markers 2025-07-10 14:22:15 +02:00
wakonig_k 39c316d6ea fix(image item): fix processor for nans in images 2025-07-10 14:22:15 +02:00
wakonig_k 3ba0fc4b44 fix(crosshair): fix crosshair support for transformations 2025-07-10 14:22:15 +02:00
wakonig_k a6fc7993a3 fix(image_processor): support for nans in nd arrays 2025-07-10 14:22:15 +02:00
wakonig_k 324a5bd3d9 feat(image_item): add support for qtransform 2025-07-10 14:22:15 +02:00
wakonig_k 8929778f07 fix(image_base): move cbar init to image base 2025-07-10 14:22:15 +02:00
semantic-release 72b5c46912 2.21.4
Automatically generated by python-semantic-release
2025-07-08 09:57:41 +00:00
wyzula_j 244bca4e1e fix(image_roi_tree): changing color dialog from ColorButtonNative is open once 2025-07-08 11:57:00 +02:00
semantic-release c50ace5818 2.21.3
Automatically generated by python-semantic-release
2025-07-03 15:24:12 +00:00
wakonig_k 25f28c47e3 fix(connector): remove safeslot for now 2025-07-03 17:23:26 +02:00
wakonig_k db720e8fa4 refactor(toolbar): split toolbar into components, bundles and connections 2025-07-03 17:23:26 +02:00
semantic-release f10140e0f3 2.21.2
Automatically generated by python-semantic-release
2025-06-30 11:53:00 +00:00
wakonig_k 09c5a443aa fix(waveform): fix waveform categorisation for aborted scans 2025-06-30 13:52:19 +02:00
wakonig_k 3f5ab142a3 test: assert config for equality, not identity 2025-06-29 11:52:14 +02:00
semantic-release 422d06d141 2.21.1
Automatically generated by python-semantic-release
2025-06-29 09:49:32 +00:00
wakonig_k 371bc485d0 fix(sbb monitor): add missing pyproject file 2025-06-29 11:48:47 +02:00
semantic-release 70970ecf00 2.21.0
Automatically generated by python-semantic-release
2025-06-28 17:36:16 +00:00
wakonig_k 3d59c25aa9 feat(sbb monitor): add sbb monitor widget 2025-06-28 19:35:36 +02:00
semantic-release 70a06c5fd1 2.20.1
Automatically generated by python-semantic-release
2025-06-28 14:23:36 +00:00
wakonig_k 7ba8863d6a fix(signal input base): unregister callback to avoid accessing deleted qt objects 2025-06-28 16:22:55 +02:00
semantic-release 00ea8bb6c6 2.20.0
Automatically generated by python-semantic-release
2025-06-26 13:03:28 +00:00
wakonig_k e841468892 refactor(curve settings): move signal logic to SignalCombobox 2025-06-26 15:02:31 +02:00
wyzula_j 48a0e5831f fix(curve_settings): larger minimalWidth for the x device combobox selection 2025-06-26 15:02:31 +02:00
wakonig_k 1e9dd4cd25 test(curve settings): add curve tree elements to the dialog test 2025-06-26 15:02:31 +02:00
wakonig_k d10328cb5c feat(waveform): move x axis selection to a combobox 2025-06-26 15:02:31 +02:00
semantic-release 6b248e93f5 2.19.4
Automatically generated by python-semantic-release
2025-06-26 07:13:15 +00:00
wakonig_k bc3085ab8c fix(curve tree): remove manual interception of the close event; call parent cleanup 2025-06-26 09:12:35 +02:00
wakonig_k 9cba696afd fix(waveform): curve tree elements must clean up signal combobox 2025-06-26 09:12:35 +02:00
semantic-release 881b7a7e9d 2.19.3
Automatically generated by python-semantic-release
2025-06-25 14:53:56 +00:00
wyzula_j 29a26b19f9 fix(scan_control): safeguard against empty history; reversed history to fetch the newest scan 2025-06-25 16:53:10 +02:00
semantic-release cba4d47f76 2.19.2
Automatically generated by python-semantic-release
2025-06-23 14:18:46 +00:00
wyzula_j 9f3dcc3ab3 build: bec_lib 3.44 required 2025-06-23 16:17:59 +02:00
wyzula_j 57f75bd4d5 refactor(scan_control): request_last_executed_scan_parameters logic adjusted 2025-06-23 16:17:59 +02:00
wyzula_j 4456297beb fix(scan_control): scan parameters fetched from the scan_history, fix #707 2025-06-23 16:17:59 +02:00
semantic-release ae26b43fb1 2.19.1
Automatically generated by python-semantic-release
2025-06-23 14:07:09 +00:00
wyzula_j 7484f5160c fix(launch_window): number of remaining connections extended to 4 2025-06-23 16:06:27 +02:00
wyzula_j 6421050116 feat(hover_widget) widget enables to display different widget upon hover; applied to scan progress and client info message in status bar of BECMainWindow 2025-06-23 16:06:27 +02:00
semantic-release 5a137d1219 2.19.0
Automatically generated by python-semantic-release
2025-06-23 12:54:48 +00:00
perl_d d5a40dabc7 fix(ci): extend check for pyside import to tests 2025-06-23 14:54:06 +02:00
perl_d f3da6e959e feat: (#494) add signal display to device browser 2025-06-23 14:54:06 +02:00
perl_d 3a103410e7 feat: (#494) display device signals 2025-06-23 14:54:06 +02:00
perl_d 3378051250 feat: (#494) add tabbed layout for device item 2025-06-23 14:54:06 +02:00
semantic-release 77db658f3d 2.18.0
Automatically generated by python-semantic-release
2025-06-22 17:40:06 +00:00
wakonig_k 6e2f2cea91 refactor(device input): refactor to SafeProperty and SafeSlot 2025-06-22 19:39:19 +02:00
wakonig_k eea5f7ebbd feat(curve settings): add combobox selection for device and signal 2025-06-22 19:39:19 +02:00
wakonig_k a9708f6d8f fix(curve settings): add initial size hint 2025-06-22 19:39:19 +02:00
wakonig_k b51de1a00e feat(signal combobox): add reset_selection slot 2025-06-22 19:39:19 +02:00
wakonig_k 8e8acd672c feat(FilterIO): add support for item data 2025-06-22 19:39:19 +02:00
wakonig_k 4c2c0c5525 feat(device combobox): emit reset event if validation fails 2025-06-22 19:39:19 +02:00
wakonig_k 5a564a5f3f fix: make settings dialog resizable 2025-06-22 19:39:19 +02:00
semantic-release 43ad207aa8 2.17.0
Automatically generated by python-semantic-release
2025-06-22 13:33:32 +00:00
wakonig_k a4274ff8cd build: update min dependency of bec to 3.42.4 2025-06-22 15:32:45 +02:00
wakonig_k b2a46e284d test(scan progress): add test for queue update logic 2025-06-22 15:32:45 +02:00
wyzula_j 9ff170660e feat(main_window): timer to show hide scan progress when it is relevant only 2025-06-22 15:32:45 +02:00
wyzula_j 6c04eac18c test(scan_progress): tests extended 2025-06-22 15:32:45 +02:00
wyzula_j aca6efb567 fix(main_window): labels and sizing of scan progress adopted 2025-06-22 15:32:45 +02:00
wyzula_j 88b42e49e3 fix(scan_progressbar): mapping of bec progress states to the progressbar enums 2025-06-22 15:32:45 +02:00
wyzula_j d3a9e0903a feat(progressbar): state setting and dynamic corner radius 2025-06-22 15:32:45 +02:00
wyzula_j 3bbb8daa24 fix(launch_window): number of remaining connections increase to 2 to include the ScanProgressBar 2025-06-22 15:32:45 +02:00
wyzula_j e8ae9725fa fix(scan_progressbar): cleanup adjusted 2025-06-22 15:32:45 +02:00
wakonig_k 497e394deb feat(main_window): added scan progress bar to BECMainWindow status bar 2025-06-22 15:32:45 +02:00
wyzula_j d5ca7b8433 feat(scan_progressbar): added oneline design for compact applications 2025-06-22 15:32:45 +02:00
wyzula_j b02c870dbf fix(bec_progressbar): layout and sizing adjustments 2025-06-22 15:32:45 +02:00
wakonig_k 92d0ffee65 refactor(progressbar): change slot / property to safeslot / safeproperty 2025-06-22 15:32:45 +02:00
wakonig_k c4b85381a4 feat(scan_progressbar): added progressbar with hooks to scan progress and device progress 2025-06-22 15:32:45 +02:00
wakonig_k a451625a5a feat(progressbar): added padding as designer property 2025-06-22 15:32:45 +02:00
semantic-release 54dd0a9913 2.16.2
Automatically generated by python-semantic-release
2025-06-20 12:26:07 +00:00
wyzula_j 3146d98c57 test(utils): DMMock can fetch get_bec_signals method 2025-06-20 14:25:27 +02:00
wyzula_j a3ffcefe80 fix(waveform): AsyncSignal are handled with the same update mechanism as async readback 2025-06-20 14:25:27 +02:00
semantic-release 1a7052073d 2.16.1
Automatically generated by python-semantic-release
2025-06-20 06:40:07 +00:00
wakonig_k 235aabf307 fix(scatter): fix tab order 2025-06-20 08:39:28 +02:00
semantic-release c1cb69b0e8 2.16.0
Automatically generated by python-semantic-release
2025-06-17 14:33:15 +00:00
perl_d 11131ef14c fix: adjust height of list widget 2025-06-17 15:32:24 +01:00
perl_d 5e4c129af6 fix: parse config on submission and reload after 2025-06-17 15:32:24 +01:00
perl_d 4d8c07cdd1 fix: make website test robust 2025-06-17 15:32:24 +01:00
perl_d 8f4c8e45b3 fix: tidy up form widget formatting 2025-06-17 15:32:24 +01:00
perl_d 5623547e92 fix: reset dict table properly 2025-06-17 15:32:24 +01:00
perl_d be73349c70 feat: add set form item 2025-06-17 15:32:24 +01:00
perl_d 1a350c3b16 fix: put waiting in thread 2025-06-17 15:32:24 +01:00
perl_d 138d4cabbd feat: generate combobox for literal str 2025-06-17 15:32:24 +01:00
perl_d b0d03c0648 refactor: rename field widgets 2025-06-17 15:32:24 +01:00
perl_d a9613a07b0 test: add tests for config dialog 2025-06-17 15:32:24 +01:00
perl_d 886964bb54 feat: allow editing device config from browser 2025-06-17 15:32:24 +01:00
perl_d 7fc85bac7f feat: add a widget to edit lists in forms 2025-06-17 15:32:24 +01:00
perl_d d626caae3d perf: replace wait with waitUntil 2025-06-17 15:32:24 +01:00
perl_d dea2568de3 fix: scale dict widget height 2025-06-17 15:32:24 +01:00
perl_d a55f561971 fix: pass on kwargs from PydanticModelForm 2025-06-17 15:32:24 +01:00
perl_d 9ce31c9833 refactor: move device config form to module 2025-06-17 15:32:24 +01:00
semantic-release 95ce98c622 2.15.1
Automatically generated by python-semantic-release
2025-06-16 15:19:40 +00:00
wyzula_j 187bf493a5 fix(main_window): added expiration timer for scroll label for ClientInfoMessage 2025-06-16 17:18:52 +02:00
wyzula_j 1612933dd9 fix(scroll_label): updating label during scrolling is done imminently, regardless scrolling 2025-06-16 17:18:52 +02:00
semantic-release 8c3d6334f6 2.15.0
Automatically generated by python-semantic-release
2025-06-15 10:39:36 +00:00
wyzula_j 30acc4c236 test(main_window): BECMainWindow tests extended 2025-06-15 12:38:56 +02:00
wyzula_j 0dec78afba feat(main_window): main window can display the messages from the send_client_info as a scrolling horizontal text; closes #700 2025-06-15 12:38:56 +02:00
wyzula_j 57b9a57a63 refactor(main_window): app id is displayed as QLabel instead of message 2025-06-15 12:38:56 +02:00
wyzula_j 644be621f2 fix(main_window): central widget cleanup check to not remove None 2025-06-15 12:38:56 +02:00
semantic-release d07265b86d 2.14.0
Automatically generated by python-semantic-release
2025-06-13 16:21:17 +00:00
wyzula_j f0d48a0508 refactor(image_roi_tree): shape switch logic adjusted to reduce code repetition 2025-06-13 18:20:37 +02:00
wyzula_j af8db0bede feat(image_roi): added EllipticalROI 2025-06-13 18:20:37 +02:00
semantic-release 0ae4b652a4 2.13.2
Automatically generated by python-semantic-release
2025-06-13 16:17:37 +00:00
perl_d 32fd959e67 fix: allow sets in generated form types 2025-06-13 18:16:56 +02:00
semantic-release 73b1886bb8 2.13.1
Automatically generated by python-semantic-release
2025-06-12 12:51:59 +00:00
wyzula_j 9f853b0864 fix(main_window): event filter applied on QEvent.Type.StatusTip; closes #698 2025-06-12 14:51:14 +02:00
semantic-release 18636e723a 2.13.0
Automatically generated by python-semantic-release
2025-06-10 15:18:29 +00:00
wyzula_j 594185dde9 feat(image_roi_tree): lock/unlock rois possible through the ROIPropertyTree 2025-06-10 17:17:31 +02:00
wyzula_j 46d7e3f517 feat(roi): rois can be lock to be not moved by mouse 2025-06-10 17:17:31 +02:00
wyzula_j f9044996f6 fix(roi): removed roi handle adding/removing inconsistencies 2025-06-10 17:17:31 +02:00
semantic-release 03474cf7f7 2.12.4
Automatically generated by python-semantic-release
2025-06-10 14:42:40 +00:00
wyzula_j 9ef418bf55 fix(image_roi): coordinates are emitted correctly when handles are inverted; closes #672 2025-06-10 16:41:59 +02:00
wakonig_k b3ce68070d ci: add stale issue job 2025-06-06 14:48:10 +02:00
semantic-release 784b54af6e 2.12.3
Automatically generated by python-semantic-release
2025-06-05 19:07:20 +00:00
wakonig_k 3740ac8e32 build: update min dependency of bec to 3.38 2025-06-05 21:06:32 +02:00
wakonig_k edfac87868 fix(crosshair): use objectName instead of config for retrieving the monitor name 2025-06-05 21:06:32 +02:00
wyzula_j 271116453d fix(image): preview signals can be used in Image widget; update logic adjusted; closes #683 2025-06-05 21:06:32 +02:00
wyzula_j 12f5233745 fix(device_combobox): tuple entries of preview signals are checked in DeviceComboBoxes just for the relevant device 2025-06-05 21:06:32 +02:00
semantic-release 392ddf9d1a 2.12.2
Automatically generated by python-semantic-release
2025-06-05 13:27:05 +00:00
wyzula_j 85705383e4 fix(waveform): safeguard for history data access, closes #571; removed return values "none" 2025-06-05 15:26:19 +02:00
semantic-release 224863569f 2.12.1
Automatically generated by python-semantic-release
2025-06-05 12:07:35 +00:00
wyzula_j 3e2544e52a fix(crosshair): emitted name from crosshair 2D is objectName of image or its id 2025-06-05 14:04:44 +02:00
semantic-release 4d5daf6557 2.12.0
Automatically generated by python-semantic-release
2025-06-04 19:51:34 +00:00
perl_d 718116afc3 fix: exclude metadata from RPC 2025-06-04 21:50:54 +02:00
perl_d 2dda58f7d2 feat: add clickable label util 2025-06-04 21:50:54 +02:00
perl_d 594912136e fix: grid formatting in TypedForm 2025-06-04 21:50:54 +02:00
perl_d 5188b38c86 feat: (#493) device browser to display config 2025-06-04 21:50:54 +02:00
perl_d a10e6f7820 fix: make generate plugin robust to multiline init
instead of str.find, use multiline regex with whitespace
2025-06-04 21:50:54 +02:00
perl_d e0e26c205b fix(device browser): mocks and utils for tests 2025-06-04 21:50:54 +02:00
perl_d 92d1d6435d feat: (#493) add dict to dynamic form types 2025-06-04 21:50:54 +02:00
perl_d a25c1a8039 feat: (#493) add helpers to dynamic form widgets 2025-06-04 21:50:54 +02:00
semantic-release fed068f857 2.11.0
Automatically generated by python-semantic-release
2025-06-04 12:12:27 +00:00
wakonig_k 7eb2f54e0e fix(image layer): add layer main if it does not exist 2025-06-04 14:11:46 +02:00
wakonig_k 92b89e7275 refactor(image_base): move default color map to image layer 2025-06-04 14:11:46 +02:00
wakonig_k a4f3117941 refactor(image_item): emit object name with removed signal 2025-06-04 14:11:46 +02:00
wakonig_k 3e789ca35b refactor(image_item): removed outdated image item config 2025-06-04 14:11:46 +02:00
wakonig_k 92dade0950 refactor(image_base): renamed layers to layer_manager and added public methods for accessing the layer manager 2025-06-04 14:11:46 +02:00
wakonig_k 4a343b2041 feat(image_layer): add default name for image layers 2025-06-04 14:11:46 +02:00
wakonig_k c2b0c8c433 refactor(image): move image item creation to layer manager 2025-06-04 14:11:46 +02:00
wakonig_k 8a299a8268 refactor(image): disconnect when layer is removed 2025-06-04 14:11:46 +02:00
wakonig_k 99ecf6a18f refactor(image): removed access to image item config 2025-06-04 14:11:46 +02:00
wakonig_k 4c0bd977fc fix(image_item): do not disconnect the monitor from within the image item 2025-06-04 14:11:46 +02:00
wakonig_k 7c47505c5a test: improve error message for widgets that are not properly cleaned up 2025-06-04 14:11:46 +02:00
wakonig_k e211e4d716 fix(image item): propagate remove call to parent class 2025-06-04 14:11:46 +02:00
wakonig_k 10f292def9 refactor(image): introduce image base and image layer; rename vrange to v_range 2025-06-04 14:11:46 +02:00
semantic-release d111ded737 2.10.3
Automatically generated by python-semantic-release
2025-06-04 09:00:59 +00:00
wyzula_j 2d0ed94f3f fix(color_button_native): popup logic to choose color moved to ColorButtonNative 2025-06-04 11:00:21 +02:00
semantic-release f68f072da3 2.10.2
Automatically generated by python-semantic-release
2025-06-03 11:57:23 +00:00
perl_d 1df6c1925b fix: remove unnecessary PySide imports 2025-06-03 13:56:35 +02:00
perl_d 6b939ac34d ci: check for disallowed imports from PySide 2025-06-03 13:56:35 +02:00
semantic-release 6bcf20af07 2.10.1
Automatically generated by python-semantic-release
2025-06-02 18:37:30 +00:00
wyzula_j a64cf0dd87 build: pyte removed from dependencies 2025-06-02 20:36:51 +02:00
wyzula_j cd4e90a79f fix(console): qt console widget deleted 2025-06-02 20:36:51 +02:00
semantic-release 49a96a18d6 2.10.0
Automatically generated by python-semantic-release
2025-06-02 13:51:20 +00:00
wakonig_k 2b4454a291 ci: fix artifact version 2025-06-02 15:50:41 +02:00
wakonig_k d12bd9fe1a ci: add job logs to e2e test 2025-06-02 15:50:41 +02:00
wyzula_j d0c1ac0cf5 feat(waveform): large async dataset warning popup 2025-06-02 15:50:41 +02:00
wyzula_j f90150d1c7 fix(waveform): waveform only update async data when scan is currently running 2025-06-02 15:50:41 +02:00
semantic-release c684b6c230 2.9.2
Automatically generated by python-semantic-release
2025-05-30 13:03:46 +00:00
wyzula_j 91126168b6 fix(log_panel): removed lambda callback method 2025-05-30 15:03:08 +02:00
wakonig_k 7322cd194f fix: move log panel to bec connector and add rate limiter 2025-05-30 15:03:08 +02:00
perl_d d9dc60ee99 fix: logpanel error cycle 2025-05-30 15:03:08 +02:00
semantic-release e4cd4891ad 2.9.1
Automatically generated by python-semantic-release
2025-05-30 11:27:23 +00:00
perl_d 12f8c82eb5 fix: make registry update log message debug level 2025-05-30 13:26:40 +02:00
semantic-release f46ffb14e1 2.9.0
Automatically generated by python-semantic-release
2025-05-30 11:14:35 +00:00
perl_d 2b9919bb34 docs: add usage docs for signal label widget 2025-05-30 13:13:55 +02:00
perl_d 822e7d06ff feat: (#569) add signal label widget
add a widget which shows the current value of a signal from BEC.
configurable with many properties in designer. intended for use mainly
in static GUIs.
2025-05-30 13:13:55 +02:00
perl_d 91195ae0fd fix(DeviceSignalInput): improve robustness
use set for storing filter properties to allow multiple set to true or
false
2025-05-30 13:13:55 +02:00
perl_d a6c5c21afa style: typing in bec_dispatcher 2025-05-30 13:13:55 +02:00
semantic-release ff06954cb7 2.8.4
Automatically generated by python-semantic-release
2025-05-30 11:01:06 +00:00
wyzula_j c8128faf79 fix(crosshair): label decimal precision is dynamically scaled with the plot zoom; API of all affected widgets adjusted; option added to PlotBase; closes #637 2025-05-30 13:00:18 +02:00
semantic-release 6b65a94c81 2.8.3
Automatically generated by python-semantic-release
2025-05-30 09:03:15 +00:00
perl_d bf172b8431 fix: guard plugin repo import in e2e test 2025-05-30 11:02:14 +02:00
perl_d 05329ab50f test(e2e): add tests involving plugin repo 2025-05-28 20:39:51 +02:00
perl_d b225a7cc90 refactor: store modules with widget search 2025-05-28 13:05:28 +02:00
semantic-release 3d8af05688 2.8.2
Automatically generated by python-semantic-release
2025-05-27 14:44:05 +00:00
wyzula_j 0bdd4e86a2 fix(image_roi): rois are invertible by default, fixes resizing bug when adding from ROI manager 2025-05-27 16:43:22 +02:00
semantic-release 104e4e427b 2.8.1
Automatically generated by python-semantic-release
2025-05-27 14:34:15 +00:00
wyzula_j ada0977a1b fix(launch_window): font and tile size fixed across OSs, closes #607 2025-05-27 16:33:36 +02:00
semantic-release 1ea467c5fc 2.8.0
Automatically generated by python-semantic-release
2025-05-26 13:05:43 +00:00
wakonig_k 4f69f5da45 refactor(toolbar): add warning if no parent is provided as it may lead to segfaults 2025-05-26 15:05:06 +02:00
wakonig_k d8547c7a56 fix(ImageProcessing): use target widget as parent 2025-05-26 15:05:06 +02:00
wakonig_k 3484507c75 feat(plot_base): add option to specify units 2025-05-26 15:05:06 +02:00
wakonig_k 8abebb7286 refactor(server): minor cleanup of imports 2025-05-26 15:05:06 +02:00
semantic-release 1d07e88b44 2.7.1
Automatically generated by python-semantic-release
2025-05-26 12:38:59 +00:00
appel_c 1a4eb1db67 fix(signal-combobox): bug fix in signal combobox that crashed upon switching from device to signal input 2025-05-26 14:38:19 +02:00
appel_c f57950c4e3 test(input-widgets): add e2e tests to test widget inputs with demo config of bec. 2025-05-26 14:38:19 +02:00
appel_c a8811c9d91 refactor: add rpc interface to signal_line_edit/combobox; add user access methods 2025-05-26 14:38:19 +02:00
appel_c ec740d31fd fix(signal-line-edit): fix signal_line_edit validity check; closes #610 2025-05-26 14:38:19 +02:00
semantic-release 5c12ab1992 2.7.0
Automatically generated by python-semantic-release
2025-05-26 12:23:35 +00:00
wyzula_j ce88787e88 feat(image): roi plots with crosshair cuts added 2025-05-26 14:22:51 +02:00
wyzula_j e12e9e534d fix(image/image_selecetion): toolbar selection tool size adjusted 2025-05-26 14:22:51 +02:00
wyzula_j 66e9445760 fix(plot_base/mouse_interactions.py): fixed parent 2025-05-26 14:22:51 +02:00
semantic-release 6bf4c53805 2.6.0
Automatically generated by python-semantic-release
2025-05-26 11:14:14 +00:00
wyzula_j a939c3b1c4 feat(image_roi_tree): gui roi manager for image widget 2025-05-26 13:13:31 +02:00
wyzula_j 41b7ca8e64 fix(image_roi): position can be set from rpc 2025-05-26 13:13:31 +02:00
wyzula_j 7a531c17d6 refactor(image_roi): glowing handles for Rectangle roi 2025-05-26 13:13:31 +02:00
wyzula_j a020f2dc7e feat(waveform): LMFitDialog cleanup after close 2025-05-26 13:13:31 +02:00
wakonig_k 53377d26e2 ci: add pr issue sync 2025-05-23 17:27:54 +02:00
perl_d 05489a1c56 chore: migrate issue template to github form syntax 2025-05-22 15:48:10 +02:00
semantic-release 0dfff71e4a 2.5.4
Automatically generated by python-semantic-release
2025-05-22 10:48:11 +00:00
wyzula_j d4def09a4e fix(dock_area): menu to add LogPanel into DockArea is temporary disabled 2025-05-22 12:47:21 +02:00
semantic-release 713653a4a5 2.5.3
Automatically generated by python-semantic-release
2025-05-22 09:59:18 +00:00
wyzula_j bcab66b187 fix(server): SimpleFileLikeFromLogOutputFunc added encoding for stdout 2025-05-22 11:58:30 +02:00
wakonig_k a345253c6e ci: reusable actions for installing bec widgets 2025-05-22 09:10:47 +02:00
semantic-release bdf33a5249 2.5.2
Automatically generated by python-semantic-release
2025-05-22 07:07:24 +00:00
perl_d f8276f0224 fix: update gitignore 2025-05-22 09:06:43 +02:00
wakonig_k 8227c44c33 docs: fix build process for sphinx 2025-05-21 14:21:16 +02:00
semantic-release 83098d930c 2.5.1
Automatically generated by python-semantic-release
2025-05-21 11:14:04 +00:00
wakonig_k a7ae856c8f fix(ui loader): fix loader for widget plugins 2025-05-21 13:13:18 +02:00
Klaus Wakonig 06f43e4883 docs: add kwargs to example 2025-05-21 09:29:24 +02:00
Klaus Wakonig 5ec9697271 docs(developer): fix hello world example 2025-05-21 09:29:24 +02:00
semantic-release 41296b5471 2.5.0
Automatically generated by python-semantic-release
2025-05-20 14:37:27 +00:00
wyzula_j 1d018e863c feat(image_rois): image rois with RPC can be added to Image widget 2025-05-20 16:36:48 +02:00
perl_d 6ee0f5004d ci: try uv for test env setup 2025-05-20 15:05:06 +02:00
semantic-release 40b5081632 2.4.3
Automatically generated by python-semantic-release
2025-05-19 15:25:35 +00:00
wakonig_k f064baae68 fix: twine upload key 2025-05-19 17:24:55 +02:00
semantic-release 58f01fb3a2 2.4.2
Automatically generated by python-semantic-release
2025-05-19 15:04:48 +00:00
wakonig_k 1e344eacb7 fix: push release using GH_token 2025-05-19 17:04:04 +02:00
semantic-release 34002fa51a 2.4.1
Automatically generated by python-semantic-release
2025-05-19 14:34:58 +00:00
wakonig_k a00d510a75 fix: skip actions on new tags 2025-05-19 16:34:19 +02:00
semantic-release 120faf9523 2.4.0
Automatically generated by python-semantic-release
2025-05-19 13:53:08 +00:00
wakonig_k d7bd61f69e ci: use custom semver action 2025-05-19 15:50:24 +02:00
wakonig_k 94bcfff724 ci: add known hosts 2025-05-19 15:10:38 +02:00
wakonig_k a17e7a0d52 ci: add deploy ssh key to release job 2025-05-19 15:02:54 +02:00
wakonig_k 7f67d28887 ci: use ssh key for push 2025-05-19 14:39:01 +02:00
wakonig_k 52d8e4b332 ci: build with ssh key 2025-05-19 14:26:17 +02:00
wakonig_k dea2b44e6a ci: fix job permissions for release 2025-05-19 13:47:25 +02:00
wakonig_k dc70ea6dfb ci: fix missing build dependencies 2025-05-19 13:32:16 +02:00
wakonig_k 133ddda3e3 ci: fix missing build dependencies 2025-05-19 13:16:29 +02:00
wakonig_k 8eee92e5cf ci: add semantic-release job 2025-05-19 12:57:17 +02:00
Klaus Wakonig 85de24aa89 chore: update issue templates 2025-05-17 20:38:32 +02:00
wakonig_k 56b6a0b8c2 feat: add web console 2025-05-17 13:34:21 +02:00
wyzula_j d579d894f0 feat(modular_toolbar): remove action/bundle by id 2025-05-17 09:55:00 +02:00
perl_d d915d2f507 fix: (#612) fix additional MD form
makes sure the form is validated on any changes of the additional
metadata table model so that they are propagated to the scan control
widget even when nothing is entered in the standard form
2025-05-16 14:37:07 +02:00
perl_d 7d7a88669f fix: (#572) signal input base filter
use name attribute rather than value from Kind, to compare with kind_str
2025-05-16 10:50:27 +02:00
wyzula_j a42dcec6d4 fix(entry_validator): device signals retrieved from ._info instead of .describe(), close #570 2025-05-15 15:33:00 +02:00
perl_d 8cf1f09926 ci: exclude test dir from coverage report 2025-05-15 11:35:45 +02:00
perl_d 83b153a14a ci: include lines with >=3 characters in report 2025-05-15 09:52:37 +02:00
wyzula_j aed450ef2c fix(side_panel): side panel can be open without icon; toolbar can be hidden if not needed 2025-05-15 08:20:19 +02:00
wakonig_k e60d0cb5ca ci: add generate-cli test 2025-05-14 23:37:15 +02:00
perl_d 01870f9cda test: coverage report settings 2025-05-14 17:43:39 +02:00
perl_d 483886495d ci: tidy workflow names 2025-05-14 17:43:39 +02:00
perl_d 42502f6eed ci: only run tests if formatter passes 2025-05-14 17:43:39 +02:00
perl_d 59d87e1c2f ci: no cov report with failed tests 2025-05-14 17:43:39 +02:00
Klaus Wakonig 3a5fa3d01a chore: update license 2025-05-14 16:36:03 +02:00
wakonig_k dbb3a1c1fb fix(workflows): update ophyd_devices clone URL to use GitHub 2025-05-14 16:15:06 +02:00
wakonig_k ca8211572f ci(workflows): update git clone URL for BEC repository to use GitHub 2025-05-14 15:41:12 +02:00
perl_d 7584af4e44 ci: don't duplicate push & PR 2025-05-14 15:08:47 +02:00
perl_d 95ef26565b ci: add codecov upload
and remove other coverage solution
2025-05-14 15:08:47 +02:00
wyzula_j abbf7a7f44 fix(device_input): remove unnecessary lowercase conversion for device selection 2025-05-14 11:58:41 +02:00
wakonig_k a301d37c4f ci: coverage 2025-05-13 19:29:11 +02:00
wyzula_j 88a17a566c fix(layout_manager): adding relative widget is shifting whole column to not destroy previous layout 2025-05-13 11:33:16 +02:00
wyzula_j bf3746da0e refactor(color_button_native): color button with OS native dialog separated from the curve tree 2025-05-12 18:24:46 +02:00
wakonig_k e3205d6c97 ci: fix upload to codecov 2025-05-12 18:23:23 +02:00
Klaus Wakonig 507ac10e8d ci: add links to badges
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-12 18:23:23 +02:00
wakonig_k 16e167019f ci: add coverage report 2025-05-12 18:23:23 +02:00
wakonig_k d712944e6b docs: badges extravaganza 2025-05-12 18:23:23 +02:00
wakonig_k d9b60c6cc9 docs: fix license reference 2025-05-12 15:37:10 +02:00
wakonig_k aee83e1a9e docs: add badge for code style, version and license 2025-05-12 15:37:10 +02:00
wakonig_k f5317341bf ci: add ci status badge 2025-05-12 15:37:10 +02:00
wakonig_k 8345dacb26 ci: add github workflows 2025-05-12 13:44:37 +02:00
semantic-release 531d9c621d 2.3.0
Automatically generated by python-semantic-release
2025-05-09 12:36:13 +00:00
wyzula_j dc151cdfe3 feat(bec_connector): ability to change object name during runtime 2025-05-09 14:27:44 +02:00
semantic-release e0dfd56a0d 2.2.0
Automatically generated by python-semantic-release
2025-05-09 09:41:37 +00:00
wakonig_k 1fb680abb4 feat(launcher): add support for launching plugin widget 2025-05-08 17:30:16 +02:00
wyzula_j b9e56c96cb refactor(launch_window): widget tile added 2025-05-08 13:50:01 +02:00
semantic-release dd956f18fe 2.1.3
Automatically generated by python-semantic-release
2025-05-07 14:31:53 +00:00
wakonig_k cf59d31113 fix(bec-dispatcher): fix reference to boundmethods to avoid duplicated subscriptions 2025-05-07 11:08:06 +02:00
semantic-release bc0e277332 2.1.2
Automatically generated by python-semantic-release
2025-05-06 11:09:41 +00:00
appel_c 75a2780fe0 tests(user-interaction-e2e): add module scoped e2e tests with user interaction; closes #508 2025-05-06 11:28:12 +02:00
appel_c a6c479e42e build: remove flush-redis from ci job 2025-05-06 11:28:12 +02:00
appel_c 64a4824054 fix(waveform): Ignore callbacks for on_async_readback from QtSender objects that are already destroyed; closes #497 2025-05-06 11:28:12 +02:00
appel_c 1619446ec9 refactor(bec-status-box): add get_server_state user_access method to BECStatusBox 2025-05-06 11:28:12 +02:00
appel_c 37f002427a refactor(bec-progressbar): add private method for bec_progressbar, udate client file 2025-05-06 11:28:12 +02:00
semantic-release 50cb70dcc6 2.1.1
Automatically generated by python-semantic-release
2025-05-06 08:37:48 +00:00
perl_d 55f7efc4f5 fix: import add operator in client 2025-05-06 10:20:47 +02:00
perl_d be72c9f270 refactor: supply bec designer filename to function 2025-05-06 10:20:47 +02:00
appel_c c8cedc0124 wip 2025-05-06 08:54:36 +02:00
semantic-release 3fdbe4031e 2.1.0
Automatically generated by python-semantic-release
2025-05-05 11:10:40 +00:00
wakonig_k c16b9dce9c test(Dock): add validation for new dock creation with invalid name 2025-05-05 13:01:21 +02:00
wakonig_k 9387275851 feat(SafeSlot): slot parameters can be overridden with kwarg; add option to raise 2025-05-05 13:01:21 +02:00
wakonig_k 94463afdba fix: ensure rpc object do not collide with protected names 2025-05-05 13:01:21 +02:00
wyzula_j 02563b10f3 refactor(colormap_widget): widget is rounded 2025-05-02 16:01:51 +02:00
wakonig_k fff4af2489 ci: install dev dependencies for formatter 2025-05-02 14:12:18 +02:00
wakonig_k 452124b528 chore(formatter): upgrade to black v25 2025-05-02 14:12:18 +02:00
semantic-release 9c84e158ba 2.0.3
Automatically generated by python-semantic-release
2025-05-02 11:52:31 +00:00
wakonig_k 58a0bc7974 fix(image_item): wrong user access name for rotation 2025-05-02 12:23:16 +02:00
wakonig_k 770dbd4b63 fix(generate_cli): apply isort config 2025-05-02 12:23:16 +02:00
wakonig_k d22035f897 ci: add job to test that the generated client is up to date 2025-05-02 11:24:38 +02:00
semantic-release fe21b39b7f 2.0.2
Automatically generated by python-semantic-release
2025-05-01 11:00:33 +00:00
wyzula_j 1b78840fd8 fix(plot_base): no content margin for plot_widget window 2025-05-01 12:02:47 +02:00
semantic-release 46519342b6 2.0.1
Automatically generated by python-semantic-release
2025-04-30 11:52:51 +00:00
wyzula_j 9079ddd727 fix(dock_area): restore state safeguard to not pass none to pyqtgraph restoreState 2025-04-30 13:14:16 +02:00
semantic-release 205745cc72 2.0.0
Automatically generated by python-semantic-release
2025-04-29 17:22:28 +00:00
wyzula_j 717017e69e doc(image): rotation update 2025-04-29 19:06:47 +02:00
wyzula_j a3de1f0a31 refactor(plots): waveform and image rpc api review 2025-04-29 18:37:53 +02:00
wakonig_k 8eef4253b0 feat(slot): add 'verify_sender' argument to SafeSlot for sender verification 2025-04-29 17:49:01 +02:00
wyzula_j 1f2db927f5 fix(scan_control): restore scan parameters always regenerate the arg box, preventing infinite loop 2025-04-29 17:38:56 +02:00
wyzula_j 98f159b25f fix(image): ImageItem remove adjusted to disconnect and remove current displayed image 2025-04-29 16:31:11 +02:00
wyzula_j 061f3481da fix(becconnector): widgets can be flagged as root widget, skipping the BECMainWindow in CLI usage 2025-04-29 16:16:35 +02:00
wyzula_j f35f4c4b29 fix(becconnector,widgets): parent_id is always fetched from the real bec widget parent; all widgets adjusted; hardcoded parent_ids removed 2025-04-29 13:23:09 +02:00
wyzula_j c36852b2ef fix(rpc_server): broadcasted data check 2025-04-29 11:48:35 +02:00
wyzula_j 4eaadd1545 fix(scan_matadata): parent passing 2025-04-29 11:35:10 +02:00
David Perl d04770fe91 refactor: rearrange base of metadata forms for generic use 2025-04-29 11:35:10 +02:00
wakonig_k 23fee22ef8 test: fix tests for launcher close / hide behavior 2025-04-29 10:09:47 +02:00
wakonig_k 6e7920c119 fix(launcher): hide launcher when launcher is closed even though it is not the last widget 2025-04-29 09:43:19 +02:00
wakonig_k e3d0d5566c test: add IPython client GUI object test module with tab completion 2025-04-28 15:38:50 +02:00
wyzula_j e5b532274e refactor(assets): new icon for ui loader 2025-04-28 14:20:42 +02:00
wakonig_k eb0323b989 build(dependencies): update min bec_lib version to 3.29 2025-04-28 08:39:05 +02:00
wakonig_k 60852e228f docs: replaces instances of QtDesigner with BEC Designer for improved clarity 2025-04-27 16:58:40 +02:00
wakonig_k b3dbe922de fix(launch_window): return None when cancelling the ui file launcher 2025-04-27 13:50:43 +02:00
wakonig_k fde912005d fix(cleanup): prevent double cleanup by tracking object destruction state 2025-04-27 13:45:58 +02:00
wakonig_k 5e4965fe1f docs(lmfit): fix links 2025-04-25 20:29:26 +02:00
wakonig_k aff5a51f4c fix(type hints): add future import to prevent sphinx from crashing 2025-04-25 20:29:26 +02:00
wakonig_k b4af2cc77a docs: updated docs for v2 (#531) 2025-04-25 20:29:26 +02:00
perl_d 25bd905cef docs: update docs for v2 2025-04-25 20:08:21 +02:00
appel_c 2f0d213e32 docs(position-indicator): update docs for positioner indicator 2025-04-25 19:41:20 +02:00
appel_c b6695b45d0 docs: update docs for various widgets 2025-04-25 19:41:20 +02:00
appel_c 77f9d42576 fix: unique name for widgets, fix new method for docks; closes #534 2025-04-25 19:41:20 +02:00
perl_d 8cca510fa1 fix(client): import reduce 2025-04-25 16:59:53 +02:00
wakonig_k 06a4954d3d fix(BECGuiClient): add launch_script parameter to dock area creation 2025-04-24 17:39:55 +02:00
appel_c 4acf5befb1 docs: review quick_start 2025-04-24 14:38:07 +02:00
appel_c 99d76236ca test: add tests for name creation of custom curves, and object name handling 2025-04-24 08:49:33 +02:00
appel_c afc818bf7d docs: update quick_start 2025-04-24 08:49:33 +02:00
appel_c 8e846d4499 fix(curve): fix unique names for custom curves 2025-04-24 08:49:33 +02:00
appel_c a1c859c743 docs: remove BECFigure from docs, fix wrong api for docs of plotting widgets 2025-04-24 08:49:33 +02:00
appel_c 75cc45d767 docs: remove BECFigure 2025-04-24 08:49:33 +02:00
appel_c 1d091071e1 fix: bugfix in cleanup of ScatterWaveform ScatterCurve; closes #520 2025-04-24 08:49:33 +02:00
appel_c 8e64b65c2d feat: delete bec_app 2025-04-24 08:49:33 +02:00
appel_c 27ea92d120 feat: deprecated and delete alignment_1d gui 2025-04-24 08:49:33 +02:00
wakonig_k 3ddfeaa49f fix(serialization): add serialization for qpointf 2025-04-23 20:42:54 +02:00
wakonig_k 074bbbc166 fix: change default colormap to plasma 2025-04-23 19:05:54 +02:00
wakonig_k 3709cdc866 fix(bec_connector): improve cleanup handling on deleted parent to prevent errors 2025-04-23 17:45:58 +02:00
wakonig_k 9d6d0b406a refactor(bec_connector): replace pyqtSlot with SafeSlot for consistency 2025-04-23 17:45:58 +02:00
wakonig_k 6318b2d822 fix(designer-plugin-generator): enhance super constructor validation for new style classes 2025-04-23 17:45:58 +02:00
wakonig_k f89e74b199 refactor: add template for debugging the cli generator 2025-04-23 17:45:58 +02:00
wakonig_k 0ac14a74b8 fix: ensure provided dock and dock_area names are valid and defaults are snake_case 2025-04-23 16:22:13 +02:00
appel_c 1910993b2b fix(positioner-indicator): fix property setters for position indicator 2025-04-23 14:00:06 +02:00
appel_c 7c303d0129 fix(ring-progress-bar): fix bug in disconnect slot of rings, enable 'scan' mode as default for init with first ring 2025-04-23 07:30:07 +02:00
appel_c 113938e71a test: fix rpc widgets e2e test 2025-04-22 21:19:37 +02:00
wakonig_k e0f146beeb fix(compact_popup): forward close event 2025-04-22 21:19:37 +02:00
wakonig_k fc1cdc814f fix(bec_connector): call cleanup on widgets if the parent was deleted 2025-04-22 21:19:37 +02:00
wakonig_k a13de45131 fix(rpc): call close on container widget if needed 2025-04-22 21:19:37 +02:00
appel_c 8ff2063bc8 fix: proper cleanup of progressbar 2025-04-22 21:19:37 +02:00
appel_c cdc613b6e7 fix(bec_queue): set parent for toolbar buttons 2025-04-22 21:19:37 +02:00
appel_c 1fc6125369 fix: forward parent to children 2025-04-22 21:19:37 +02:00
appel_c fef07ac8e1 fix: import from qtpy instead of PySide6 2025-04-22 21:19:37 +02:00
appel_c 86647b9b7e fix(rpc-base): deprecate widget_name in favor of object_name; closes #499 2025-04-22 21:19:37 +02:00
appel_c 36dc174bfe test: add function scoped rpc_widgets e2e test; closes #510 2025-04-22 21:19:37 +02:00
appel_c a06f0600c1 fix(dark-mode-button): fix parent passed to QObjects in various classes 2025-04-22 21:19:37 +02:00
appel_c f88dfc8f1b refactor: add pragma no cover to various TYPE_CHECKING 2025-04-22 21:19:37 +02:00
appel_c c70cd9d6e8 fix(moduar-toolbar): fix cleanup of modular toolbar and dock_area 2025-04-22 21:19:37 +02:00
appel_c 8fbd54c3aa fix(website-widget): add super().cleanup() in website widget 2025-04-22 21:19:37 +02:00
appel_c ef4a52cc17 fix: RPC access enabled for certain widgets. 2025-04-22 21:19:37 +02:00
appel_c b460ea9955 fix(progress-ring-bar): fix parent inheritance and cleanup of ring objects; closes #496 2025-04-22 21:19:37 +02:00
wakonig_k 1fe052e9da docs: grammar improvement 2025-04-22 15:22:18 +02:00
wakonig_k f2d5b57e86 fix(docs): update copyright year to be dynamic 2025-04-22 15:22:18 +02:00
wakonig_k 6630ba1c42 docs(auto_updates): update documentation for auto updates functionality and add launcher image 2025-04-22 15:22:18 +02:00
perl_d ef148317de fix: wrap fetching plugin widgets in case of errors 2025-04-15 20:13:11 +02:00
wakonig_k e10f5ec088 test(launch_window): tests for default and plugin auto updates 2025-04-15 12:26:09 +02:00
wakonig_k 33a8a767f3 test(launch_window): add test for launching UI file that raises ValueError for QMainWindow 2025-04-15 12:08:06 +02:00
wakonig_k 8efa93d2d2 feat(launch_window): add user access permissions 2025-04-15 12:07:54 +02:00
wakonig_k 29653239c5 feat(launch_window): enhance auto update functionality with selector and dynamic loading 2025-04-15 11:44:26 +02:00
wakonig_k 778230b5ed feat(auto_updates): enforce rpc widget class for subclasses of auto updates 2025-04-15 11:41:03 +02:00
wakonig_k b7795b4d0a refactor(client_utils): remove unused auto update attributes from BECGuiClient 2025-04-15 11:40:22 +02:00
wakonig_k c434af9b92 feat(plugin_utils): add functionality to retrieve auto update classes from plugins 2025-04-15 11:40:04 +02:00
wakonig_k be722683a7 fix(main_window): show app id only when connected to redis 2025-04-15 09:10:35 +02:00
wakonig_k 9a940bb8d5 refactor(launch_window): remove cleanup method 2025-04-15 08:59:17 +02:00
wakonig_k a6ce312f7c refactor(ui_loader): remove unused import 2025-04-15 08:58:59 +02:00
wakonig_k d5e422c7fc test(launch_window): add unit tests for LaunchWindow initialization and custom UI file launching 2025-04-15 08:58:14 +02:00
wakonig_k 3cd6e05b24 fix(launch_window): update LaunchTile icon to use new UI loader tile image 2025-04-14 21:56:27 +02:00
wakonig_k 3089ca15ec feat(launch_window): add custom UI file launching functionality and UI tile 2025-04-14 21:42:22 +02:00
wakonig_k d60cf6c843 refactor(ui_loader): remove unnecessary parent_id handling 2025-04-14 21:41:54 +02:00
wakonig_k 45cd82e635 feat(ui_launch_window): add UILaunchWindow class 2025-04-14 21:40:46 +02:00
wakonig_k f653fc5f7e feat(positioner_box): add units QLabel to device UI components and update visibility logic 2025-04-14 13:33:11 +02:00
wakonig_k d6fccd10f5 fix(rpc_server): update _serialize_bec_connector to include wait parameter for registration check 2025-04-14 10:26:31 +02:00
wakonig_k 064343acf2 fix(bec_connector): add setObjectName method to update object name and broadcast if registered; closes #472 2025-04-14 10:26:31 +02:00
wakonig_k 82b82659b7 fix(rpc_register): change add_rpc parameter type to BECConnector and add object_is_registered method 2025-04-14 10:26:31 +02:00
wakonig_k 1921444e15 fix(bec_connector): add assertion to ensure BECConnector is used with a QObject; closes #475 2025-04-14 10:26:31 +02:00
wakonig_k 3b16c9f5a2 fix(bec_connector): move RPC registration into single shot method to ensure the rpc name is in sync 2025-04-14 10:26:31 +02:00
wakonig_k 4381fcc4c2 fix(designer): avoid touching deleted widgets during init as QtDesigner will segfault 2025-04-14 10:26:31 +02:00
wakonig_k e4e9febc98 fix(ring_progress_bar): replaced hard-coded endpoints by MessageEndpoints 2025-04-14 10:16:47 +02:00
wakonig_k ac9224e5f2 refactor(auto_updates): move cleanup method from user section to internal section 2025-04-14 10:04:43 +02:00
wakonig_k 18e4ba6cfe fix(auto_updates): fix condition to skip auto update 2025-04-14 10:04:43 +02:00
wakonig_k cfc8272ac2 docs: add missing class doc strings for rpc-enabled widgets 2025-04-12 21:14:01 +02:00
perl_d d2c90757c2 docs: better document logpanel code 2025-04-11 18:27:28 +02:00
perl_d 1d7b423bb3 fix: warning in logpanel
- chain a signal to the child BecLogsQueue rather than passing the
signal instance in
2025-04-11 18:27:28 +02:00
wakonig_k cb91ebc0c3 refactor(rpc_server): add type hint for _get_becwidget_ancestor method parameter; minor cleanup of imports 2025-04-11 13:39:26 +02:00
wakonig_k 08168f28d3 refactor(rpc_server): add type hints and docstrings for heartbeat and registry update methods 2025-04-11 13:37:42 +02:00
wakonig_k 125afc8907 fix(rpc_server): enhance serialization logic for BECConnector objects and fix return types 2025-04-11 13:34:05 +02:00
wakonig_k 4dc59aa5e9 fix(rpc_base): ensure message wait event is set after processing RPC response 2025-04-11 13:28:28 +02:00
wakonig_k 96b31a4509 fix(client_utils): simplify RPC client instantiation in BECGuiClient 2025-04-11 13:25:10 +02:00
wyzula_j 20a86ad325 fix(server): turn_off_the_lights cleanup fixed for parent_id widgets 2025-04-11 10:54:45 +02:00
wyzula_j 7e65d4f2d6 fix(launch_window): redesign 2025-04-11 10:54:45 +02:00
wyzula_j 11feeff37c fix(main_window): connected to theme change 2025-04-11 10:45:28 +02:00
wyzula_j c1bbb16dad fix(round_frame): orientation can be vertical 2025-04-11 10:45:28 +02:00
wyzula_j a5f1f4781e build(bec_lib): raised required version to 3.28.1 2025-04-11 10:45:28 +02:00
wyzula_j 56c2827140 refactor(auto_update): auto_update changed to be BECMainWindow; removed auto update logic from BECDockArea 2025-04-11 10:45:28 +02:00
wyzula_j b03d2eaeed fix(waveform): dap curve flickering 2025-04-11 10:45:28 +02:00
wyzula_j 3a82c95f60 fix(waveform, rpc_reference): __getitem__ removed form waveform and rpc_reference 2025-04-11 10:45:28 +02:00
wyzula_j 5f272a66a4 feat(auto_update): add GUI highlight management for auto updates status 2025-04-11 10:45:28 +02:00
wyzula_j 55baa84eb6 feat(main_window): add launcher menu and functionality to show launcher 2025-04-11 10:45:28 +02:00
wyzula_j b51d637c5f test(plot_base): test for plot base re-enabled 2025-04-11 10:45:28 +02:00
wyzula_j c97db6aaae fix(client): regenerated client 2025-04-11 10:45:28 +02:00
wakonig_k e725de3c45 fix(dock_area): close BECMainWindow if dock area is central widget 2025-04-11 10:45:28 +02:00
wyzula_j 6082e7a690 refactor(rpc_server): cli_server renamed to rpc_server 2025-04-11 10:45:28 +02:00
wakonig_k 8914f1d506 test(setting_dialog): test that settings reject calls cleanup 2025-04-11 10:45:28 +02:00
wakonig_k d06605122e test: qapp must shutdown cli server before checking for leaked QTimer 2025-04-11 10:45:28 +02:00
wakonig_k a8adb064f5 test(generate_cli): fix reference output 2025-04-11 10:45:28 +02:00
wakonig_k 31c3b64d7b test(device_signal_input): fix init of device input widget 2025-04-11 10:45:28 +02:00
wakonig_k 23bdd95d8c test(bec_connector): BECConnector requires a QObject 2025-04-11 10:45:28 +02:00
wakonig_k d1712552ff fix(cli): add type ignore comment to generated files 2025-04-11 10:45:28 +02:00
wakonig_k 20a1c5ddb3 feat(launcher): add option for launching with auto updates 2025-04-11 10:45:28 +02:00
wakonig_k 2511056557 feat!: add support for auto updates 2025-04-11 10:45:27 +02:00
wyzula_j 99383b7715 refactor(launcher,main_window): launcher window moved to inherit from BECMainWindow 2025-04-11 10:45:27 +02:00
wakonig_k 337a332ed1 fix(plot_framework): all widgets, popups and side menus cleanups adjusted 2025-04-11 10:45:27 +02:00
wyzula_j a1bec75115 fix(widgets)!: BECConnector resolves hierarchy including objectName, parent, parent_id upon init; all widgets adjusted 2025-04-11 10:45:27 +02:00
wyzula_j a2128ad8d6 fix(RPCReference): setattr added 2025-04-10 16:11:59 +02:00
wyzula_j 5f27a90989 feat(server,launcher)!: RPC server separated with the launcher window introduced 2025-04-10 16:11:59 +02:00
wyzula_j 39164feb18 fix(waveform): signals for x device can be defined from gui 2025-04-09 23:52:31 +02:00
appel_c af28e2e433 fix: support auto_range_x/y for viewAll during measurement 2025-04-09 14:35:52 +02:00
appel_c 515d7ad055 refactor: add fallback to 'index' plotting in case of missmatch in length 2025-04-09 14:35:52 +02:00
appel_c 0e276d4c09 refactor: add support to plot against x_data 2025-04-09 14:35:52 +02:00
appel_c ed2d958de6 refactor: improve plotting behaviour from history 2025-04-09 14:35:52 +02:00
appel_c 25820a1cde refactor: set downsampling to auto=True, method 'peak', activate clipToView for (Async)-Curves and fix ViewAll hook from pg.view_box menu 2025-04-09 14:35:52 +02:00
appel_c 7f7891dfa5 fix: add support for 'add_slice', add downsampling for performance improvements. add tests 2025-04-09 14:35:52 +02:00
appel_c b5015e4e72 built: cleanup gitlab-ci, remove pyqt6 related lines 2025-04-08 14:45:47 +02:00
perl_d 7653e0877c hack: comment out segfaulting test 2025-04-07 14:19:37 +02:00
perl_d 52a9f29bdc docs: add docs on widget plugins 2025-04-07 14:19:37 +02:00
perl_d ca2bb4f9b4 feat: add loader/helper for widget plugins 2025-04-07 14:19:37 +02:00
perl_d b4925918f7 refactor: tidy client generation and add options 2025-04-03 16:12:57 +02:00
perl_d 43e1aa9505 fix: add designer plugin for ScanMetadata 2025-04-03 16:12:57 +02:00
perl_d 28ae0d2b57 fix: expose common classes from bec_widgets package 2025-04-03 16:12:57 +02:00
perl_d 7726d83b68 fix: create widget enum programatically 2025-04-03 16:12:57 +02:00
wyzula_j be552d3ece refactor(utils): qt_utils moved to utils 2025-04-03 16:09:33 +02:00
wyzula_j 8d17f7e32f fix(rpc_register): _lock and _skip_broad_cast moved to instance attributes 2025-04-03 16:09:33 +02:00
wyzula_j 4a74891184 fix(server): BECDockArea type added 2025-04-03 16:09:33 +02:00
wyzula_j c2d2c484cd fix(waveform): legend is correctly updated when changed from curve dialog 2025-04-03 16:09:33 +02:00
wyzula_j b91f1fe487 fix(waveform): fix dap curve categorization logic 2025-04-03 16:09:33 +02:00
wyzula_j d4106c548e ci(e2e): e2e tests are saving logs 2025-04-03 16:09:33 +02:00
wyzula_j 288ea4dbbd fix(waveform): error where scan history is empty 2025-04-03 16:09:33 +02:00
wyzula_j 9fb9a1cfd2 refactor(plots): plot_next_gen module renamed to plots 2025-04-03 16:09:33 +02:00
wyzula_j 378398a29b test(e2e): e2e tests adjusted for new plotting framework 2025-04-03 16:09:33 +02:00
wyzula_j 6ade934356 test(unit_tests): unit tests adjusted to use a modern plotting framework instead of BECFigure 2025-04-03 16:09:33 +02:00
wyzula_j 6ca4aa0f9b fix(client): RPC API adjusted for DockArea, ImageItem and Waveform 2025-04-03 16:09:33 +02:00
wyzula_j b58a098ed4 fix(round_frame): RoundFrame removed from BECWidget inheritance 2025-04-03 16:09:33 +02:00
wyzula_j 42e3b9c137 fix(plot_indicators): plot indicators added to the PlotBase 2025-04-03 16:09:33 +02:00
wyzula_j 4e29291b3a refactor: AutoUpdate disabled 2025-04-03 16:09:33 +02:00
wyzula_j f76d9319bd refactor(bec_figure): BECFigure removed 2025-04-03 16:09:33 +02:00
wyzula_j 6c90ca3107 fix(rpc_register): Lock changed to RLock 2025-04-03 16:09:33 +02:00
wyzula_j 94c2e2db65 fix(setting_widget): added parent kwarg into all settings widgets in plotting framework 2025-04-03 16:09:33 +02:00
wyzula_j 7c31bbd9c2 refactor(multi_waveform_widget): BECMultiWaveformWidget removed 2025-04-03 16:09:33 +02:00
wyzula_j 77f96160ab feat(multi_waveform): multi-waveform widget based on new PlotBase 2025-04-03 16:09:33 +02:00
wyzula_j 1cc2a98489 fix(colormap_widget): size policy fixed 2025-04-03 16:09:33 +02:00
wyzula_j 112eed694c fix(side_panel): side panel menu can be initialized without a title 2025-04-03 16:09:33 +02:00
wyzula_j 1a0097e027 feat(widget_io): added handler for Sliders 2025-04-03 16:09:33 +02:00
wyzula_j 8558b46114 fix(rpc_base): timeout run_rpc 3s 2025-04-03 16:09:33 +02:00
appel_c 75b24467de fix: server shutdown widgets 2025-04-03 16:09:33 +02:00
appel_c c8bdcaabde tests: add test for rpcrefernce on rpcbase object 2025-04-03 16:09:33 +02:00
appel_c a5f06c8f83 fix: broadcast context manager to emit registry changes just once 2025-04-03 16:09:33 +02:00
appel_c d05179a519 refactor: fix cleanup for various widgets, including RoundedFrame 2025-04-03 16:09:33 +02:00
appel_c be83c7d5f4 refactor: fix cleanup bug for BECConnector items, renamed _registry_state to _server_registry 2025-04-03 16:09:33 +02:00
appel_c 757375f117 tests(bec-figure): Comment all BECFigure tests as they will be removed 2025-04-03 16:09:33 +02:00
appel_c 5872253123 refactor: cleanup, fix tests and _top_level dict/windows 2025-04-03 16:09:33 +02:00
appel_c 7ba93ce934 refactor: cleanup rpc reference tracking, fix appquit, fix namespace updates edge cases 2025-04-03 16:09:33 +02:00
appel_c bd5e251ee9 refactor(rpc_reference): refactor rpc reference tracking 2025-04-03 16:09:33 +02:00
appel_c f3d3c9425d test: fix tests for namespace updates 2025-04-03 16:09:33 +02:00
appel_c ee2eefdace fix (client-utils): start server if not running for 'show' and 'new' 2025-04-03 16:09:33 +02:00
appel_c 43b747ec8a fix(device_input_base): removed enums from Pydantic models to make them serialisable 2025-04-03 16:09:33 +02:00
appel_c 58b0c7ddc1 fix(server): remove window.hide() since widgets will be teared down on kill_server before siginit signals is sent 2025-04-03 16:09:33 +02:00
wakonig_k 2ba9b4cb23 feat: add rpc broadcast 2025-04-03 16:09:33 +02:00
wyzula_j 9f2a083abb fix(motor_map): limit map creating optimized 2025-04-03 16:09:33 +02:00
wyzula_j f878e87ad5 refactor(motor_map_widget): BECMotorMapWidget removed 2025-04-03 16:09:33 +02:00
wyzula_j fec26d793e feat(motor_map): new MotorMap widget based on PlotBase 2025-04-03 16:09:33 +02:00
wyzula_j 98eda03f4d fix(plot_base): do not enable inner axes when label is changed 2025-04-03 16:09:33 +02:00
wyzula_j 0204d9c86f fix(plot_base): axis setting filter for relevant properties 2025-04-03 16:09:33 +02:00
wyzula_j e6795dd87c fix(scatter_waveform,waveform): Added QTimer to fetch the last data points after 500ms 2025-04-03 16:09:33 +02:00
wyzula_j 95fcf016c3 feat(scatter_waveform): scatter waveform widget based on new Plotbase 2025-04-03 16:09:33 +02:00
wyzula_j 0dd9617e6e refactor(tests): create dummy scan item moved to client_mocks.py 2025-04-03 16:09:33 +02:00
wyzula_j 4f9514fbd1 fix(plot_base): improved handling of matplotlib exporter errors 2025-04-03 16:09:33 +02:00
wyzula_j 890b50115f fix(plot_base): ability to set y label suffix 2025-04-03 16:09:33 +02:00
wyzula_j de10609b3c refactor(image_widget): old BECImageWidget removed 2025-04-03 16:09:33 +02:00
wyzula_j cb39ff3fbd feat(image): new Image widget based on new PlotBase 2025-04-03 16:09:33 +02:00
wyzula_j ac08bdfab2 fix(toolbar): update action check handling logic for SwitchableToolBarAction 2025-04-03 16:09:33 +02:00
wyzula_j 30db18367e fix(plot_base): enable popup property fixed 2025-04-03 16:09:33 +02:00
wyzula_j a85402dde1 fix(crosshair): adapted for 2D image 2025-04-03 16:09:33 +02:00
appel_c 17f2dda977 test: disable test_bec_dock_rpc_e2e module, issue to fix this created #450 2025-04-03 16:09:33 +02:00
appel_c d211bd67ab tests: fix e2e tests for namespace refactoring 2025-04-03 16:09:33 +02:00
appel_c 0b00cd24fd refactor: cleanup MR 2025-04-03 16:09:32 +02:00
wakonig_k ac3c5a38e4 feat!: namespace update for gui, dock_area and docks. 2025-04-03 16:09:32 +02:00
wyzula_j b085ef6e73 docs(plot_base): update docstrings for properties and setters 2025-04-03 16:09:32 +02:00
wyzula_j 96cff49cd4 refactor(waveform_widget): removed and replaced by Waveform 2025-04-03 16:09:32 +02:00
wyzula_j 360fe4c9c3 test(plot_indicators): tests adapted to not be dependent on BECWaveformWidget 2025-04-03 16:09:32 +02:00
wyzula_j 4865341010 fix(plot_indicators): cleanup adjusted 2025-04-03 16:09:32 +02:00
wyzula_j 4bec181f3a feat(waveform): new Waveform widget based on NextGen PlotBase 2025-04-03 16:09:32 +02:00
wyzula_j da05877dd0 fix(entry_validator): validator reports list of signal if user chooses the wrong one 2025-04-03 16:09:32 +02:00
wyzula_j fc24c8b3a5 fix(plot_base): update mouse mode state on mode change 2025-04-03 16:09:32 +02:00
wyzula_j 19d8aeb162 fix(plot_base): aspect ratio removed from the PlotBase 2025-04-03 16:09:32 +02:00
wyzula_j 055b96818a fix(plot_base): inner and outer axis setting in popup mode 2025-04-03 16:09:32 +02:00
wyzula_j 39cf4ddd5a fix(plot_base): fix cleanup of popups if popups are still open when PlotBase is closed 2025-04-03 16:09:32 +02:00
wyzula_j 584b945005 fix(lmfit_dialog_vertical): vertical sizePolicy fixed 2025-04-03 16:09:32 +02:00
wyzula_j 9dabf2c66c build: pyside6 capped to 6.9 2025-04-03 15:56:34 +02:00
semantic-release 8f2f42f818 1.25.1
Automatically generated by python-semantic-release
2025-03-24 19:00:20 +00:00
wakonig_k e5c9dd288c fix(positioner_box): if possible tweak should use the current setpoint instead of the readback 2025-03-24 15:27:32 +01:00
wakonig_k be274a10fc fix(positioner_box): fixed motor moving flags for spinner 2025-03-21 18:12:55 +01:00
wakonig_k d86ef4e763 ci: add e2e job for pre_release branches 2025-03-13 16:44:57 +01:00
wakonig_k 6cf39b3796 ci: fix conda channels for PSI policy change 2025-03-13 16:13:44 +01:00
semantic-release 15e11b287d 1.25.0
Automatically generated by python-semantic-release
2025-03-07 15:19:37 +00:00
wyzula_j 7cbebbb1f0 feat(waveform): add slice handling and reset functionality for async updates 2025-03-07 15:44:46 +01:00
semantic-release 66f4f9bfa8 1.24.5
Automatically generated by python-semantic-release
2025-03-06 14:51:03 +00:00
appel_c 66c6c7fa50 fix: add support for additional keyword arguments in widget constructors 2025-03-06 15:39:16 +01:00
semantic-release 31c3337300 1.24.4
Automatically generated by python-semantic-release
2025-03-05 19:59:54 +00:00
wakonig_k 2c506ee3c8 fix(cli/server): handle RedisError during heartbeat emission to properly close the app even if the Redis connection is lost 2025-03-05 20:41:33 +01:00
semantic-release 25423f4a3a 1.24.3
Automatically generated by python-semantic-release
2025-03-05 09:46:53 +00:00
wyzula_j fa91366dcb fix(multi_waveform): update on_async_readback to use structured metadata for async updates with "add" instead of "extend" 2025-03-04 22:31:14 +01:00
semantic-release 4db0f9f10c 1.24.2
Automatically generated by python-semantic-release
2025-02-27 10:08:57 +00:00
wyzula_j 46b1a228be fix(e2e): added wait time to flaky e2e 2025-02-27 10:54:36 +01:00
semantic-release 531018b0ac 1.24.1
Automatically generated by python-semantic-release
2025-02-26 21:06:09 +00:00
wyzula_j 8679b5f08b test: extended test coverage for axis settings, plot base and qt toolbar action 2025-02-26 21:54:33 +01:00
wyzula_j 6f2c2401ac refactor(plot_base): toolbar buttons adapted for the Switch actions from toolbar; plot export and mouse modes consolidated into one switch button 2025-02-26 21:54:33 +01:00
wyzula_j 6d1106e33e fix(toolbar): Switch Actions for default checked actions fixed 2025-02-26 21:54:33 +01:00
wyzula_j 90a184643a refactor(axis_settings): spinbox migrated to new BECSpinBoxes 2025-02-26 21:54:33 +01:00
wyzula_j 3aa2f2225f fix(plot_base): ability to choose between popup or side panel gui mode 2025-02-26 21:54:33 +01:00
semantic-release f54e69f1cf 1.24.0
Automatically generated by python-semantic-release
2025-02-26 11:20:07 +00:00
perl_d 7309c1dede feat: add metadata widget to scan control 2025-02-26 12:08:32 +01:00
perl_d 1c0021f98b fix: make scan metadata use collapsible frame 2025-02-26 12:08:32 +01:00
perl_d d32952a0d5 style: isort 2025-02-26 12:08:32 +01:00
perl_d 5206528fec feat: add expandable/collapsible frame 2025-02-26 12:08:32 +01:00
perl_d 42665b69c5 fix: replace add'l md table w/ tree view 2025-02-26 12:08:32 +01:00
semantic-release 209c898e3d 1.23.1
Automatically generated by python-semantic-release
2025-02-24 13:54:40 +00:00
perl_d 6a43554f3b fix: update redis mock for changes in bec 2025-02-24 14:43:02 +01:00
semantic-release 95c931af0b 1.23.0
Automatically generated by python-semantic-release
2025-02-24 10:00:25 +00:00
wyzula_j f19d9485df feat(bec_spin_box): double spin box with setting inside for defining decimals 2025-02-24 10:49:10 +01:00
semantic-release 575c988c4f 1.22.0
Automatically generated by python-semantic-release
2025-02-19 16:54:57 +00:00
wyzula_j 6b08f7cfb2 refactor(toolbar): added dark mode button for testing appearance for the toolbar example 2025-02-19 17:43:49 +01:00
wyzula_j 6ae33a23a6 test(toolbar): blocking tests fixed 2025-02-19 17:08:56 +01:00
wyzula_j facb8c30ff fix(toolbar): update_separators logic updated, there cannot be two separators next to each other 2025-02-19 15:44:44 +01:00
wyzula_j 333570ba2f feat(toolbar): SwitchableToolBarButton 2025-02-19 15:42:31 +01:00
wyzula_j ef36a7124d fix(toolbar): widget actions are more compact 2025-02-19 15:02:17 +01:00
wyzula_j c2c022154b fix(toolbar): QMenu Icons are visible 2025-02-19 15:02:17 +01:00
wyzula_j 4c4f1592c2 fix(modular_toolbar): add action to an already existing bundle 2025-02-19 15:02:17 +01:00
semantic-release d7fb291877 1.21.4
Automatically generated by python-semantic-release
2025-02-19 13:29:43 +00:00
wyzula_j ae18279685 fix(colors): pyqtgraph styling updated on the app level 2025-02-19 14:18:18 +01:00
wyzula_j 97c0ed53df fix(plot_base): mouse interactions default state fetch to toolbar 2025-02-19 14:18:18 +01:00
wyzula_j ff8e282034 refactor(plot_base): Change the PlotWidget to GraphicalLayoutWidget 2025-02-19 14:18:18 +01:00
semantic-release 440f36f289 1.21.3
Automatically generated by python-semantic-release
2025-02-19 12:44:37 +00:00
wyzula_j 0addef5f17 fix(bec_signal_proxy): unblock signal timer cleanup added 2025-02-19 13:33:16 +01:00
semantic-release 8c2a5e61fc 1.21.2
Automatically generated by python-semantic-release
2025-02-18 14:41:43 +00:00
wyzula_j 056731c9ad fix(client_utils): autoupdate has correct propagation of BECDockArea to plugin repos 2025-02-18 15:06:53 +01:00
semantic-release 911c81a167 1.21.1
Automatically generated by python-semantic-release
2025-02-17 14:54:21 +00:00
wyzula_j 8651314d93 build:unlock pyside version 2025-02-17 15:18:29 +01:00
wyzula_j 383936ffc2 fix(bec_connector): workers stored in reference to not be cleaned up with garbage collector 2025-02-17 15:18:29 +01:00
semantic-release 4378d33880 1.21.0
Automatically generated by python-semantic-release
2025-02-17 10:37:33 +00:00
perl_d 1708bd405f feat: generated form for scan metadata 2025-02-17 11:21:08 +01:00
wakonig_k 12811eccdb tests(scan_control): fixed hard-coded redis paths 2025-02-13 17:49:00 +01:00
semantic-release 5959fa87de 1.20.0
Automatically generated by python-semantic-release
2025-02-06 15:37:33 +00:00
perl_d b3217b7ca5 feat(widget): add LogPanel widget
hopefully without segfaults - compared to first implementation:
- explicitly set parent of all dialog components
- try/except and log for redis new message callback
- pass in ServiceStatusMixin and explicitly clean it up
2025-02-06 16:26:02 +01:00
semantic-release 35b941d054 1.19.2
Automatically generated by python-semantic-release
2025-02-06 15:23:58 +00:00
perl_d fc6d7c0824 fix: cleanup timer in Minesweeper 2025-02-06 15:12:48 +01:00
perl_d fb051865d5 fix: mock QTimer, improve timeout message 2025-02-06 15:12:48 +01:00
semantic-release 8aba3d975f 1.19.1
Automatically generated by python-semantic-release
2025-02-05 13:49:03 +00:00
wakonig_k 5e3289f5bd fix(macos): suppress IMKClient warning on macos 2025-02-05 13:01:40 +01:00
wakonig_k d07744397e Revert "feat(widget): add LogPanel widget"
This reverts commit f048880277
2025-02-05 08:57:09 +01:00
semantic-release dc7bf6b3c4 1.19.0
Automatically generated by python-semantic-release
2025-01-31 10:57:04 +00:00
perl_d f219c6fb57 docs: add docs for LogPanel 2025-01-31 10:10:08 +01:00
perl_d f048880277 feat(widget): add LogPanel widget 2025-01-31 10:10:08 +01:00
perl_d 50a572dacd fix: enable type checking for BECDispatcher in BECConnector 2025-01-30 17:28:30 +01:00
semantic-release b87549ba99 1.18.1
Automatically generated by python-semantic-release
2025-01-30 16:22:51 +00:00
appel_c f0c4efefa0 docs: add screenshots for device and signal input 2025-01-30 17:11:44 +01:00
appel_c db70442cc2 fix(signal_combo_box): added missing plugin modules for signal line_edit/combobox 2025-01-30 17:11:44 +01:00
semantic-release 07b8910686 1.18.0
Automatically generated by python-semantic-release
2025-01-30 16:07:01 +00:00
wyzula_j e7c97290cd feat(plot_base_next_gen): new type of plot base inherited from QWidget 2025-01-30 16:49:13 +01:00
wyzula_j 48fc63d83e fix(generate_cli): widgets can be tagged with RPC=False, then they are excluded from client.py for RPC 2025-01-30 16:49:13 +01:00
wyzula_j a20935e862 build: pyqt6 support dropped 2025-01-30 15:53:38 +01:00
wyzula_j 4f8e6835fe ci: fix formatter 2024 versions 2025-01-30 14:41:00 +01:00
semantic-release 042adfa51e 1.17.2
Automatically generated by python-semantic-release
2025-01-28 19:12:25 +00:00
wyzula_j b2b0450bcb fix(widget_state_manager): skip QLabel saving; skip_setting property widget excluded from INI; stored=False property excluded from INI 2025-01-28 18:34:21 +01:00
semantic-release 12e06fa971 1.17.1
Automatically generated by python-semantic-release
2025-01-26 15:32:17 +00:00
wyzula_j 6f2f2aa06a fix(bec_signal_proxy): timeout for blocking implemented 2025-01-26 14:29:30 +01:00
semantic-release 21965a0ee3 1.17.0
Automatically generated by python-semantic-release
2025-01-23 12:51:19 +00:00
perl_d 6df57103bb fix: focus policy and tab order for positioner_box_2d 2025-01-23 13:21:04 +01:00
perl_d 9a8cc31f6c docs: add documentation for 2D positioner box 2025-01-23 13:21:04 +01:00
perl_d d2ffddb6d8 feat(widget): add 2d positioner box widget 2025-01-23 13:21:04 +01:00
perl_d 3770db51be refactor: move positioner_box logic to base class 2025-01-23 13:21:04 +01:00
perl_d 2419521f5f refactor: move positioner_box and line into submodule
PositionerBox and PositionerControlLine are now exported from
from bec_widgets.widgets.control.device_control.positioner_box, removing
one level of hierarchy
2025-01-23 13:21:04 +01:00
semantic-release 80937cba97 1.16.5
Automatically generated by python-semantic-release
2025-01-22 19:12:06 +00:00
wyzula_j df961a9b88 fix(cli): server log level info and error 2025-01-22 20:02:00 +01:00
wyzula_j 219d43d325 fix(error_popups): errors in SafeProperty and in SafeSlot are always logged, even with error message popup enabled 2025-01-22 15:15:11 +01:00
semantic-release 229833eb99 1.16.4
Automatically generated by python-semantic-release
2025-01-21 16:29:14 +00:00
perl_d 141e1a34c9 fix: make combo box plugin files conform to autogen name 2025-01-20 15:24:05 +01:00
semantic-release d40075f85b 1.16.3
Automatically generated by python-semantic-release
2025-01-20 09:20:33 +00:00
wyzula_j dfa2908c3d test(error_popups): SafeSlot tests adjusted; tests extended to cover SafeProperty 2025-01-20 10:08:44 +01:00
wyzula_j 02a4862afd fix(error_popups): logger message in SafeSlot for errors; identification in error log from which property or signal errors comes from 2025-01-20 10:08:44 +01:00
semantic-release 13438e22d3 1.16.2
Automatically generated by python-semantic-release
2025-01-20 09:06:13 +00:00
wyzula_j 889ea8629f fix(widget_io): ToggleSwitchHandler added 2025-01-16 12:26:40 +01:00
semantic-release 0ef509e9ca 1.16.1
Automatically generated by python-semantic-release
2025-01-16 10:37:04 +00:00
wyzula_j b40d2c5f0b fix(error_popups): SafeProperty logger import fixed 2025-01-16 11:22:14 +01:00
semantic-release 6cd7ff6ef7 1.16.0
Automatically generated by python-semantic-release
2025-01-14 15:59:07 +00:00
wyzula_j 0fd5dd5a26 fix(e2e): num of elements to wait for scan fixed to steps requested in the scan 2025-01-14 16:47:57 +01:00
wyzula_j 508abfa8a5 fix(toolbar): adjusted to future plot base 2025-01-14 16:47:57 +01:00
wyzula_j 001e6fc807 feat(modular_toolbar): context menu and action bundles 2025-01-14 13:53:08 +01:00
semantic-release 111dcef35a 1.15.1
Automatically generated by python-semantic-release
2025-01-13 13:41:49 +00:00
wyzula_j 3b04b985b6 fix(error_popups): SafeProperty wrapper extended to catch more errors and not crash Designer 2025-01-13 11:25:25 +01:00
semantic-release 5944626d93 1.15.0
Automatically generated by python-semantic-release
2025-01-10 15:51:23 +00:00
wyzula_j a00d368c25 feat(widget_state_manager): example app added 2025-01-10 16:32:31 +01:00
wyzula_j 01b4608331 feat(widget_state_manager): state manager for single widget 2025-01-10 16:32:31 +01:00
semantic-release b7221d1151 1.14.1
Automatically generated by python-semantic-release
2025-01-10 14:34:09 +00:00
perl_d fa9ecaf433 fix: cast spinner widget angle to int when using for arc 2025-01-10 15:22:58 +01:00
semantic-release c751d25f85 1.14.0
Automatically generated by python-semantic-release
2025-01-09 14:29:40 +00:00
perl_d e2c7dc98d2 docs: add docs for games/minesweeper 2025-01-09 15:24:00 +01:00
perl_d 507d46f88b feat(widget): make Minesweeper into BEC widget 2025-01-09 15:24:00 +01:00
wakonig_k 57dc1a3afc feat(widgets): added minesweeper widget 2025-01-09 15:24:00 +01:00
semantic-release 6a78da0e71 1.13.0
Automatically generated by python-semantic-release
2025-01-09 14:18:04 +00:00
wakonig_k fb545eebb3 tests(safeslot): wait for panels to be properly rendered 2025-01-09 14:55:31 +01:00
wakonig_k b4a240e463 tests(e2e): wait for the plotting to finish before checking the data 2025-01-09 14:38:58 +01:00
wyzula_j 54e64c9f10 feat(widget_io): general change signal for supported widgets 2025-01-06 10:28:16 +01:00
wakonig_k 1c8b06cbe6 refactor(rpc,client_utils): minor cleanup and type hint improvements 2024-12-23 15:59:10 +01:00
guijar_m 52c5286d64 fix: do not display error popup if command is executed via RPC 2024-12-23 15:59:10 +01:00
guijar_m c405421db9 fix: use generator exec feature of BEC Connector to remove the AutoUpdate thread+queue 2024-12-23 15:59:10 +01:00
guijar_m 0ff0c06bd1 feat: add test for BECGuiClient features .new, .delete, .show, .hide, .close 2024-12-23 15:59:10 +01:00
guijar_m 955cc64257 fix: tests: rename fixtures and add 'connected_client_gui_obj' 2024-12-23 15:59:10 +01:00
guijar_m 09cb08a233 fix: prevent top-level dock areas to be destroyed with [X] button 2024-12-23 15:59:10 +01:00
guijar_m 5c83702382 refactor: move RPC-related classes and modules to 'rpc' directory
This allows to break circular import, too
2024-12-23 15:59:10 +01:00
guijar_m 1b0382524f fix: simplify AutoUpdate code thanks to threadpool executor in BEC Connector 2024-12-23 15:59:10 +01:00
guijar_m 92b802021f feat: add '.delete()' method to BECDockArea, make main window undeletable 2024-12-23 15:59:10 +01:00
guijar_m 48c140f937 fix: add .windows property to keep track of top level windows, ensure all windows are shown/hidden 2024-12-23 15:59:10 +01:00
guijar_m 42fd78df40 fix: remove useless class member 2024-12-23 15:59:10 +01:00
guijar_m 271a4a24e7 fix: determine default figure since the beginning 2024-12-23 15:59:10 +01:00
guijar_m 1b03ded906 fix: prevent infinite recursion in show/hide methods 2024-12-23 15:59:10 +01:00
guijar_m bde5618699 feat: add "new()" command to create new dock area windows from client 2024-12-23 15:59:10 +01:00
guijar_m 6f2eb6b4cd fix: bec-gui-server script: fix logic with __name__ == '__main__'
When started with "bec-gui-server" entry point, __name__ is
"bec_widgets.cli.server".
When started with "python -m bec_widgets.cli.server", __name__ is
"__main__".
So, better to not rely on __name__ at all.
2024-12-23 15:59:10 +01:00
guijar_m 2742a3c6cf fix: set minimum size hint on BECDockArea 2024-12-23 15:59:10 +01:00
guijar_m 809e654087 refactor: BECGuiClientMixin -> BECGuiClient
- Mixin class was only used with BECDockArea, now it is a class by itself
which represents the client object connected to the GUI server ; ".main"
is the dock area of the main window
- Enhanced "wait_for_server"
- ".selected_device" is stored in Redis, to allow server-side to know
about the auto update configuration instead of keeping it on client
2024-12-23 15:59:10 +01:00
guijar_m bdb25206d9 fix: use specified timeout in _run_rpc 2024-12-23 15:59:10 +01:00
wakonig_k bd5414288c build: fixed pytest bec dependency 2024-12-20 18:13:00 +01:00
wakonig_k 95f6a7ceb7 ci: install pytest plugin from specified repo, not pypi 2024-12-20 17:37:52 +01:00
semantic-release b75c4c88fe 1.12.0
Automatically generated by python-semantic-release
2024-12-12 10:35:17 +00:00
wyzula_j e38048964f feat(safe_property): added decorator to handle errors in Property decorator from qt to not crash designer 2024-12-11 22:37:03 +01:00
semantic-release ce11d1382c 1.11.0
Automatically generated by python-semantic-release
2024-12-11 16:19:34 +00:00
wyzula_j ff654b56ae test(collapsible_panel_manager): fixture changed to not use .show() 2024-12-11 15:24:59 +01:00
wyzula_j a434d3ee57 feat(collapsible_panel_manager): panel manager to handle collapsing and expanding widgets from the main widget added 2024-12-11 15:18:43 +01:00
semantic-release b467b29f77 1.10.0
Automatically generated by python-semantic-release
2024-12-10 19:59:55 +00:00
wyzula_j 17a63e3b63 feat(layout_manager): grid layout manager widget 2024-12-10 20:49:19 +01:00
semantic-release 66fc5306d6 1.9.1
Automatically generated by python-semantic-release
2024-12-10 19:34:00 +00:00
wyzula_j 6563abfddc fix(designer): general way to find python lib on linux 2024-12-10 19:12:21 +01:00
semantic-release 0d470ddf05 1.9.0
Automatically generated by python-semantic-release
2024-12-10 10:53:44 +00:00
wyzula_j 9b95b5d616 test(side_panel): tests added 2024-12-10 11:42:46 +01:00
wyzula_j c7d7c6d9ed feat(side_menu): side menu with stack widget added 2024-12-10 11:42:46 +01:00
semantic-release 4686a643f5 1.8.0
Automatically generated by python-semantic-release
2024-12-10 10:08:47 +00:00
wyzula_j 9370351abb test(modular_toolbar): tests added 2024-12-09 21:10:18 +01:00
wyzula_j a55134c3bf feat(modular_toolbar): material icons can be added/removed/hide/show/update dynamically 2024-12-09 20:56:03 +01:00
wyzula_j 5fdb2325ae feat(modular_toolbar): orientation setting 2024-12-09 15:04:59 +01:00
wyzula_j 6a36ca512d feat(round_frame): rounded frame for plot widgets and contrast adjustments 2024-12-09 15:01:09 +01:00
semantic-release a274a14900 1.7.0
Automatically generated by python-semantic-release
2024-12-02 15:21:52 +00:00
guijar_m da579b6d21 fix(tests): add test for Console widget 2024-12-02 14:44:29 +01:00
guijar_m 02086aeae0 feat(console): add 'terminate' and 'send_ctrl_c' methods to Console
.terminate() ends the started process, sending SIGTERM signal.
If process is not dead after optional timeout, SIGKILL is sent.
.send_ctrl_c() sends SIGINT to the child process, and waits for
prompt until optional timeout is reached.
Timeouts raise 'TimeoutError' exception.
2024-12-02 14:44:29 +01:00
guijar_m 3aeb0b66fb feat(console): add "prompt" signal to inform when shell is at prompt 2024-12-02 14:44:29 +01:00
semantic-release b4b8ae81d8 1.6.0
Automatically generated by python-semantic-release
2024-11-27 11:04:08 +00:00
guijar_m da18c2ceec fix(tests): make use of BECDockArea with client mixin to start server and use it in tests
Depending on the test, auto-updates are enabled or not.
2024-11-27 11:44:03 +01:00
guijar_m 31d87036c9 feat: '._auto_updates_enabled' attribute can be used to activate auto updates installation in BECDockArea 2024-11-27 11:44:03 +01:00
guijar_m cffcdf2923 fix: differentiate click and drag for DeviceItem, adapt tests accordingly
This fixes the blocking "QDrag.exec_()" on Linux, indeed before the
drag'n'drop operation was started with a simple click and it was
waiting for drop forever. Now there are 2 different cases, click or
drag'n'drop - the drag'n'drop test actually moves the mouse and releases
the button.
2024-11-27 11:44:03 +01:00
guijar_m 2fe7f5e151 fix(server): use dock area by default 2024-11-27 11:44:03 +01:00
wyzula_j 3ba0b1daf5 feat: add rpc_id member to client objects 2024-11-27 11:44:03 +01:00
guijar_m e68e2b5978 feat(client): add show()/hide() methods to "gui" object 2024-11-27 11:44:03 +01:00
guijar_m daf6ea0159 feat(server): add main window, with proper gui_id derived from given id 2024-11-27 11:44:03 +01:00
wyzula_j f80ec33ae5 feat: add main window container widget 2024-11-27 11:44:03 +01:00
wyzula_j c27d058b01 fix(rpc): gui hide/show also hide/show all floating docks 2024-11-27 11:44:03 +01:00
guijar_m 96e255e4ef fix: do not quit automatically when last window is "closed"
Qt confuses closed and hidden
2024-11-27 11:44:03 +01:00
guijar_m 60292465e9 fix: no need to call inspect.signature - it can fail on methods coming from C (like Qt methods) 2024-11-27 11:44:03 +01:00
guijar_m 2047e484d5 feat: asynchronous .start() for GUI 2024-11-27 11:44:03 +01:00
guijar_m 1f71d8e5ed feat: do not take focus when GUI is loaded 2024-11-25 08:16:10 +01:00
guijar_m 1f60fec720 feat: add '--hide' argument to BEC GUI server 2024-11-25 08:16:10 +01:00
wyzula_j e9983521ed fix: add back accidentally removed variables 2024-11-25 08:16:10 +01:00
semantic-release ed72393699 1.5.3
Automatically generated by python-semantic-release
2024-11-21 16:19:45 +00:00
wyzula_j e71e3b2956 fix(alignment_1d): fix imports after widget module refactor 2024-11-21 16:39:10 +01:00
appel_c 6e39bdbf53 ci: fix ci syntax for package-dep-job 2024-11-21 09:13:18 +01:00
semantic-release 2e7383a10c 1.5.2
Automatically generated by python-semantic-release
2024-11-18 13:53:35 +00:00
wakonig_k 746359b2cc fix: support for bec v3 2024-11-18 14:23:12 +01:00
semantic-release 0219f7c78a 1.5.1
Automatically generated by python-semantic-release
2024-11-14 13:30:02 +00:00
wyzula_j aab0229a40 refactor(widgets): widget module structure reorganised 2024-11-14 14:20:20 +01:00
wyzula_j 7a1b8748a4 fix(plugin_utils): plugin utils are able to detect classes for plugin creation based on class attribute rather than if it is top level widget 2024-11-14 14:19:22 +01:00
semantic-release 245ebb444e 1.5.0
Automatically generated by python-semantic-release
2024-11-12 15:29:42 +00:00
wyzula_j 0cd85ed9fa fix(crosshair): crosshair adapted for multi waveform widget 2024-11-12 16:19:42 +01:00
wyzula_j 42d4f182f7 docs(multi_waveform): docs added 2024-11-12 16:19:42 +01:00
wyzula_j f3a39a69e2 feat(multi-waveform): new widget added 2024-11-12 16:19:42 +01:00
semantic-release ec39dae273 1.4.1
Automatically generated by python-semantic-release
2024-11-12 13:46:09 +00:00
wakonig_k 8e5c0ad8c8 fix(positioner_box): adjusted default signals 2024-11-12 14:36:38 +01:00
semantic-release bf0b49b863 1.4.0
Automatically generated by python-semantic-release
2024-11-11 14:19:33 +00:00
wyzula_j 11e5937ae0 fix(crosshair): label of coordinates of TextItem displays numbers in general format 2024-11-11 15:09:55 +01:00
wyzula_j 4f31ea655c fix(crosshair): label of coordinates of TextItem is updated according to the current theme of qapp 2024-11-11 15:09:55 +01:00
wyzula_j 64df805a9e test(crosshair): tests extended 2024-11-11 15:09:55 +01:00
wyzula_j 035136d517 feat(crosshair): TextItem to display crosshair coordinates 2024-11-11 15:09:55 +01:00
wyzula_j b2eb71aae0 fix(crosshair): log is separately scaled for backend logic and for signal emit 2024-11-11 15:09:55 +01:00
semantic-release 1e6659c379 1.3.3
Automatically generated by python-semantic-release
2024-11-07 23:02:04 +00:00
wyzula_j 5fabd4bea9 fix(scan_control): DeviceLineEdit kwargs readings changed to get name of the positioner 2024-11-07 16:47:42 +01:00
appel_c 4f0693cae3 docs: update outdated text in docs 2024-11-07 12:49:36 +01:00
semantic-release ba76d6bb86 1.3.2
Automatically generated by python-semantic-release
2024-11-05 14:53:05 +00:00
wyzula_j 2304c9f849 fix(plot_base): legend text color is changed when changing dark-light theme 2024-11-05 10:37:53 +01:00
wyzula_j c6e48ec1fe build: PySide6 version fixed 6.7.2 2024-11-04 14:41:43 +01:00
semantic-release f837129023 1.3.1
Automatically generated by python-semantic-release
2024-10-31 14:37:23 +00:00
wyzula_j 940ee6552c fix(ophyd_kind_util): Kind enums are imported from the bec widget util class 2024-10-31 12:26:10 +01:00
semantic-release 86b60b4aed 1.3.0
Automatically generated by python-semantic-release
2024-10-30 13:19:18 +00:00
wyzula_j 14dd8c5b29 fix(colors): extend color map validation for matplotlib and colorcet maps (if available) 2024-10-28 17:17:03 +01:00
wyzula_j b039933405 feat(colormap_button): colormap button with menu to select colormap filtered by the colormap type 2024-10-28 13:48:56 +01:00
semantic-release d8c80293c7 1.2.0
Automatically generated by python-semantic-release
2024-10-25 17:17:49 +00:00
wyzula_j 40c9fea35f feat(colors): evenly spaced color generation + new golden ratio calculation 2024-10-25 19:08:13 +02:00
appel_c 5d4b86e1c6 refactor: add bec_lib version to statusbox 2024-10-25 16:12:06 +02:00
semantic-release 5681c0cbd1 1.1.0
Automatically generated by python-semantic-release
2024-10-25 08:19:34 +00:00
appel_c 91959e82de refactor: do not flush selection upon receiving config update; allow widgetIO to receive kwargs to be able to use get_value to receive string instead of int for QComboBox 2024-10-24 18:09:18 +02:00
appel_c 5eb15b785f refactor: allow to set selection in DeviceInput; automatic update of selection on device config update; cleanup 2024-10-24 13:38:26 +02:00
appel_c 6fb20552ff refactor: cleanup, added device_signal for signal inputs 2024-10-24 09:21:32 +02:00
appel_c 0350833f36 feat: add filter i/o utility class 2024-10-22 16:56:16 +02:00
wyzula_j acb79020d4 test(scan_control): tests added for grid_scan to ensure scan_args signal validity 2024-10-22 16:05:14 +02:00
semantic-release 9c6ba6ae73 1.0.2
Automatically generated by python-semantic-release
2024-10-22 13:34:16 +00:00
wyzula_j 4f5448cf51 fix(scan_control): scan args signal fixed to emit list instead of hardcoded structure 2024-10-22 15:04:23 +02:00
semantic-release 6f0182115f 1.0.1
Automatically generated by python-semantic-release
2024-10-22 08:47:29 +00:00
wakonig_k 7469c892c8 fix(waveform): added support for live_data and data access 2024-10-18 17:10:53 +02:00
semantic-release cb45527f3e 1.0.0
Automatically generated by python-semantic-release
2024-10-18 09:48:29 +00:00
wyzula_j f9a889fc6d fix(crosshair): downsample clear markers 2024-10-18 11:32:12 +02:00
wyzula_j 2ab12ed60a feat!: ability to disable scatter from waveform & compatible crosshair with down sampling 2024-10-18 11:32:12 +02:00
semantic-release 98c68e9ff4 0.119.0
Automatically generated by python-semantic-release
2024-10-17 15:09:10 +00:00
appel_c 19f4e407e0 fix: fix syntax due to change of api for simulated devices 2024-10-17 16:07:11 +02:00
guijar_m a23841b255 fix: remove wrongly scoped test 2024-10-17 16:07:11 +02:00
guijar_m 6982711fea fix: rename 'compact' property -> 'compact_view' 2024-10-17 16:07:11 +02:00
guijar_m 0015f0e2d6 fix: Alignment 1D update, make app window a main window (in .ui file) 2024-10-17 16:07:11 +02:00
guijar_m af9655de0c feat: new PositionerGroup widget 2024-10-17 16:07:11 +02:00
guijar_m e4121a01cb feat: add 'expand_popup' property to CompactPopupWidget
This property tells if expand should show a popup (by default), or
if the widget should expand in-place
2024-10-17 16:07:11 +02:00
guijar_m a69d2870e2 refactor: redesign of scan selection and scan control boxes 2024-10-17 16:07:07 +02:00
guijar_m e3d0a7bbf9 refactor: move add/remove bundle to scan group box 2024-10-17 09:29:55 +02:00
guijar_m 523cc43572 fix: set (Minimum, Fixed) size policy on Stop button 2024-10-17 09:29:55 +02:00
guijar_m 261578796f feat: PositionerBox with a popup view 2024-10-17 09:29:55 +02:00
guijar_m 0b9b1a3c89 feat: emit 'device_selected' and 'scan_axis' from scan control widget 2024-10-14 16:45:26 +02:00
guijar_m 9801d2769e feat: new 'device_selected' signals to ScanControl, ScanGroupBox, DeviceLineEdit 2024-10-14 16:45:26 +02:00
semantic-release dfccf97a99 0.118.0
Automatically generated by python-semantic-release
2024-10-13 14:18:42 +00:00
wyzula_j 9ef1d1c9ac feat(image): image widget can take data from monitor_1d endpoint 2024-10-13 16:13:53 +02:00
wakonig_k b23695167a docs(sphinx-build): adjusted pyside verion 2024-10-11 17:36:24 +02:00
semantic-release 92cc808d65 0.117.1
Automatically generated by python-semantic-release
2024-10-11 15:27:05 +00:00
wyzula_j 3a22392780 fix(FPS): qtimer cleanup leaking 2024-10-11 17:17:50 +02:00
wakonig_k f5f1f6c304 feature(vscode): added support for vscode instructions 2024-10-11 15:36:56 +02:00
wakonig_k 923867947f feature(vscode): support for controlling vscode from widgets 2024-10-11 15:36:56 +02:00
semantic-release 91260bb579 0.117.0
Automatically generated by python-semantic-release
2024-10-11 10:29:41 +00:00
wyzula_j 8dc892df0a tests(plot_base): tests extended 2024-10-11 12:17:17 +02:00
wyzula_j 8c5ef26843 feat(utils): FPS counter utility based on the viewBox updates, integrated to waveform and image widget 2024-10-11 09:52:48 +02:00
semantic-release b681b13a33 0.116.0
Automatically generated by python-semantic-release
2024-10-11 07:17:54 +00:00
guijar_m 499b6b9a12 feat: UI changes to have top toolbar with compact popup widgets (fix issue #360) 2024-10-11 09:08:37 +02:00
guijar_m 94ce92f5b0 feat: adapt BECQueue and BECStatusBox widgets to use CompactPopupWidget 2024-10-11 09:08:37 +02:00
guijar_m 49268e3829 feat: add 'CompactPopupWidget' container widget
Makes it easy to write widgets which can have a compact
representation with LED-like global state indicator,
with the possibility to display a popup dialog with more
complete UI
2024-10-11 09:08:37 +02:00
wyzula_j 908dbc1760 build: fix PySide6 to 6.7.2 2024-10-10 22:42:16 +02:00
semantic-release d7e6506a27 0.115.0
Automatically generated by python-semantic-release
2024-10-08 09:48:59 +00:00
guijar_m c5e9ed6e42 fix: make Alignment1D a MainWindow as it is an application 2024-10-08 11:39:43 +02:00
guijar_m b207e45a67 fix: adjust bec_qthemes dependency 2024-10-08 11:39:43 +02:00
guijar_m 8bf4842788 feat: add bec-app script to launch applications 2024-10-08 11:39:43 +02:00
semantic-release 49b9bfc9d3 0.114.0
Automatically generated by python-semantic-release
2024-10-02 20:32:01 +00:00
guijar_m 04cfb1edf1 fix: prevent exception when empty string updates are coming from widget 2024-10-02 16:17:24 +02:00
guijar_m efa276358b fix: use new 'scan_axis' signal, to set_x and select x axis on waveform
Fixes #361, do not try to change x axis when not permitted
2024-10-02 16:17:24 +02:00
guijar_m f084e2514b feat: new 'scan_axis' signal
Signal is emitted before "scan_started", to inform about scan positioner
and (start, stop) positions. In case of multiple bundles, the signal
is emitted multiple times.
2024-10-02 16:17:24 +02:00
semantic-release 7cd0b3630e 0.113.0
Automatically generated by python-semantic-release
2024-10-02 11:47:57 +00:00
appel_c dc0c825fd5 test: add tests for scan_status_callback 2024-10-01 22:16:16 +02:00
appel_c 1dcfeb6cfc feat : Add bec_signal_proxy to handle signals with option to unblock them manually. 2024-10-01 22:16:16 +02:00
appel_c f554f3c167 refactor: various minor improvements for the alignment gui 2024-10-01 22:16:16 +02:00
appel_c 0f9953e8fd fix: add is_log checks and functionality to plot_indicator_items 2024-10-01 22:16:16 +02:00
appel_c 63c24f97a3 feat: add first draft for alignment_1d GUI 2024-10-01 22:16:16 +02:00
appel_c efe90eb163 refactor: allow hiding of arg/kwarg boxes 2024-10-01 22:16:16 +02:00
appel_c 281cb27d8b feat: add move to position button to lmfit dialog 2024-10-01 22:16:16 +02:00
appel_c 5c740371d8 refactor: add proxy to waveform to limit the dap_request frequency 2024-10-01 22:16:16 +02:00
appel_c 28ee3856be refactor: update dap_model also if x and y axis are selected 2024-10-01 22:16:16 +02:00
appel_c 7cc0726398 refactor: linear_region_selector accepts log_x data 2024-10-01 22:16:16 +02:00
appel_c e039304fd3 refactor: use accent colors for bec_status_box icons; closes #338 2024-09-26 12:07:33 +02:00
semantic-release 6fa7ca8f09 0.112.1
Automatically generated by python-semantic-release
2024-09-19 09:05:41 +00:00
wyzula_j b2f7d3c5f3 fix: test e2e dap wait_for_fit 2024-09-19 09:30:26 +02:00
wakonig_k e3b5e338bf docs(dap_combo_box): updated screenshot 2024-09-18 14:15:06 +02:00
wakonig_k c8e614b575 docs(device_box): updated screenshot 2024-09-18 14:00:10 +02:00
semantic-release 8e44ca1ad0 0.112.0
Automatically generated by python-semantic-release
2024-09-17 08:13:25 +00:00
guijar_m 286ad7196b feat: console: various improvements, auto-adapt rows to widget size, Qt Designer plugin 2024-09-17 10:08:49 +02:00
semantic-release adef25f4e2 0.111.0
Automatically generated by python-semantic-release
2024-09-17 04:41:08 +00:00
wakonig_k 60f7d54e2b docs(position_indicator): updated position indicator documentation and added designer properties 2024-09-16 16:56:58 +02:00
wakonig_k dd932dd8f3 fix(position_indicator): fixed user access 2024-09-16 16:56:58 +02:00
wakonig_k d3c1a1b2ed fix(generate_cli): fixed type annotations 2024-09-16 16:56:58 +02:00
wakonig_k 7ea4a482e7 fix(positioner_box): visual improvements to the positioner_box and positioner_control_line 2024-09-16 13:34:39 +02:00
wakonig_k 9045323049 fix(palette viewer): fixed background for tool tip 2024-09-14 18:57:50 +02:00
wakonig_k d15b22250f feat(position_indicator): improved design and added more customization options 2024-09-14 18:33:00 +02:00
semantic-release 5557bfe717 0.110.0
Automatically generated by python-semantic-release
2024-09-12 08:28:50 +00:00
wakonig_k a8576c164c feat(palette_viewer): added widget to display the current palette and accent colors 2024-09-12 08:58:54 +02:00
semantic-release f5807ec5cd 0.109.1
Automatically generated by python-semantic-release
2024-09-09 15:50:30 +00:00
appel_c b0d786b991 fix: refactor textbox widget, remove inheritance, adhere to bec style; closes #324 2024-09-09 17:41:27 +02:00
semantic-release 774044d2a7 0.109.0
Automatically generated by python-semantic-release
2024-09-06 17:30:40 +00:00
wakonig_k 84a59f70ee feat(accent colors): added helper function to get all accent colors 2024-09-06 19:26:17 +02:00
wakonig_k de303f0227 fix(theme): fixed theme access for themecontainer 2024-09-06 19:26:17 +02:00
semantic-release cb2131b1de 0.108.0
Automatically generated by python-semantic-release
2024-09-06 15:18:45 +00:00
wakonig_k 7d07cea946 docs(progressbar): added docs 2024-09-06 17:09:45 +02:00
wakonig_k f6d1d0bbe3 feat(progressbar): added bec progressbar 2024-09-06 17:09:45 +02:00
wakonig_k a52182dca9 feat(generate_cli): added support for property and qproperty setter 2024-09-06 17:09:45 +02:00
semantic-release 6731b655e7 0.107.0
Automatically generated by python-semantic-release
2024-09-06 13:34:20 +00:00
appel_c bd126dddbb refactor: change style to bec_accent_colors 2024-09-06 15:11:56 +02:00
appel_c e6976dc151 docs: extend waveform docs 2024-09-06 12:46:35 +02:00
appel_c b1aff6d791 test: add tests, including extension to end-2-end test 2024-09-06 12:46:35 +02:00
appel_c 7bdca84314 feat: add roi select for dap, allow automatic clear curves on plot request 2024-09-06 12:46:35 +02:00
semantic-release 6b3ea0101e 0.106.0
Automatically generated by python-semantic-release
2024-09-05 12:52:33 +00:00
wyzula_j 06d7741622 feat(plot_base): toggle to switch outer axes for plotting widgets 2024-09-05 14:43:20 +02:00
appel_c 6b15abcc73 test: fix tests 2024-09-04 17:59:36 +02:00
appel_c 998a745133 refactor: use DAPComboBox in curve_dialog selection 2024-09-04 17:18:40 +02:00
semantic-release 3c519461ec 0.105.0
Automatically generated by python-semantic-release
2024-09-04 14:40:53 +00:00
appel_c 0fd5cee776 refactor: cleanup and renaming of slot/signals 2024-09-04 16:31:44 +02:00
appel_c cc691d4039 feat: add dap_combobox 2024-09-04 16:31:44 +02:00
wakonig_k 3a5d7d0796 refactor(logger): changed prints to logger calls 2024-09-04 16:26:13 +02:00
semantic-release 814c823875 0.104.0
Automatically generated by python-semantic-release
2024-09-04 14:25:02 +00:00
wyzula_j 90479167fb fix(scan_control): SafeSlot applied to run_scan to avoid faulty scan requests 2024-09-04 16:15:56 +02:00
wyzula_j 730e25fd3a docs(scan_control): docs extended 2024-09-04 16:15:56 +02:00
wyzula_j b07e67715c test(scan_control): tests extended for getting kwargs between scan switching and getting parameters from redis 2024-09-04 16:15:56 +02:00
wyzula_j 85dcbdaa88 refactor(scan_control): scan control layout adjusted 2024-09-04 16:15:56 +02:00
wyzula_j ec3bc8b519 fix(scan_control): scan parameters can be loaded from the last executed scan from redis 2024-09-04 16:15:56 +02:00
wyzula_j 2cd9c7f585 fix(toggle): state can be determined with the widget initialisation 2024-09-04 16:15:56 +02:00
wyzula_j d28f9b04c4 feat(scan_control): scan control remember the previously set parameters and shares kwarg settings across scans 2024-09-04 16:15:56 +02:00
wyzula_j fe8dc55eb1 refactor(scan_control): basic pydantic config added 2024-09-04 16:15:56 +02:00
wakonig_k 26920f8482 test(conftest): only run cleanup checks if test passed 2024-09-04 14:34:08 +02:00
semantic-release 8a354690c9 0.103.0
Automatically generated by python-semantic-release
2024-09-04 12:33:15 +00:00
wakonig_k d5eb30cd7d test(webview): fixed tests after refactoring 2024-09-04 13:21:59 +02:00
wakonig_k 52da835803 feat(vscode): open vscode on a free port 2024-09-04 13:21:00 +02:00
wakonig_k 9be19d4abe feat(website): added method to wait until the webpage is loaded 2024-09-04 13:21:00 +02:00
wakonig_k 9866075100 fix(theme): fixed segfault for webengineview for auto updates 2024-09-04 13:21:00 +02:00
usov_i 158c19eda7 ci: prefill variables for manual pipeline start 2024-09-04 10:02:57 +02:00
wakonig_k 39f98ec223 test(vscode): popen call does not have to be the only one 2024-09-04 09:42:38 +02:00
semantic-release e12a85feaa 0.102.0
Automatically generated by python-semantic-release
2024-09-04 05:55:14 +00:00
wyzula_j 047aa26a60 docs(buttons): buttons section of docs split to appearance and queue buttons 2024-09-04 07:45:43 +02:00
wyzula_j 9dd43aa1fd fix(queue_reset_button): queue reset has to be confirmed with msgBox 2024-09-04 07:45:43 +02:00
wyzula_j 0d7c10e670 feat(queue): BECQueue controls extended with Resume, Stop, Abort, Reset buttons 2024-09-04 07:45:43 +02:00
wyzula_j df5eff3147 refactor(tests): positioner box test changed to use create_widget fixture 2024-09-03 13:39:30 +02:00
wyzula_j 18d8561c96 docs(tests): added tests tutorial for widget 2024-09-03 13:32:41 +02:00
semantic-release 103410d4c7 0.101.0
Automatically generated by python-semantic-release
2024-09-02 11:58:55 +00:00
appel_c 61ecf491e5 refactor: add docs, cleanup 2024-09-02 13:12:59 +02:00
appel_c 9781b77de2 feat: add Dap dialog widget 2024-09-01 20:57:46 +02:00
semantic-release 162e0ae78b 0.100.0
Automatically generated by python-semantic-release
2024-09-01 08:14:47 +00:00
wakonig_k 99d5e8e71c docs(becwidget): improvements to the bec widget base class docs; fixed type hint import for sphinx 2024-08-31 21:42:08 +02:00
wakonig_k 6c1f89ad39 fix(pyqt slot): removed slot decorator to avoid problems with pyqt6 2024-08-31 14:51:12 +02:00
wakonig_k 7fb938a850 feat(theme): added theme handler to bec widget base class; added tests 2024-08-31 14:32:38 +02:00
semantic-release 08c3d7d175 0.99.15
Automatically generated by python-semantic-release
2024-08-31 09:14:46 +00:00
wakonig_k af23e74f71 fix(theme): update pg axes on theme update 2024-08-31 11:11:13 +02:00
wakonig_k 0bf1cf9b8a fix(positioner_box): fixed positioner box dialog; added test; closes #332 2024-08-31 09:45:10 +02:00
semantic-release 6dd64dd8e1 0.99.14
Automatically generated by python-semantic-release
2024-08-30 14:13:56 +00:00
wyzula_j 99a98de8a3 fix(color_button): signal and slot added for selecting color and for emitting color after change 2024-08-30 16:03:22 +02:00
wyzula_j 3c0e501c56 fix(color_button): inheritance changed to QWidget 2024-08-30 16:03:22 +02:00
semantic-release 9d76d8bf6c 0.99.13
Automatically generated by python-semantic-release
2024-08-30 11:36:36 +00:00
wakonig_k a3110d9814 fix(dark mode button): fixed dark mode button state for external updates, including auto 2024-08-30 10:42:13 +02:00
wakonig_k ec9c8f2963 docs: minor updates to the widget tutorial 2024-08-29 16:43:30 +02:00
wyzula_j b32ced85ff docs(widget tutorial): step by step guide added 2024-08-29 16:43:30 +02:00
semantic-release d0e5643d4f 0.99.12
Automatically generated by python-semantic-release
2024-08-29 13:20:39 +00:00
wyzula_j 2efd48736c fix(toolbar): widget action added 2024-08-29 15:17:32 +02:00
wyzula_j 6ed1efc6af fix(reset_button): reset button added 2024-08-29 15:03:42 +02:00
wyzula_j a568633c32 fix(abort_button): abort button added; some minor fixes 2024-08-29 14:14:32 +02:00
semantic-release 6a919be88f 0.99.11
Automatically generated by python-semantic-release
2024-08-29 11:45:18 +00:00
wyzula_j 8be8295b2b fix(resume_button): resume button added 2024-08-29 13:36:32 +02:00
wyzula_j 5d73fe455a refactor(icons): general app icon changed; jupyter app icon changed to material icon 2024-08-29 13:04:04 +02:00
appel_c 7dadab1f14 refactor: add option to select scan and hide arg bundle buttons 2024-08-29 12:57:40 +02:00
semantic-release 664bbce01d 0.99.10
Automatically generated by python-semantic-release
2024-08-29 09:36:18 +00:00
wyzula_j 097946fd68 refactor(stop_button): stop button changed to QWidget and adapted for toolbar 2024-08-29 11:26:23 +02:00
wyzula_j 4a890281f7 fix(stop_button): queue logic scan changed to halt instead of abort and reset 2024-08-29 10:56:16 +02:00
appel_c cdd175207e refactor: added hide option for device selection button 2024-08-28 22:33:47 +02:00
semantic-release 3210a42e42 0.99.9
Automatically generated by python-semantic-release
2024-08-28 20:29:26 +00:00
wakonig_k 719254cf0a fix: fixed build process and excluded docs and tests from tarballs and wheels 2024-08-28 22:20:34 +02:00
semantic-release 02193967de 0.99.8
Automatically generated by python-semantic-release
2024-08-28 19:33:38 +00:00
wakonig_k 5f37e862c9 fix(website): fixed designer integration for website widget 2024-08-28 21:24:15 +02:00
wakonig_k 9925bbdb48 refactor(website): changed inheritance of website widget to simple qwidget; closes #325 2024-08-28 21:24:15 +02:00
semantic-release 1f7ca4813c 0.99.7
Automatically generated by python-semantic-release
2024-08-28 15:04:54 +00:00
wyzula_j ffc871ebbd fix(toolbar): material icons can accept color as kwarg 2024-08-28 16:16:23 +02:00
semantic-release 7b9a36403d 0.99.6
Automatically generated by python-semantic-release
2024-08-28 13:42:33 +00:00
wyzula_j 09c6c93c39 fix(toolbar): use of native qt separators 2024-08-28 15:33:39 +02:00
wyzula_j c31e9a3aff docs: various bugs fixed 2024-08-28 15:17:31 +02:00
semantic-release 960d84b7fe 0.99.5
Automatically generated by python-semantic-release
2024-08-28 13:09:31 +00:00
wyzula_j e6f204b6aa fix(dock_area): dark button added 2024-08-28 15:06:59 +02:00
wyzula_j 02239de0a3 docs(index): index page is centered 2024-08-28 15:06:08 +02:00
semantic-release 0aad9a0988 0.99.4
Automatically generated by python-semantic-release
2024-08-28 13:05:07 +00:00
wakonig_k c5501860e8 fix(theme): apply theme to all pyqtgraph widgets on manual updates 2024-08-28 14:34:50 +02:00
wyzula_j 4e5520aee2 docs(buttons): added missing buttons docs 2024-08-27 20:58:05 +02:00
wyzula_j 4591ba8f73 refactor(buttons): changed grid and thumbnail fig in gallery 2024-08-27 20:58:05 +02:00
wakonig_k f335763280 refactor(icons): removed toolbar icons from assets 2024-08-27 18:38:08 +02:00
wakonig_k e890091d86 refactor(icons): moved widget icons to class attribute ICON_NAME 2024-08-27 18:38:08 +02:00
wyzula_j ac2cb5197d docs(developer): tutorial for BECWidget base class 2024-08-27 18:18:10 +02:00
semantic-release 65345187b3 0.99.3
Automatically generated by python-semantic-release
2024-08-27 13:49:04 +00:00
wakonig_k d48243483e build: updated min version of bec qthemes 2024-08-27 13:09:21 +02:00
wakonig_k 1ca9499edd fix(cmaps): unified all defaults to magma cmap 2024-08-27 13:09:21 +02:00
wakonig_k 060935ffc5 fix(color maps): color maps should take the background color into account; fixed min colors to 10 2024-08-27 12:36:02 +02:00
semantic-release 50dbef52c0 0.99.2
Automatically generated by python-semantic-release
2024-08-27 09:03:23 +00:00
wakonig_k bb385f07ca ci: additional tests are not allowed to fail 2024-08-27 10:54:46 +02:00
wakonig_k cf28730515 fix(widgets): fixed default theme for widgets
If not theme is set, the init of the BECWidget base class sets the default theme to "dark"
2024-08-27 10:54:46 +02:00
semantic-release 13ae383455 0.99.1
Automatically generated by python-semantic-release
2024-08-27 07:28:47 +00:00
wakonig_k 2265458dcc fix(crosshair): emit all crosshair events, not just line coordinates 2024-08-26 14:10:46 +02:00
semantic-release 0a59f08fcc 0.99.0
Automatically generated by python-semantic-release
2024-08-25 11:49:52 +00:00
wakonig_k c70724a456 refactor(darkmodebutton): renamed set_dark_mode_enabled to toggle_dark_mode 2024-08-25 13:45:56 +02:00
wakonig_k 406c263746 docs(darkmodebutton): added dark mode button docs 2024-08-25 13:45:56 +02:00
wakonig_k df35aabff3 test(dark_mode_button): added tests for dark mode button 2024-08-25 13:45:56 +02:00
wakonig_k cc8c166b5c feat(darkmodebutton): added button to toggle between dark and light mode 2024-08-25 13:45:56 +02:00
wakonig_k c4f3308dc0 fix(toggle): emit state change 2024-08-25 13:45:56 +02:00
semantic-release 8f3824c0e7 0.98.0
Automatically generated by python-semantic-release
2024-08-25 11:45:36 +00:00
wakonig_k afdf4e8782 fix(toolbar): removed hardcoded color values 2024-08-23 23:00:49 +02:00
wakonig_k 2a82032644 fix: transitioning to material icons 2024-08-23 22:40:21 +02:00
wakonig_k 88a2f66758 fix(dock_area): transitioned to MaterialIconAction 2024-08-23 22:05:56 +02:00
wakonig_k 3f3b207295 fix: fix color palette if qtheme was not called 2024-08-23 20:14:53 +02:00
wakonig_k 44cfda1c07 refactor(waveform): use set theme for demo 2024-08-23 20:04:44 +02:00
wakonig_k e42b84c636 fix(figure): removed theme from figure init 2024-08-23 20:04:44 +02:00
wakonig_k 77c5aa741c fix: use globally set theme instead of the internal bec widgets theme 2024-08-23 20:04:44 +02:00
wakonig_k 2b4449afeb feat(themes): added set_theme method 2024-08-23 20:04:44 +02:00
wakonig_k 36ad464159 fix(waveform): fixed icon appearance 2024-08-23 20:04:44 +02:00
semantic-release e8ae6f2e43 0.97.0
Automatically generated by python-semantic-release
2024-08-23 13:06:31 +00:00
wakonig_k 3ecbd60627 fix(toolbar icon): fixed material icon toolbar for theme changes 2024-08-23 14:14:40 +02:00
wakonig_k 82a55ddf3e feat(designer): added designer icon factory 2024-08-23 14:12:33 +02:00
semantic-release 7d190719b1 0.96.3
Automatically generated by python-semantic-release
2024-08-23 07:47:59 +00:00
wakonig_k 8c2e7c8259 fix: minor fixes for type annotations 2024-08-22 20:44:28 +02:00
wyzula_j dd7c71bb1e docs(dispatcher): docs added 2024-08-22 14:52:52 +02:00
semantic-release 7b5b7a8cbb 0.96.2
Automatically generated by python-semantic-release
2024-08-22 09:49:04 +00:00
wyzula_j af28574bd5 fix(waveform): validation of custom curves removed 2024-08-22 11:35:27 +02:00
wakonig_k 617db36ed4 fix(waveform): skip validation for curves that are not BECCurve instances 2024-08-22 10:55:49 +02:00
semantic-release ebc2e44c7c 0.96.1
Automatically generated by python-semantic-release
2024-08-22 08:41:33 +00:00
wakonig_k 44738057a3 fix(crosshair): update markers if necessary 2024-08-22 10:32:35 +02:00
wakonig_k f98a9f9771 fix(waveform_widget): fixed icon appearance 2024-08-22 10:32:35 +02:00
wakonig_k 2fe72c9ccb fix: bubble-up signals 2024-08-22 10:32:35 +02:00
wakonig_k f0203d9bf6 ci: fail pytest after 2 failed tests 2024-08-22 10:32:35 +02:00
wyzula_j 37835cbf76 fix(crosshair): fixed crosshair for image and waveforms 2024-08-22 10:32:35 +02:00
semantic-release e005be33d1 0.96.0
Automatically generated by python-semantic-release
2024-08-22 07:50:58 +00:00
wakonig_k 9d7718c3d9 docs(scan_control): added designer options 2024-08-22 09:42:00 +02:00
wakonig_k 9d8fb0b761 feat(scan_control): added the ability to configure the scan control widget from designer 2024-08-22 09:42:00 +02:00
semantic-release 9df1e0899b 0.95.1
Automatically generated by python-semantic-release
2024-08-22 07:36:54 +00:00
wyzula_j 640464a654 fix(docs): changed link to scan gui config in main docs 2024-08-21 21:46:44 +02:00
wakonig_k 84abe46050 refactor: removed designer pngs 2024-08-21 21:28:32 +02:00
wakonig_k 1d2afaa09e refactor: moved to dynamically loaded material design icons 2024-08-21 21:28:32 +02:00
wyzula_j 2bf5c7096e docs: links section added 2024-08-21 21:07:50 +02:00
894 changed files with 120607 additions and 20093 deletions
+41
View File
@@ -0,0 +1,41 @@
name: Bug report
description: File a bug report.
title: "[BUG]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Bug report:
- type: textarea
id: description
attributes:
label: Provide a brief description of the bug.
- type: textarea
id: expected
attributes:
label: Describe what you expected to happen and what actually happened.
- type: textarea
id: reproduction
attributes:
label: Outline the steps that lead to the bug's occurrence. Be specific and provide a clear sequence of actions.
- type: input
id: version
attributes:
label: bec_widgets version
description: which version of BEC widgets was running?
- type: input
id: bec-version
attributes:
label: bec core version
description: which version of BEC core was running?
- type: textarea
id: extra
attributes:
label: Any extra info / data? e.g. log output...
- type: input
id: issues
attributes:
label: Related issues
description: please tag any related issues
@@ -1,3 +1,13 @@
---
name: Documentation update request
about: Suggest an update to the docs
title: '[DOCS]: '
type: documentation
label: documentation
assignees: ''
---
## Documentation Section
[Specify the section or page of the documentation that needs updating]
@@ -1,3 +1,13 @@
---
name: Feature request
about: Suggest an idea for this project
title: '[FEAT]: '
type: feature
label: feature
assignees: ''
---
## Feature Summary
[Provide a brief and clear summary of the new feature you are requesting]
@@ -37,4 +47,3 @@
## Additional Information
[Provide any additional information that might be relevant to the feature request, such as user feedback, market trends, or similar features in other products]
+65
View File
@@ -0,0 +1,65 @@
name: "BEC Widgets Install"
description: "Install BEC Widgets and related os dependencies"
inputs:
BEC_WIDGETS_BRANCH: # id of input
required: false
default: "main"
description: "Branch of BEC Widgets to install"
BEC_CORE_BRANCH: # id of input
required: false
default: "main"
description: "Branch of BEC Core to install"
OPHYD_DEVICES_BRANCH: # id of input
required: false
default: "main"
description: "Branch of Ophyd Devices to install"
PYTHON_VERSION: # id of input
required: false
default: "3.11"
description: "Python version to use"
runs:
using: "composite"
steps:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.PYTHON_VERSION }}
- name: Checkout BEC Core
uses: actions/checkout@v4
with:
repository: bec-project/bec
ref: ${{ inputs.BEC_CORE_BRANCH }}
path: ./bec
- name: Checkout Ophyd Devices
uses: actions/checkout@v4
with:
repository: bec-project/ophyd_devices
ref: ${{ inputs.OPHYD_DEVICES_BRANCH }}
path: ./ophyd_devices
- name: Checkout BEC Widgets
uses: actions/checkout@v4
with:
repository: bec-project/bec_widgets
ref: ${{ inputs.BEC_WIDGETS_BRANCH }}
path: ./bec_widgets
- name: Install dependencies
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y libgl1 libegl1 x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
sudo apt-get -y install libnss3 libxdamage1 libasound2t64 libatomic1 libxcursor1
sudo apt-get -y install ttyd
- name: Install Python dependencies
shell: bash
run: |
pip install uv
uv pip install --system -e ./ophyd_devices
uv pip install --system -e ./bec/bec_lib[dev]
uv pip install --system -e ./bec/bec_ipython_client
uv pip install --system -e ./bec_widgets[dev,pyside6]
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
@@ -1,19 +1,24 @@
## Description
[Provide a brief description of the changes introduced by this merge request.]
[Provide a brief description of the changes introduced by this pull request.]
## Related Issues
[Cite any related issues or feature requests that are addressed or resolved by this merge request. Use the gitlab syntax for linking issues, for example, `fixes #123` or `closes #123`.]
[Cite any related issues or feature requests that are addressed or resolved by this pull request. Link the associated issue, for example, with `fixes #123` or `closes #123`.]
## Type of Change
- Change 1
- Change 2
## How to test
- Run unit tests
- Open [widget] in designer and play around with the properties
## Potential side effects
[Describe any potential side effects or risks of merging this MR.]
[Describe any potential side effects or risks of merging this PR.]
## Screenshots / GIFs (if applicable)
+28
View File
@@ -0,0 +1,28 @@
name: Check PR status for branch
on:
workflow_call:
outputs:
branch-pr:
description: The PR number if the branch is in one
value: ${{ jobs.pr.outputs.branch-pr }}
jobs:
pr:
runs-on: "ubuntu-latest"
outputs:
branch-pr: ${{ steps.script.outputs.result }}
steps:
- uses: actions/github-script@v7
id: script
if: github.event_name == 'push' && github.event.ref_type != 'tag'
with:
script: |
const prs = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
head: context.repo.owner + ':${{ github.ref_name }}'
})
if (prs.data.length) {
console.log(`::notice ::Skipping CI on branch push as it is already run in PR #${prs.data[0]["number"]}`)
return prs.data[0]["number"]
}
+64
View File
@@ -0,0 +1,64 @@
name: Run Pytest with Coverage
on:
workflow_call:
inputs:
BEC_CORE_BRANCH:
description: 'Branch for BEC Core'
required: false
default: 'main'
type: string
OPHYD_DEVICES_BRANCH:
description: 'Branch for Ophyd Devices'
required: false
default: 'main'
type: string
BEC_WIDGETS_BRANCH:
description: 'Branch for BEC Widgets'
required: false
default: 'main'
type: string
jobs:
bec:
name: BEC Unit Tests
runs-on: ubuntu-latest
defaults:
run:
shell: bash -el {0}
steps:
- name: Checkout BEC
uses: actions/checkout@v4
with:
repository: bec-project/bec
ref: ${{ inputs.BEC_CORE_BRANCH }}
- name: Install BEC and dependencies
uses: ./.github/actions/bec_install
with:
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH }}
OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH }}
PYTHON_VERSION: '3.11'
- name: Run Pytest
run: |
cd ./bec
pip install pytest pytest-random-order
pytest -v --maxfail=2 --junitxml=report.xml --random-order ./bec_server/tests ./bec_ipython_client/tests/client_tests ./bec_lib/tests
bec-e2e-test:
name: BEC End2End Tests
runs-on: ubuntu-latest
steps:
- name: Checkout BEC
uses: actions/checkout@v4
with:
repository: bec-project/bec
ref: ${{ inputs.BEC_CORE_BRANCH }}
- name: Run E2E Tests
uses: ./.github/actions/bec_e2e_install
with:
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH }}
OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH }}
BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH }}
PYTHON_VERSION: '3.11'
+84
View File
@@ -0,0 +1,84 @@
name: Full CI
on:
push:
pull_request:
workflow_dispatch:
inputs:
BEC_WIDGETS_BRANCH:
description: 'Branch of BEC Widgets to install'
required: false
type: string
BEC_CORE_BRANCH:
description: 'Branch of BEC Core to install'
required: false
type: string
OPHYD_DEVICES_BRANCH:
description: 'Branch of Ophyd Devices to install'
required: false
type: string
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
pull-requests: write
jobs:
check_pr_status:
uses: ./.github/workflows/check_pr.yml
formatter:
needs: check_pr_status
if: needs.check_pr_status.outputs.branch-pr == ''
uses: ./.github/workflows/formatter.yml
unit-test:
needs: [check_pr_status, formatter]
if: needs.check_pr_status.outputs.branch-pr == ''
uses: ./.github/workflows/pytest.yml
with:
BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH || github.head_ref }}
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }}
OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH || 'main' }}
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
unit-test-matrix:
needs: [check_pr_status, formatter]
if: needs.check_pr_status.outputs.branch-pr == ''
uses: ./.github/workflows/pytest-matrix.yml
with:
BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH || github.head_ref || github.sha}}
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }}
OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH || 'main' }}
generate-cli-test:
needs: [check_pr_status, formatter]
if: needs.check_pr_status.outputs.branch-pr == ''
uses: ./.github/workflows/generate-cli-check.yml
end2end-test:
needs: [check_pr_status, formatter]
if: needs.check_pr_status.outputs.branch-pr == ''
uses: ./.github/workflows/end2end-conda.yml
child-repos:
needs: [check_pr_status, formatter]
if: needs.check_pr_status.outputs.branch-pr == ''
uses: ./.github/workflows/child_repos.yml
with:
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }}
OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH || 'main'}}
BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH || github.head_ref || github.sha }}
plugin_repos:
needs: [check_pr_status, formatter]
if: needs.check_pr_status.outputs.branch-pr == ''
uses: bec-project/bec/.github/workflows/plugin_repos.yml@main
with:
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }}
BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH || github.head_ref || github.sha }}
secrets:
GH_READ_TOKEN: ${{ secrets.GH_READ_TOKEN }}
+59
View File
@@ -0,0 +1,59 @@
name: Run Pytest with Coverage
on: [workflow_call]
jobs:
pytest:
runs-on: ubuntu-latest
defaults:
run:
shell: bash -el {0}
env:
CHILD_PIPELINE_BRANCH: main # Set the branch you want for ophyd_devices
BEC_CORE_BRANCH: main # Set the branch you want for bec
OPHYD_DEVICES_BRANCH: main # Set the branch you want for ophyd_devices
PLUGIN_REPO_BRANCH: main # Set the branch you want for the plugin repo
PROJECT_PATH: ${{ github.repository }}
QTWEBENGINE_DISABLE_SANDBOX: 1
QT_QPA_PLATFORM: "offscreen"
steps:
- uses: actions/checkout@v4
- name: Set up Conda
uses: conda-incubator/setup-miniconda@v3
with:
auto-update-conda: true
auto-activate-base: true
python-version: "3.11"
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgl1 libegl1 x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
sudo apt-get -y install libnss3 libxdamage1 libasound2t64 libatomic1 libxcursor1
sudo apt-get -y install ttyd
- name: Conda install and run pytest
run: |
echo -e "\033[35;1m Using branch $BEC_CORE_BRANCH of BEC CORE \033[0;m";
git clone --branch $BEC_CORE_BRANCH https://github.com/bec-project/bec.git
echo -e "\033[35;1m Using branch $OPHYD_DEVICES_BRANCH of OPHYD_DEVICES \033[0;m";
git clone --branch $OPHYD_DEVICES_BRANCH https://github.com/bec-project/ophyd_devices.git
export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
echo -e "\033[35;1m Using branch $PLUGIN_REPO_BRANCH of bec_testing_plugin \033[0;m";
git clone --branch $PLUGIN_REPO_BRANCH https://github.com/bec-project/bec_testing_plugin.git
cd ./bec
conda create -q -n test-environment python=3.11
source ./bin/install_bec_dev.sh -t
cd ../
pip install -e ./ophyd_devices -e .[dev,pyside6] -e ./bec_testing_plugin
pytest -v --files-path ./ --start-servers --random-order ./tests/end-2-end
- name: Upload logs if job fails
if: failure()
uses: actions/upload-artifact@v4
with:
name: pytest-logs
path: ./bec/logs/*.log
retention-days: 7
+66
View File
@@ -0,0 +1,66 @@
name: Formatter and Pylint jobs
on: [workflow_call]
jobs:
Formatter:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Run black and isort
run: |
pip install uv
uv pip install --system black isort
uv pip install --system -e .[dev]
black --check --diff --color .
isort --check --diff ./
- name: Check for disallowed imports from PySide
run: '! grep -re "from PySide6\." bec_widgets/ tests/ | grep -v -e "PySide6.QtDesigner" -e "PySide6.scripts"'
Pylint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pylint pylint-exit anybadge
- name: Run Pylint
run: |
mkdir -p ./pylint
set +e
pylint ./${{ github.event.repository.name }} --output-format=text > ./pylint/pylint.log
pylint-exit $?
set -e
- name: Extract Pylint Score
id: score
run: |
SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint.log)
echo "score=$SCORE" >> $GITHUB_OUTPUT
- name: Create Badge
run: |
anybadge --label=Pylint --file=./pylint/pylint.svg --value="${{ steps.score.outputs.score }}" 2=red 4=orange 8=yellow 10=green
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: pylint-artifacts
path: |
# ./pylint/pylint.log # not sure why this isn't working
./pylint/pylint.svg
+49
View File
@@ -0,0 +1,49 @@
name: Run bw-generate-cli
on: [workflow_call]
jobs:
pytest:
runs-on: ubuntu-latest
defaults:
run:
shell: bash -el {0}
env:
CHILD_PIPELINE_BRANCH: main # Set the branch you want for ophyd_devices
BEC_CORE_BRANCH: main # Set the branch you want for bec
OPHYD_DEVICES_BRANCH: main # Set the branch you want for ophyd_devices
PROJECT_PATH: ${{ github.repository }}
QTWEBENGINE_DISABLE_SANDBOX: 1
QT_QPA_PLATFORM: "offscreen"
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install os dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgl1 libegl1 x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
sudo apt-get -y install libnss3 libxdamage1 libasound2t64 libatomic1 libxcursor1
- name: Clone and install dependencies
run: |
echo -e "\033[35;1m Using branch $BEC_CORE_BRANCH of BEC CORE \033[0;m";
git clone --branch $BEC_CORE_BRANCH https://github.com/bec-project/bec.git
echo -e "\033[35;1m Using branch $OPHYD_DEVICES_BRANCH of OPHYD_DEVICES \033[0;m";
git clone --branch $OPHYD_DEVICES_BRANCH https://github.com/bec-project/ophyd_devices.git
export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
pip install -e ./ophyd_devices
pip install -e ./bec/bec_lib[dev]
pip install -e ./bec/bec_ipython_client
pip install -e .[dev,pyside6]
- name: Run bw-generate-cli
run: |
bw-generate-cli --target bec_widgets
git diff --exit-code
+59
View File
@@ -0,0 +1,59 @@
name: Run Pytest with different Python versions
on:
workflow_call:
inputs:
pr_number:
description: 'Pull request number'
required: false
type: number
BEC_CORE_BRANCH:
description: 'Branch of BEC Core to install'
required: false
default: 'main'
type: string
OPHYD_DEVICES_BRANCH:
description: 'Branch of Ophyd Devices to install'
required: false
default: 'main'
type: string
BEC_WIDGETS_BRANCH:
description: 'Branch of BEC Widgets to install'
required: false
default: 'main'
type: string
jobs:
pytest-matrix:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
env:
BEC_WIDGETS_BRANCH: main # Set the branch you want for bec_widgets
BEC_CORE_BRANCH: main # Set the branch you want for bec
OPHYD_DEVICES_BRANCH: main # Set the branch you want for ophyd_devices
PROJECT_PATH: ${{ github.repository }}
QTWEBENGINE_DISABLE_SANDBOX: 1
QT_QPA_PLATFORM: "offscreen"
steps:
- name: Checkout BEC Widgets
uses: actions/checkout@v4
with:
repository: bec-project/bec_widgets
ref: ${{ inputs.BEC_WIDGETS_BRANCH }}
- name: Install BEC Widgets and dependencies
uses: ./.github/actions/bw_install
with:
BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH }}
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH }}
OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH }}
PYTHON_VERSION: ${{ matrix.python-version }}
- name: Run Pytest
run: |
pip install pytest pytest-random-order
pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
+72
View File
@@ -0,0 +1,72 @@
name: Run Pytest with Coverage
on:
workflow_call:
inputs:
pr_number:
description: 'Pull request number'
required: false
type: number
BEC_CORE_BRANCH:
description: 'Branch of BEC Core to install'
required: false
default: 'main'
type: string
OPHYD_DEVICES_BRANCH:
description: 'Branch of Ophyd Devices to install'
required: false
default: 'main'
type: string
BEC_WIDGETS_BRANCH:
description: 'Branch of BEC Widgets to install'
required: false
default: 'main'
type: string
secrets:
CODECOV_TOKEN:
required: true
permissions:
pull-requests: write
jobs:
pytest:
runs-on: ubuntu-latest
env:
QTWEBENGINE_DISABLE_SANDBOX: 1
QT_QPA_PLATFORM: "offscreen"
steps:
- name: Checkout BEC Widgets
uses: actions/checkout@v4
with:
repository: bec-project/bec_widgets
ref: ${{ inputs.BEC_WIDGETS_BRANCH }}
- name: Install BEC Widgets and dependencies
uses: ./.github/actions/bw_install
with:
BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH }}
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH }}
OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH }}
PYTHON_VERSION: 3.11
- name: Run Pytest with Coverage
id: coverage
run: pytest --random-order --cov=bec_widgets --cov-config=pyproject.toml --cov-branch --cov-report=xml --no-cov-on-fail tests/unit_tests/
- name: Upload test artifacts
uses: actions/upload-artifact@v4
if: failure()
with:
name: image-references
path: bec_widgets/tests/reference_failures/
if-no-files-found: ignore
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: bec-project/bec_widgets
+103
View File
@@ -0,0 +1,103 @@
name: Continuous Delivery
on:
push:
branches:
- main
# default: least privileged permissions across all jobs
permissions:
contents: read
jobs:
release:
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-release-${{ github.ref_name }}
cancel-in-progress: false
env:
CHILD_PIPELINE_BRANCH: main # Set the branch you want for ophyd_devices
BEC_CORE_BRANCH: main # Set the branch you want for bec
OPHYD_DEVICES_BRANCH: main # Set the branch you want for ophyd_devices
PROJECT_PATH: ${{ github.repository }}
QTWEBENGINE_DISABLE_SANDBOX: 1
QT_QPA_PLATFORM: "offscreen"
permissions:
contents: write
steps:
# Note: We checkout the repository at the branch that triggered the workflow
# with the entire history to ensure to match PSR's release branch detection
# and history evaluation.
# However, we forcefully reset the branch to the workflow sha because it is
# possible that the branch was updated while the workflow was running. This
# prevents accidentally releasing un-evaluated changes.
- name: Setup | Checkout Repository on Release Branch
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
fetch-depth: 0
ssh-key: ${{ secrets.CI_DEPLOY_SSH_KEY }}
ssh-known-hosts: ${{ secrets.CI_DEPLOY_SSH_KNOWN_HOSTS }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Setup | Force release branch to be at workflow sha
run: |
git reset --hard ${{ github.sha }}
- name: Evaluate | Verify upstream has NOT changed
# Last chance to abort before causing an error as another PR/push was applied to
# the upstream branch while this workflow was running. This is important
# because we are committing a version change (--commit). You may omit this step
# if you have 'commit: false' in your configuration.
#
# You may consider moving this to a repo script and call it from this step instead
# of writing it in-line.
shell: bash
run: |
set +o pipefail
UPSTREAM_BRANCH_NAME="$(git status -sb | head -n 1 | cut -d' ' -f2 | grep -E '\.{3}' | cut -d'.' -f4)"
printf '%s\n' "Upstream branch name: $UPSTREAM_BRANCH_NAME"
set -o pipefail
if [ -z "$UPSTREAM_BRANCH_NAME" ]; then
printf >&2 '%s\n' "::error::Unable to determine upstream branch name!"
exit 1
fi
git fetch "${UPSTREAM_BRANCH_NAME%%/*}"
if ! UPSTREAM_SHA="$(git rev-parse "$UPSTREAM_BRANCH_NAME")"; then
printf >&2 '%s\n' "::error::Unable to determine upstream branch sha!"
exit 1
fi
HEAD_SHA="$(git rev-parse HEAD)"
if [ "$HEAD_SHA" != "$UPSTREAM_SHA" ]; then
printf >&2 '%s\n' "[HEAD SHA] $HEAD_SHA != $UPSTREAM_SHA [UPSTREAM SHA]"
printf >&2 '%s\n' "::error::Upstream has changed, aborting release..."
exit 1
fi
printf '%s\n' "Verified upstream branch has not changed, continuing with release..."
- name: Semantic Version Release
id: release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pip install python-semantic-release==9.* wheel build twine
semantic-release -vv version
if [ ! -d dist ]; then echo No release will be made; exit 0; fi
twine upload dist/* -u __token__ -p ${{ secrets.CI_PYPI_TOKEN }} --skip-existing
semantic-release publish
+19
View File
@@ -0,0 +1,19 @@
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '00 10 * * *'
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
stale-issue-message: 'This issue is stale because it has been open 120 days with no activity. Remove stale label or comment or this will be closed in 14 days.'
stale-pr-message: 'This PR is stale because it has been open 120 days with no activity. Remove stale label or comment or this will be closed in 14 days.'
days-before-stale: 120
days-before-close: 14
+35
View File
@@ -0,0 +1,35 @@
name: Sync PR to Project
on:
pull_request:
types:
[
opened,
assigned,
unassigned,
edited,
ready_for_review,
converted_to_draft,
reopened,
synchronize,
closed,
]
jobs:
sync-project:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: read
contents: read
steps:
- name: Sync PR to Project
uses: bec-project/action-issue-sync-pr@v1
with:
token: ${{ secrets.ADD_ISSUE_TO_PROJECT }}
org: ${{ github.repository_owner }}
repo: ${{ github.event.repository.name }}
project-number: 3
pr-number: ${{ github.event.pull_request.number }}
+3
View File
@@ -64,6 +64,9 @@ coverage.xml
.pytest_cache/
cover/
# Output from end2end testing
tests/reference_failures/
# Translations
*.mo
*.pot
-263
View File
@@ -1,263 +0,0 @@
# This file is a template, and might need editing before it works on your project.
# Official language image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/python/tags/
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.11
#commands to run in the Docker container before starting each job.
variables:
DOCKER_TLS_CERTDIR: ""
BEC_CORE_BRANCH: "main"
OPHYD_DEVICES_BRANCH: "main"
CHILD_PIPELINE_BRANCH: $CI_DEFAULT_BRANCH
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
- if: $CI_PIPELINE_SOURCE == "web"
- if: $CI_PIPELINE_SOURCE == "pipeline"
- if: $CI_PIPELINE_SOURCE == "parent_pipeline"
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH
include:
- template: Security/Secret-Detection.gitlab-ci.yml
- project: "bec/awi_utils"
file: "/templates/check-packages-job.yml"
inputs:
stage: test
path: "."
pytest_args: "-v --random-order tests/"
exclude_packages: ""
# different stages in the pipeline
stages:
- Formatter
- test
- AdditionalTests
- End2End
- Deploy
.install-qt-webengine-deps: &install-qt-webengine-deps
- apt-get -y install libnss3 libxdamage1 libasound2 libatomic1 libxcursor1
- export QTWEBENGINE_DISABLE_SANDBOX=1
.clone-repos: &clone-repos
- echo -e "\033[35;1m Using branch $BEC_CORE_BRANCH of BEC CORE \033[0;m";
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
- echo -e "\033[35;1m Using branch $OPHYD_DEVICES_BRANCH of OPHYD_DEVICES \033[0;m";
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
.install-repos: &install-repos
- pip install -e ./ophyd_devices
- pip install -e ./bec/bec_lib[dev]
- pip install -e ./bec/bec_ipython_client
.install-os-packages: &install-os-packages
- apt-get update
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
- *install-qt-webengine-deps
before_script:
- if [[ "$CI_PROJECT_PATH" != "bec/bec_widgets" ]]; then
echo -e "\033[35;1m Using branch $CHILD_PIPELINE_BRANCH of BEC Widgets \033[0;m";
test -d bec_widgets || git clone --branch $CHILD_PIPELINE_BRANCH https://gitlab.psi.ch/bec/bec_widgets.git; cd bec_widgets;
fi
formatter:
stage: Formatter
needs: []
script:
- pip install black isort
- isort --check --diff ./
- black --check --diff --color ./
rules:
- if: $CI_PROJECT_PATH == "bec/bec_widgets"
pylint:
stage: Formatter
needs: []
before_script:
- pip install pylint pylint-exit anybadge
- pip install -e .[dev,pyqt6]
script:
- mkdir ./pylint
- pylint ./bec_widgets --output-format=text --output=./pylint/pylint.log | tee ./pylint/pylint.log || pylint-exit $?
- PYLINT_SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint.log)
- anybadge --label=Pylint --file=pylint/pylint.svg --value=$PYLINT_SCORE 2=red 4=orange 8=yellow 10=green
- echo "Pylint score is $PYLINT_SCORE"
artifacts:
paths:
- ./pylint/
expire_in: 1 week
rules:
- if: $CI_PROJECT_PATH == "bec/bec_widgets"
pylint-check:
stage: Formatter
needs: []
allow_failure: true
before_script:
- pip install pylint pylint-exit anybadge
- apt-get update
- apt-get install -y bc
script:
- git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
# Identify changed Python files
- if [ "$CI_PIPELINE_SOURCE" == "merge_request_event" ]; then
TARGET_BRANCH_COMMIT_SHA=$(git rev-parse origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME);
CHANGED_FILES=$(git diff --name-only $TARGET_BRANCH_COMMIT_SHA HEAD | grep '\.py$' || true);
else
CHANGED_FILES=$(git diff --name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA | grep '\.py$' || true);
fi
- if [ -z "$CHANGED_FILES" ]; then echo "No Python files changed."; exit 0; fi
- echo "Changed Python files:"
- $CHANGED_FILES
# Run pylint only on changed files
- mkdir ./pylint
- pylint $CHANGED_FILES --output-format=text | tee ./pylint/pylint_changed_files.log || pylint-exit $?
- PYLINT_SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint_changed_files.log)
- echo "Pylint score is $PYLINT_SCORE"
# Fail the job if the pylint score is below 9
- if [ "$(echo "$PYLINT_SCORE < 9" | bc)" -eq 1 ]; then echo "Your pylint score is below the acceptable threshold (9)."; exit 1; fi
artifacts:
paths:
- ./pylint/
expire_in: 1 week
rules:
- if: $CI_PROJECT_PATH == "bec/bec_widgets"
tests:
stage: test
needs: []
variables:
QT_QPA_PLATFORM: "offscreen"
script:
- *clone-repos
- *install-os-packages
- *install-repos
- pip install -e .[dev,pyqt6]
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
artifacts:
reports:
junit: report.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
paths:
- tests/reference_failures/
when: always
test-matrix:
parallel:
matrix:
- PYTHON_VERSION:
- "3.10"
- "3.11"
- "3.12"
QT_PCKG:
- "pyside6"
- "pyqt6"
stage: AdditionalTests
needs: []
variables:
QT_QPA_PLATFORM: "offscreen"
PYTHON_VERSION: ""
QT_PCKG: ""
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:$PYTHON_VERSION
script:
- *clone-repos
- *install-os-packages
- *install-repos
- pip install -e .[dev,$QT_PCKG]
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
allow_failure: true
end-2-end-conda:
stage: End2End
needs: []
image: continuumio/miniconda3
allow_failure: false
variables:
QT_QPA_PLATFORM: "offscreen"
script:
- *clone-repos
- *install-os-packages
- conda config --prepend channels conda-forge
- conda config --set channel_priority strict
- conda config --set always_yes yes --set changeps1 no
- conda create -q -n test-environment python=3.11
- conda init bash
- source ~/.bashrc
- conda activate test-environment
- cd ./bec
- source ./bin/install_bec_dev.sh -t
- cd ../
- pip install -e ./ophyd_devices
- pip install -e .[dev,pyqt6]
- cd ./tests/end-2-end
- pytest -v --start-servers --flush-redis --random-order
artifacts:
when: on_failure
paths:
- ./logs/*.log
expire_in: 1 week
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
- if: '$CI_PIPELINE_SOURCE == "web"'
- if: '$CI_PIPELINE_SOURCE == "pipeline"'
- if: '$CI_PIPELINE_SOURCE == "parent_pipeline"'
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "production"'
semver:
stage: Deploy
needs: ["tests"]
script:
- git config --global user.name "ci_update_bot"
- git config --global user.email "ci_update_bot@bec.ch"
- git checkout "$CI_COMMIT_REF_NAME"
- git reset --hard origin/"$CI_COMMIT_REF_NAME"
# delete all local tags
- git tag -l | xargs git tag -d
- git fetch --tags
- git tag
# build and publish package
- pip install python-semantic-release==9.* wheel build twine
- export GL_TOKEN=$CI_UPDATES
- semantic-release -vv version
# check if any artifacts were created
- if [ ! -d dist ]; then echo No release will be made; exit 0; fi
- twine upload dist/* -u __token__ -p $CI_PYPI_TOKEN --skip-existing
- semantic-release publish
allow_failure: false
rules:
- if: '$CI_COMMIT_REF_NAME == "main" && $CI_PROJECT_PATH == "bec/bec_widgets"'
pages:
stage: Deploy
needs: ["semver"]
variables:
TARGET_BRANCH: $CI_COMMIT_REF_NAME
rules:
- if: "$CI_COMMIT_TAG != null"
variables:
TARGET_BRANCH: $CI_COMMIT_TAG
- if: '$CI_COMMIT_REF_NAME == "main" && $CI_PROJECT_PATH == "bec/bec_widgets"'
script:
- curl -X POST -d "branches=$CI_COMMIT_REF_NAME" -d "token=$RTD_TOKEN" https://readthedocs.org/api/v2/webhook/bec-widgets/253243/
@@ -1,17 +0,0 @@
## Bug report
## Summary
[Provide a brief description of the bug.]
## Expected Behavior vs Actual Behavior
[Describe what you expected to happen and what actually happened.]
## Steps to Reproduce
[Outline the steps that lead to the bug's occurrence. Be specific and provide a clear sequence of actions.]
## Related Issues
[Paste links to any related issues or feature requests.]
+1 -1
View File
@@ -52,7 +52,7 @@ persistent=yes
# Minimum Python version to use for version dependent checks. Will default to
# the version used to run pylint.
py-version=3.10
py-version=3.11
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
+7 -5
View File
@@ -7,13 +7,13 @@ version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-20.04
os: ubuntu-22.04
tools:
python: "3.10"
python: "3.11"
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
configuration: docs/conf.py
# If using Sphinx, optionally build your docs in additional formats such as PDF
# formats:
@@ -21,5 +21,7 @@ sphinx:
# Optionally declare the Python requirements required to build your docs
python:
install:
- requirements: docs/requirements.txt
install:
- requirements: docs/requirements.txt
- method: pip
path: .[dev]
+9709 -64
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
BSD 3-Clause License
Copyright (c) 2023, bec
Copyright (c) 2025, Paul Scherrer Institute
All rights reserved.
Redistribution and use in source and binary forms, with or without
+176 -50
View File
@@ -1,74 +1,200 @@
![banner_opti](https://github.com/user-attachments/assets/44e483be-3f0d-4eb0-bd98-613157456b81)
# BEC Widgets
BEC Widgets is a GUI framework designed for interaction with [BEC (Beamline Experiment Control)](https://gitlab.psi.ch/bec/bec).
[![CI](https://github.com/bec-project/bec_widgets/actions/workflows/ci.yml/badge.svg)](https://github.com/bec-project/bec_widgets/actions/workflows/ci.yml)
[![badge](https://img.shields.io/pypi/v/bec-widgets)](https://pypi.org/project/bec-widgets/)
[![License](https://img.shields.io/github/license/bec-project/bec_widgets)](./LICENSE)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue?logo=python&logoColor=white)](https://www.python.org)
[![PySide6](https://img.shields.io/badge/PySide6-blue?logo=qt&logoColor=white)](https://doc.qt.io/qtforpython/)
[![Conventional Commits](https://img.shields.io/badge/conventional%20commits-1.0.0-yellow?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org)
[![codecov](https://codecov.io/gh/bec-project/bec_widgets/graph/badge.svg?token=0Z9IQRJKMY)](https://codecov.io/gh/bec-project/bec_widgets)
A modular PySide6(Qt6) toolkit for [BEC (Beamline Experiment Control)](https://github.com/bec-project/bec). Create
high-performance, dockable GUIs to move devices, run scans, and stream live or disk data—powered by Redis and a modular
plugin system.
## Highlights
- **No-code first** — For ~90% of day-to-day workflows, you can compose, operate, and save workspaces **without writing
a single line of code**. Just launch, drag widgets, and do your experiment.
- **Flexible layout composition** — Build complex experiment GUIs in seconds with the `BECDockArea`: dragdock, tab,
split, and export profiles/workspaces for reuse.
- **CLI / scripting** — Control your beamline experiment from the command line a robust RPC layer using
`BECIPythonClient`.
- **Designer integration** — Use Qt Designer plugins to drop BEC widgets next to any Qt control, then launch the `.ui`
with the custom BEC loader for a zeroglue workflow.
- **Operational integration** — Widgets stay in sync with your running BEC/Redis as the single source of truth:
Subscribe to events from BEC and create dynamically updating UIs. BECWidgets also grants you easy access the
acquisition history.
- **Extensible by design** — Build new widgets with minimal boilerplate using `BECWidget` and `BECDispatcher` for BEC data and
messaging. Use the generator command to scaffold RPC interfaces and Designer plugin stubs; beamline plugins can extend
or override behavior as needed.
## Table of Contents
- [Installation](#installation)
- [Features](#features)
- [1. Dock area interface: build GUIs in seconds](#1-dock-area-interface-build-guis-in-seconds)
- [2. Qt Designer plugins + BEC Launcher (no glue)](#2-qt-designer-plugins--bec-launcher-no-glue)
- [3. Robust RPC from CLI & remote scripting](#3-robust-rpc-from-cli--remote-scripting)
- [4. Rapid development (extensible by design)](#4-rapid-development-extensible-by-design)
- [Widget Library](#widget-library)
- [Documentation](#documentation)
- [License](#license)
## Installation
Use any of the following setups:
### Stable release
Use the package manager [pip](https://pip.pypa.io/en/stable/) to install BEC Widgets:
```bash
pip install bec_widgets PyQt6
pip install bec_widgets
```
### From source (recommended for development)
For development purposes, you can clone the repository and install the package locally in editable mode:
```bash
git clone https://gitlab.psi.ch/bec/bec-widgets
git clone https://github.com/bec-project/bec_widgets.git
cd bec_widgets
pip install -e .[dev,pyqt6]
pip install -e .[dev]
```
BEC Widgets currently supports both Pyside6 and PyQt6, however, no default distribution is specified. As a result, users must install one of the supported
Python Qt distributions manually.
## Features
To select a specific Python Qt distribution, install the package with an additional tag:
### 1. Dock area interface: build GUIs in seconds
```bash
pip install bec_widgets[pyqt6]
The fastest way to explore BEC Widgets. Launch the BEC IPython client with simply `bec` in terminal and the **BECDockArea** opens as the default UI:
drag widgets, dock/tab/split panes, and explore. Everything is live—widgets auto-connect to BEC/Redis, so you can
operate immediately and refine later with RPC or Designer if needed.
![dock_area_example](https://github.com/user-attachments/assets/219a2806-19a8-4a07-9734-b7b554850833)
### 2. Qt Designer plugins + BEC Launcher (no glue)
All BEC Widgets ship as **Qt Designer plugins** with our custom Qt Designer launchable by `bec-designer`. Design your UI
visually in Designer, save a `.ui`, then launch it with
the **BEC Launcher**—no glue code. Widgets autoconnect to BEC/Redis on startup, so your UI is operational immediately.
![designer_opti](https://github.com/user-attachments/assets/fed4843c-1cce-438a-b41f-6636fa5e1545)
### 3. Robust RPC from CLI & remote scripting
Operate and automate BEC Widgets directly from the `BECIPythonClient`. Create or attach to GUIs, address any sub-widget
via a simple hierarchical API with tab-completion, and script event-driven behavior that reacts to BEC (scan lifecycle,
active devices, topics)—so your UI can be heavily automated.
- Create & control GUIs: launch, load profiles, open/close panels, tweak properties—all from the shell.
- Hierarchical addressing: navigate widgets and sub-widgets with discoverable paths and tab-completion.
- Event scripting: subscribe to BEC events (e.g., scan start/finish, device readiness, topic updates) and trigger
actions,switch profiles, open diagnostic views, or start specific scans.
- Remote & headless: run automation on analysis nodes or from notebooks without a local GUI process.
- Plays with no-code: Use the Dock Area / BEC Designer to set up the layout and add automation with RPC when needed.
![rpc_opti](https://github.com/user-attachments/assets/666be7fb-9a0d-44c2-8d44-2f9d1dae4497)
### 4. Rapid development (extensible by design)
Build new widgets fast: Inherit from `BECWidget`, list your RPC methods in `USER_ACCESS`, and use `bec_dispatcher` to
bind endpoints. Then run `bw-generate-cli --target <your-plugin-repo>`. This generates the RPC CLI bindings and a Qt
Designer plugin that are immediately usable with your BEC setup. Widgets
come online with live BEC/Redis wiring out of the box.
<details>
<summary> View code: Example Widget </summary>
```python
from typing import Literal
from qtpy.QtWidgets import QWidget, QLabel, QPushButton, QHBoxLayout, QVBoxLayout, QApplication
from qtpy.QtCore import Slot
from bec_lib.endpoints import MessageEndpoints
from bec_widgets import BECWidget, SafeSlot
class SimpleMotorWidget(BECWidget, QWidget):
USER_ACCESS = ["move"]
def __init__(self, parent=None, motor_name="samx", step=5.0, **kwargs):
super().__init__(parent=parent, **kwargs)
self.motor_name = motor_name
self.step = float(step)
self.get_bec_shortcuts()
self.value_label = QLabel(f"{self.motor_name}: —")
self.btn_left = QPushButton("◀︎ -5")
self.btn_right = QPushButton("+5 ▶︎")
row = QHBoxLayout()
row.addWidget(self.btn_left)
row.addWidget(self.btn_right)
col = QVBoxLayout(self)
col.addWidget(self.value_label)
col.addLayout(row)
self.btn_left.clicked.connect(lambda: self.move("left", self.step))
self.btn_right.clicked.connect(lambda: self.move("right", self.step))
self.bec_dispatcher.connect_slot(self.on_readback, MessageEndpoints.device_readback(self.motor_name))
@SafeSlot(dict, dict)
def on_readback(self, data: dict, meta: dict):
current_value = data.get("signals").get(self.motor_name).get('value')
self.value_label.setText(f"{self.motor_name}: {current_value:.3f}")
@Slot(str, float)
def move(self, direction: Literal["left", "right"] = "left", step: float = 5.0):
if direction == "left":
self.dev[self.motor_name].move(-step, relative=True)
else:
self.dev[self.motor_name].move(step, relative=True)
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = SimpleMotorWidget(motor_name="samx", step=5.0)
w.setWindowTitle("MotorJogWidget")
w.resize(280, 90)
w.show()
sys.exit(app.exec_())
```
or
```bash
pip install bec_widgets[pyside6]
```
</details>
## Widget Library
A large and growing catalog—plug, configure, run:
### Plotting
Waveform, MultiWaveform, and Image/Heatmap widgets deliver responsive plots with crosshairs and ROIs for live and
history data.
<img width="1108" height="838" alt="plotting_hr" src="https://github.com/user-attachments/assets/f50462a5-178d-44d4-aee5-d378c74b107b" />
### Scan orchestration and motion control.
Start and stop scans, track progress, reuse parameter presets, and browse history from a focused control surface.
Positioner boxes and tweak controls handle precise moves, homing, and calibration for daytoday alignment.
<img width="1496" height="1388" alt="control" src="https://github.com/user-attachments/assets/d4fb2e2e-04f9-4621-8087-790680797620" />
## Documentation
Documentation of BEC Widgets can be found [here](https://bec-widgets.readthedocs.io/en/latest/). The documentation of the BEC can be found [here](https://bec.readthedocs.io/en/latest/).
## Contributing
All commits should use the Angular commit scheme:
> #### <a name="commit-header"></a>Angular Commit Message Header
>
> ```
> <type>(<scope>): <short summary>
> │ │ │
> │ │ └─⫸ Summary in present tense. Not capitalized. No period at the end.
> │ │
> │ └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core|
> │ elements|forms|http|language-service|localize|platform-browser|
> │ platform-browser-dynamic|platform-server|router|service-worker|
> │ upgrade|zone.js|packaging|changelog|docs-infra|migrations|ngcc|ve|
> │ devtools
>
> └─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test
> ```
>
> The `<type>` and `<summary>` fields are mandatory, the `(<scope>)` field is optional.
> ##### Type
>
> Must be one of the following:
>
> * **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
> * **ci**: Changes to our CI configuration files and scripts (examples: CircleCi, SauceLabs)
> * **docs**: Documentation only changes
> * **feat**: A new feature
> * **fix**: A bug fix
> * **perf**: A code change that improves performance
> * **refactor**: A code change that neither fixes a bug nor adds a feature
> * **test**: Adding missing tests or correcting existing tests
Documentation of BEC Widgets can be found [here](https://bec-widgets.readthedocs.io/en/latest/). The documentation of
the BEC can be found [here](https://bec.readthedocs.io/en/latest/).
## License
[BSD-3-Clause](https://choosealicense.com/licenses/bsd-3-clause/)
[BSD-3-Clause](https://choosealicense.com/licenses/bsd-3-clause/)
+28
View File
@@ -0,0 +1,28 @@
While BEC Widgets is shipped with BSD-3-Clause license, it includes third-party components with different licenses. Below is a list of these components along with their respective licenses.
Core Dependencies:
- BEC: BSD-3-Clause License, see [here](https://github.com/bec-project/bec/blob/main/LICENSE)
- black: MIT License, see [here](https://github.com/psf/black/blob/main/LICENSE)
- isort: MIT License, see [here](https://github.com/PyCQA/isort/blob/main/LICENSE)
- pydantic: MIT License, see [here](https://github.com/pydantic/pydantic/blob/main/LICENSE)
- pyqtgraph: MIT License, see [here](https://github.com/pyqtgraph/pyqtgraph/blob/master/LICENSE.txt)
- PySide6: LGPLv3 License, see [here](https://doc.qt.io/qtforpython/licenses.html)
- qtconsole: BSD-3-Clause License, see [here](https://github.com/spyder-ide/qtconsole/blob/main/LICENSE)
- qtpy: MIT License, see [here](https://github.com/spyder-ide/qtpy/blob/master/LICENSE.txt)
- qtmonaco: BSD-3-Clause License, see [here](https://github.com/bec-project/qtmonaco/blob/main/LICENSE)
- thefuzz: MIT License, see [here](https://github.com/seatgeek/thefuzz/blob/master/LICENSE.txt)
Additional Dependencies (Testing/Development):
- coverage: Apache License 2.0, see [here](https://github.com/coveragepy/coveragepy/blob/main/LICENSE.txt)
- fakeredis: BSD-3-Clause License, see [here](https://github.com/cunla/fakeredis-py/blob/master/LICENSE)
- pytest-bec-e2e: BSD-3-Clause License, see [here](https://github.com/bec-project/bec/blob/main/LICENSE)
- pytest-qt: MIT License, see [here](https://github.com/pytest-dev/pytest-qt/blob/master/LICENSE)
- pytest-random-order: MIT License, see [here](https://github.com/pytest-dev/pytest-random-order/blob/main/LICENSE)
- pytest-timeout: MIT License, see [here](https://github.com/pytest-dev/pytest-timeout/blob/main/LICENSE)
- pytest-xvfb: MIT License, see [here](https://github.com/The-Compiler/pytest-xvfb/blob/master/LICENSE)
- pytest: MIT License, see [here](https://github.com/pytest-dev/pytest/blob/main/LICENSE)
- pytest-cov: MIT License, see [here](https://github.com/pytest-dev/pytest-cov/blob/main/LICENSE)
- watchdog: Apache License 2.0, see [here](https://github.com/gorakhargosh/watchdog/blob/master/LICENSE)
- pre_commit: MIT License, see [here](https://github.com/pre-commit/pre-commit/blob/main/LICENSE)
+19
View File
@@ -0,0 +1,19 @@
import os
import sys
import bec_widgets.widgets.containers.qt_ads as QtAds
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
if sys.platform.startswith("linux"):
qt_platform = os.environ.get("QT_QPA_PLATFORM", "")
if qt_platform != "offscreen":
os.environ["QT_QPA_PLATFORM"] = "xcb"
# Default QtAds configuration
QtAds.CDockManager.setConfigFlag(QtAds.CDockManager.eConfigFlag.FocusHighlighting, True)
QtAds.CDockManager.setConfigFlag(
QtAds.CDockManager.eConfigFlag.RetainTabSizeWhenCloseButtonHidden, True
)
__all__ = ["BECWidget", "SafeSlot", "SafeProperty"]
+54
View File
@@ -0,0 +1,54 @@
from __future__ import annotations
from typing import Literal
from bec_lib import bec_logger
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea
logger = bec_logger.logger
def dock_area(
object_name: str | None = None, startup_profile: str | Literal["restore", "skip"] | None = None
) -> BECDockArea:
"""
Create an advanced dock area using Qt Advanced Docking System.
Args:
object_name(str): The name of the advanced dock area.
startup_profile(str | Literal["restore", "skip"] | None): Startup mode for
the workspace:
- None: start empty
- "restore": restore last used profile
- "skip": do not initialize profile state
- "<name>": load specific profile
Returns:
BECDockArea: The created advanced dock area.
"""
widget = BECDockArea(
object_name=object_name,
root_widget=True,
profile_namespace="bec",
startup_profile=startup_profile,
)
logger.info(f"Created advanced dock area with startup_profile: {startup_profile}")
return widget
def auto_update_dock_area(object_name: str | None = None) -> AutoUpdates:
"""
Create a dock area with auto update enabled.
Args:
object_name(str): The name of the dock area.
Returns:
BECDockArea: The created dock area.
"""
_auto_update = AutoUpdates(object_name=object_name)
return _auto_update
+716
View File
@@ -0,0 +1,716 @@
from __future__ import annotations
import os
import xml.etree.ElementTree as ET
from typing import TYPE_CHECKING, Callable
from bec_lib.logger import bec_logger
from qtpy.QtCore import Qt, Signal # type: ignore
from qtpy.QtGui import QFontMetrics, QPainter, QPainterPath, QPixmap
from qtpy.QtWidgets import (
QApplication,
QComboBox,
QFileDialog,
QHBoxLayout,
QLabel,
QPushButton,
QSizePolicy,
QSpacerItem,
QWidget,
)
import bec_widgets
from bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets
from bec_widgets.utils.container_utils import WidgetContainerUtils
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.name_utils import pascal_to_snake
from bec_widgets.utils.plugin_utils import get_plugin_auto_updates
from bec_widgets.utils.round_frame import RoundedFrame
from bec_widgets.utils.screen_utils import apply_window_geometry, centered_geometry_for_app
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea
from bec_widgets.widgets.containers.dock_area.profile_utils import get_last_profile, list_profiles
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow, BECMainWindowNoRPC
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
if TYPE_CHECKING: # pragma: no cover
from qtpy.QtCore import QObject
from bec_widgets.utils.bec_widget import BECWidget
logger = bec_logger.logger
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
START_EMPTY_PROFILE_OPTION = "Start Empty (No Profile)"
class LaunchTile(RoundedFrame):
DEFAULT_SIZE = (250, 300)
open_signal = Signal()
def __init__(
self,
parent: QObject | None = None,
icon_path: str | None = None,
top_label: str | None = None,
main_label: str | None = None,
description: str | None = None,
show_selector: bool = False,
tile_size: tuple[int, int] | None = None,
):
super().__init__(parent=parent, orientation="vertical")
# Provide a perinstance TILE_SIZE so the class can compute layout
if tile_size is None:
tile_size = self.DEFAULT_SIZE
self.tile_size = tile_size
self.icon_label = QLabel(parent=self)
self.icon_label.setFixedSize(100, 100)
self.icon_label.setScaledContents(True)
pixmap = QPixmap(icon_path)
if not pixmap.isNull():
size = 100
circular_pixmap = QPixmap(size, size)
circular_pixmap.fill(Qt.transparent)
painter = QPainter(circular_pixmap)
painter.setRenderHints(QPainter.RenderHint.Antialiasing, True)
path = QPainterPath()
path.addEllipse(0, 0, size, size)
painter.setClipPath(path)
pixmap = pixmap.scaled(
size,
size,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
painter.drawPixmap(0, 0, pixmap)
painter.end()
self.icon_label.setPixmap(circular_pixmap)
self.layout.addWidget(self.icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
# Top label
self.top_label = QLabel(top_label.upper())
font_top = self.top_label.font()
font_top.setPointSize(10)
self.top_label.setFont(font_top)
self.layout.addWidget(self.top_label, alignment=Qt.AlignmentFlag.AlignCenter)
# Main label
self.main_label = QLabel(main_label)
# Desired default appearance
font_main = self.main_label.font()
font_main.setPointSize(14)
font_main.setBold(True)
self.main_label.setFont(font_main)
self.main_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Shrink font if the default would wrap on this platform / DPI
content_width = (
self.tile_size[0]
- self.layout.contentsMargins().left()
- self.layout.contentsMargins().right()
)
self._fit_label_to_width(self.main_label, content_width)
# Give every tile the same reserved height for the title so the
# description labels start at an identical yoffset.
self.main_label.setFixedHeight(QFontMetrics(self.main_label.font()).height() + 2)
self.layout.addWidget(self.main_label)
self.spacer_top = QSpacerItem(0, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
self.layout.addItem(self.spacer_top)
# Description
self.description_label = QLabel(description)
self.description_label.setWordWrap(True)
self.description_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.layout.addWidget(self.description_label)
# Selector
if show_selector:
self.selector = QComboBox(self)
self.layout.addWidget(self.selector)
else:
self.selector = None
self.spacer_bottom = QSpacerItem(
0, 0, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding
)
self.layout.addItem(self.spacer_bottom)
# Action button
self.action_button = QPushButton("Open")
self.action_button.setStyleSheet("""
QPushButton {
background-color: #007AFF;
border: none;
padding: 8px 16px;
color: white;
border-radius: 6px;
font-weight: bold;
}
QPushButton:hover {
background-color: #005BB5;
}
""")
self.layout.addWidget(self.action_button, alignment=Qt.AlignmentFlag.AlignCenter)
def _fit_label_to_width(self, label: QLabel, max_width: int, min_pt: int = 10):
"""
Fit the label text to the specified maximum width by adjusting the font size.
Args:
label(QLabel): The label to adjust.
max_width(int): The maximum width the label can occupy.
min_pt(int): The minimum font point size to use.
"""
font = label.font()
for pt in range(font.pointSize(), min_pt - 1, -1):
font.setPointSize(pt)
metrics = QFontMetrics(font)
if metrics.horizontalAdvance(label.text()) <= max_width:
label.setFont(font)
label.setWordWrap(False)
return
# If nothing fits, fall back to eliding
metrics = QFontMetrics(font)
label.setFont(font)
label.setWordWrap(False)
label.setText(metrics.elidedText(label.text(), Qt.TextElideMode.ElideRight, max_width))
class LaunchWindow(BECMainWindow):
RPC = True
PLUGIN = False
TILE_SIZE = (250, 300)
DEFAULT_LAUNCH_SIZE = (800, 600)
USER_ACCESS = ["show_launcher", "hide_launcher"]
def __init__(
self,
parent=None,
gui_id: str = None,
window_title="BEC Launcher",
launch_gui_class: str = None,
launch_gui_id: str = None,
*args,
**kwargs,
):
super().__init__(parent=parent, gui_id=gui_id, window_title=window_title, **kwargs)
self.app = QApplication.instance()
self.tiles: dict[str, LaunchTile] = {}
# Track the smallest mainlabel font size chosen so far
self._min_main_label_pt: int | None = None
# Toolbar
self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
self.toolbar = ModularToolBar(parent=self)
self.addToolBar(Qt.TopToolBarArea, self.toolbar)
self.spacer = QWidget(self)
self.spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.toolbar.addWidget(self.spacer)
self.toolbar.addWidget(self.dark_mode_button)
# Main Widget
self.central_widget = QWidget(self)
self.central_widget.layout = QHBoxLayout(self.central_widget)
self.setCentralWidget(self.central_widget)
self.register_tile(
name="dock_area",
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
top_label="Get started",
main_label="BEC Advanced Dock Area",
description="Flexible application for managing modular widgets and user profiles.",
action_button=self._open_dock_area,
show_selector=True,
selector_items=list_profiles("bec"),
)
self._refresh_dock_area_profiles(preserve_selection=False)
self.available_auto_updates: dict[str, type[AutoUpdates]] = (
self._update_available_auto_updates()
)
self.register_tile(
name="auto_update",
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "auto_update.png"),
top_label="Get automated",
main_label="BEC Auto Update Dock Area",
description="Dock area with auto update functionality for BEC widgets plotting.",
action_button=self._open_auto_update,
show_selector=True,
selector_items=list(self.available_auto_updates.keys()) + ["Default"],
)
self.register_tile(
name="custom_ui_file",
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "ui_loader_tile.png"),
top_label="Get customized",
main_label="Launch Custom UI File",
description="GUI application with custom UI file.",
action_button=self._open_custom_ui_file,
show_selector=False,
)
# plugin widgets
self.available_widgets: dict[str, type[BECWidget]] = get_all_plugin_widgets().as_dict()
if self.available_widgets:
plugin_repo_name = next(iter(self.available_widgets.values())).__module__.split(".")[0]
plugin_repo_name = plugin_repo_name.removesuffix("_bec").upper()
self.register_tile(
name="widget",
icon_path=os.path.join(
MODULE_PATH, "assets", "app_icons", "widget_launch_tile.png"
),
top_label="Get quickly started",
main_label=f"Launch a {plugin_repo_name} Widget",
description=f"GUI application with one widget from the {plugin_repo_name} repository.",
action_button=self._open_widget,
show_selector=True,
selector_items=list(self.available_widgets.keys()),
)
self._update_theme()
self.register = RPCRegister()
self.register.callbacks.append(self._turn_off_the_lights)
self.register.broadcast()
if launch_gui_class and launch_gui_id:
# If a specific gui class is provided, launch it and hide the launcher
self.launch(launch_gui_class, name=launch_gui_id)
self.hide()
def register_tile(
self,
name: str,
icon_path: str | None = None,
top_label: str | None = None,
main_label: str | None = None,
description: str | None = None,
action_button: Callable | None = None,
show_selector: bool = False,
selector_items: list[str] | None = None,
):
"""
Register a tile in the launcher window.
Args:
name(str): The name of the tile.
icon_path(str): The path to the icon.
top_label(str): The top label of the tile.
main_label(str): The main label of the tile.
description(str): The description of the tile.
action_button(callable): The action to be performed when the button is clicked.
show_selector(bool): Whether to show a selector or not.
selector_items(list[str]): The items to be shown in the selector.
"""
tile = LaunchTile(
icon_path=icon_path,
top_label=top_label,
main_label=main_label,
description=description,
show_selector=show_selector,
tile_size=self.TILE_SIZE,
)
tile.setFixedWidth(self.TILE_SIZE[0])
tile.setMinimumHeight(self.TILE_SIZE[1])
tile.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.MinimumExpanding)
if action_button:
tile.action_button.clicked.connect(action_button)
if show_selector and selector_items:
tile.selector.addItems(selector_items)
self.central_widget.layout.addWidget(tile)
# keep all tiles' main labels at a unified point size
current_pt = tile.main_label.font().pointSize()
if self._min_main_label_pt is None or current_pt < self._min_main_label_pt:
# New global minimum shrink every existing tile to this size
self._min_main_label_pt = current_pt
for t in self.tiles.values():
f = t.main_label.font()
f.setPointSize(self._min_main_label_pt)
t.main_label.setFont(f)
t.main_label.setFixedHeight(QFontMetrics(f).height() + 2)
elif current_pt > self._min_main_label_pt:
# Tile is larger than global minimum shrink it to match
f = tile.main_label.font()
f.setPointSize(self._min_main_label_pt)
tile.main_label.setFont(f)
tile.main_label.setFixedHeight(QFontMetrics(f).height() + 2)
self.tiles[name] = tile
def _refresh_dock_area_profiles(self, preserve_selection: bool = True) -> None:
"""
Refresh the dock-area profile selector, optionally preserving the selection.
Defaults to Start Empty when no valid selection can be preserved.
Args:
preserve_selection(bool): Whether to preserve the current selection or not.
"""
tile = self.tiles.get("dock_area")
if tile is None or tile.selector is None:
return
selector = tile.selector
selected_text = (
selector.currentText().strip() if preserve_selection and selector.count() > 0 else ""
)
profiles = list_profiles("bec")
selector_items = [START_EMPTY_PROFILE_OPTION, *profiles]
selector.blockSignals(True)
selector.clear()
for profile in selector_items:
selector.addItem(profile)
if selected_text:
# Try to preserve the current selection
idx = selector.findText(selected_text, Qt.MatchFlag.MatchExactly)
if idx >= 0:
selector.setCurrentIndex(idx)
else:
# Selection no longer exists, fall back to default startup selection.
self._set_selector_to_default_profile(selector, profiles)
else:
# No selection to preserve, use default startup selection.
self._set_selector_to_default_profile(selector, profiles)
selector.blockSignals(False)
def _set_selector_to_default_profile(self, selector: QComboBox, profiles: list[str]) -> None:
"""
Set the selector default.
Preference order:
1) Start Empty option (if available)
2) Last used profile
3) First available profile
Args:
selector(QComboBox): The combobox to set.
profiles(list[str]): List of available profiles.
"""
start_empty_idx = selector.findText(START_EMPTY_PROFILE_OPTION, Qt.MatchFlag.MatchExactly)
if start_empty_idx >= 0:
selector.setCurrentIndex(start_empty_idx)
return
# Try to get last used profile
last_profile = get_last_profile(namespace="bec")
if last_profile and last_profile in profiles:
idx = selector.findText(last_profile, Qt.MatchFlag.MatchExactly)
if idx >= 0:
selector.setCurrentIndex(idx)
return
# If nothing else, select first item
if selector.count() > 0:
selector.setCurrentIndex(0)
def launch(
self,
launch_script: str,
name: str | None = None,
geometry: tuple[int, int, int, int] | None = None,
**kwargs,
) -> QWidget | None:
"""Launch the specified script. If the launch script creates a QWidget, it will be
embedded in a BECMainWindow. If the launch script creates a BECMainWindow, it will be shown
as a separate window.
Args:
launch_script(str): The name of the script to be launched.
name(str): The name of the dock area.
geometry(tuple): The geometry parameters to be passed to the dock area.
Returns:
QWidget: The created dock area.
"""
from bec_widgets.applications import bw_launch
with RPCRegister.delayed_broadcast() as rpc_register:
if geometry is None and launch_script != "custom_ui_file":
geometry = self._default_launch_geometry()
existing_dock_areas = rpc_register.get_names_of_rpc_by_class_type(BECDockArea)
if name is not None:
WidgetContainerUtils.raise_for_invalid_name(name)
# If name already exists, generate a unique one with counter suffix
if name in existing_dock_areas:
name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas)
else:
name = "dock_area"
name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas)
if launch_script is None:
launch_script = "dock_area"
if not isinstance(launch_script, str):
raise ValueError(f"Launch script must be a string, but got {type(launch_script)}.")
if launch_script == "custom_ui_file":
ui_file = kwargs.pop("ui_file", None)
if not ui_file:
return None
return self._launch_custom_ui_file(ui_file)
if launch_script == "auto_update":
auto_update = kwargs.pop("auto_update", None)
return self._launch_auto_update(auto_update, geometry=geometry)
if launch_script == "widget":
widget = kwargs.pop("widget", None)
if widget is None:
raise ValueError("Widget name must be provided.")
return self._launch_widget(widget, geometry=geometry)
launch = getattr(bw_launch, launch_script, None)
if launch is None:
raise ValueError(f"Launch script {launch_script} not found.")
result_widget = launch(name, **kwargs)
# TODO Should we simply use the specified name as title here?
result_widget.window().setWindowTitle(f"BEC - {name}")
logger.info(f"Created new dock area: {name}")
if isinstance(result_widget, BECMainWindow):
apply_window_geometry(result_widget, geometry)
result_widget.show()
else:
window = BECMainWindowNoRPC()
window.setCentralWidget(result_widget)
window.setWindowTitle(f"BEC - {result_widget.objectName()}")
apply_window_geometry(window, geometry)
window.show()
return result_widget
def _launch_custom_ui_file(self, ui_file: str | None) -> BECMainWindow:
"""
Load a custom .ui file. If the top-level widget is a MainWindow subclass,
instantiate it directly; otherwise, embed it in a UILaunchWindow.
"""
if ui_file is None:
raise ValueError("UI file must be provided for custom UI file launch.")
filename = os.path.basename(ui_file).split(".")[0]
WidgetContainerUtils.raise_for_invalid_name(filename)
# Parse the UI to detect top-level widget class
tree = ET.parse(ui_file)
root = tree.getroot()
# Check if the top-level widget is a QMainWindow
widget = root.find("widget")
if widget is None:
raise ValueError("No widget found in the UI file.")
# Load the UI into a widget
loader = UILoader(None)
loaded = loader.loader(ui_file)
# Display the UI in a BECMainWindow
if isinstance(loaded, BECMainWindow):
window = loaded
window.object_name = filename
else:
window = BECMainWindow(object_name=filename)
window.setCentralWidget(loaded)
window.setWindowTitle(f"BEC - {filename}")
apply_window_geometry(window, None)
window.show()
logger.info(f"Launched custom UI: {filename}, type: {type(window).__name__}")
return window
def _launch_auto_update(
self, auto_update: str, geometry: tuple[int, int, int, int] | None = None
) -> AutoUpdates:
if auto_update in self.available_auto_updates:
auto_update_cls = self.available_auto_updates[auto_update]
window = auto_update_cls()
else:
auto_update = "auto_updates"
window = AutoUpdates()
window.resize(window.minimumSizeHint())
window.setWindowTitle(f"BEC - {window.objectName()}")
apply_window_geometry(window, geometry)
window.show()
return window
def _launch_widget(
self, widget: type[BECWidget], geometry: tuple[int, int, int, int] | None = None
) -> QWidget:
name = pascal_to_snake(widget.__name__)
WidgetContainerUtils.raise_for_invalid_name(name)
window = BECMainWindowNoRPC()
widget_instance = widget(root_widget=True, object_name=name)
assert isinstance(widget_instance, QWidget)
window.setCentralWidget(widget_instance)
window.resize(window.minimumSizeHint())
window.setWindowTitle(f"BEC - {widget_instance.objectName()}")
apply_window_geometry(window, geometry)
window.show()
return window
def apply_theme(self, theme: str):
"""
Change the theme of the application.
"""
for tile in self.tiles.values():
tile.apply_theme(theme)
super().apply_theme(theme)
def _open_auto_update(self):
"""
Open the auto update window.
"""
if self.tiles["auto_update"].selector is None:
auto_update = None
else:
auto_update = self.tiles["auto_update"].selector.currentText()
if auto_update == "Default":
auto_update = None
return self.launch("auto_update", auto_update=auto_update)
def _open_dock_area(self):
"""
Open Advanced Dock Area using the selected profile.
"""
tile = self.tiles.get("dock_area")
if tile is None or tile.selector is None:
startup_profile = None
else:
selection = tile.selector.currentText().strip()
if selection == START_EMPTY_PROFILE_OPTION:
startup_profile = None
else:
startup_profile = selection if selection else None
return self.launch("dock_area", startup_profile=startup_profile)
def _open_widget(self):
"""
Open a widget from the available widgets.
"""
if self.tiles["widget"].selector is None:
return
widget = self.tiles["widget"].selector.currentText()
if widget not in self.available_widgets:
raise ValueError(f"Widget {widget} not found in available widgets.")
return self.launch("widget", widget=self.available_widgets[widget])
def _default_launch_geometry(self) -> tuple[int, int, int, int] | None:
width, height = self.DEFAULT_LAUNCH_SIZE
return centered_geometry_for_app(width=width, height=height)
@SafeSlot(popup_error=True)
def _open_custom_ui_file(self):
"""
Open a file dialog to select a custom UI file and launch it.
"""
ui_file, _ = QFileDialog.getOpenFileName(
self, "Select UI File", "", "UI Files (*.ui);;All Files (*)"
)
self.launch("custom_ui_file", ui_file=ui_file)
@staticmethod
def _update_available_auto_updates() -> dict[str, type[AutoUpdates]]:
"""
Load all available auto updates from the plugin repository.
"""
try:
auto_updates = get_plugin_auto_updates()
logger.info(f"Available auto updates: {auto_updates.keys()}")
except Exception as exc:
logger.error(f"Failed to load auto updates: {exc}")
return {}
return auto_updates
def show_launcher(self):
"""
Show the launcher window.
"""
self.show()
def hide_launcher(self):
"""
Hide the launcher window.
"""
self.hide()
def showEvent(self, event):
self._refresh_dock_area_profiles()
super().showEvent(event)
self.setFixedSize(self.size())
def _launcher_is_last_widget(self, connections: dict) -> bool:
"""
Check if the launcher is the last widget in the application.
"""
# get all parents of connections
for connection in connections.values():
try:
parent = connection.parent()
if parent is None and connection.objectName() != self.objectName():
logger.info(
f"Found non-launcher connection without parent: {connection.objectName()}"
)
return False
except Exception as e:
logger.error(f"Error getting parent of connection: {e}")
return False
return True
def _turn_off_the_lights(self, connections: dict):
"""
If there is only one connection remaining, it is the launcher, so we show it.
Once the launcher is closed as the last window, we quit the application.
"""
if self._launcher_is_last_widget(connections):
self.show()
self.activateWindow()
self.raise_()
if self.app:
self.app.setQuitOnLastWindowClosed(True) # type: ignore
return
self.hide()
if self.app:
self.app.setQuitOnLastWindowClosed(False) # type: ignore
def closeEvent(self, event):
"""
Close the launcher window.
"""
connections = self.register.list_all_connections()
if self._launcher_is_last_widget(connections):
event.accept()
return
event.ignore()
self.hide()
if __name__ == "__main__": # pragma: no cover
import sys
from bec_widgets.utils.colors import apply_theme
app = QApplication(sys.argv)
apply_theme("dark")
launcher = LaunchWindow()
launcher.show()
sys.exit(app.exec())
+397
View File
@@ -0,0 +1,397 @@
from bec_qthemes import material_icon
from qtpy.QtGui import QAction # type: ignore
from qtpy.QtWidgets import QApplication, QHBoxLayout, QStackedWidget, QWidget
from bec_widgets.applications.navigation_centre.reveal_animator import ANIMATION_DURATION
from bec_widgets.applications.navigation_centre.side_bar import SideBar
from bec_widgets.applications.navigation_centre.side_bar_components import NavigationItem
from bec_widgets.applications.views.developer_view.developer_view import DeveloperView
from bec_widgets.applications.views.device_manager_view.device_manager_view import DeviceManagerView
from bec_widgets.applications.views.dock_area_view.dock_area_view import DockAreaView
from bec_widgets.applications.views.view import ViewBase, WaveformViewInline, WaveformViewPopup
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.guided_tour import GuidedTour
from bec_widgets.utils.name_utils import sanitize_namespace
from bec_widgets.utils.screen_utils import (
apply_centered_size,
available_screen_geometry,
main_app_size_for_screen,
)
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
class BECMainApp(BECMainWindow):
RPC = False
PLUGIN = False
def __init__(
self,
parent=None,
*args,
anim_duration: int = ANIMATION_DURATION,
show_examples: bool = False,
**kwargs,
):
super().__init__(parent=parent, *args, **kwargs)
self._show_examples = bool(show_examples)
# --- Compose central UI (sidebar + stack)
self.sidebar = SideBar(parent=self, anim_duration=anim_duration)
self.stack = QStackedWidget(self)
container = QWidget(self)
layout = QHBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(self.sidebar, 0)
layout.addWidget(self.stack, 1)
self.setCentralWidget(container)
# Mapping for view switching
self._view_index: dict[str, int] = {}
self._current_view_id: str | None = None
self.sidebar.view_selected.connect(self._on_view_selected)
self._add_views()
# Initialize guided tour
self.guided_tour = GuidedTour(self)
self._setup_guided_tour()
def _add_views(self):
self.add_section("BEC Applications", "bec_apps")
self.dock_area = DockAreaView(self)
self.device_manager = DeviceManagerView(self)
# self.developer_view = DeveloperView(self) #TODO temporary disable until the bugs with BECShell are resolved
self.add_view(icon="widgets", title="Dock Area", widget=self.dock_area, mini_text="Docks")
self.add_view(
icon="display_settings",
title="Device Manager",
widget=self.device_manager,
mini_text="DM",
)
# TODO temporary disable until the bugs with BECShell are resolved
# self.add_view(
# icon="code_blocks",
# title="IDE",
# widget=self.developer_view,
# mini_text="IDE",
# exclusive=True,
# )
if self._show_examples:
self.add_section("Examples", "examples")
waveform_view_popup = WaveformViewPopup(
parent=self, view_id="waveform_view_popup", title="Waveform Plot"
)
waveform_view_stack = WaveformViewInline(
parent=self, view_id="waveform_view_stack", title="Waveform Plot"
)
self.add_view(
icon="show_chart",
title="Waveform With Popup",
widget=waveform_view_popup,
mini_text="Popup",
)
self.add_view(
icon="show_chart",
title="Waveform InLine Stack",
widget=waveform_view_stack,
mini_text="Stack",
)
self.set_current("dock_area")
self.sidebar.add_dark_mode_item()
# Add guided tour to Help menu
self._add_guided_tour_to_menu()
# --- Public API ------------------------------------------------------
def add_section(self, title: str, id: str, position: int | None = None):
return self.sidebar.add_section(title, id, position)
def add_separator(self):
return self.sidebar.add_separator()
def add_dark_mode_item(self, id: str = "dark_mode", position: int | None = None):
return self.sidebar.add_dark_mode_item(id=id, position=position)
def add_view(
self,
*,
icon: str,
title: str,
view_id: str | None = None,
widget: QWidget,
mini_text: str | None = None,
position: int | None = None,
from_top: bool = True,
toggleable: bool = True,
exclusive: bool = True,
) -> NavigationItem:
"""
Register a view in the stack and create a matching nav item in the sidebar.
Args:
icon(str): Icon name for the nav item.
title(str): Title for the nav item.
view_id(str, optional): Unique ID for the view/item. If omitted, uses mini_text;
if mini_text is also omitted, uses title.
widget(QWidget): The widget to add to the stack.
mini_text(str, optional): Short text for the nav item when sidebar is collapsed.
position(int, optional): Position to insert the nav item.
from_top(bool, optional): Whether to count position from the top or bottom.
toggleable(bool, optional): Whether the nav item is toggleable.
exclusive(bool, optional): Whether the nav item is exclusive.
Returns:
NavigationItem: The created navigation item.
"""
resolved_id = sanitize_namespace(view_id or mini_text or title)
item = self.sidebar.add_item(
icon=icon,
title=title,
id=resolved_id,
mini_text=mini_text,
position=position,
from_top=from_top,
toggleable=toggleable,
exclusive=exclusive,
)
# Wrap plain widgets into a ViewBase so enter/exit hooks are available
if isinstance(widget, ViewBase):
view_widget = widget
view_widget.view_id = resolved_id
view_widget.view_title = title
else:
view_widget = ViewBase(content=widget, parent=self, view_id=resolved_id, title=title)
view_widget.change_object_name(resolved_id)
idx = self.stack.addWidget(view_widget)
self._view_index[resolved_id] = idx
return item
def set_current(self, id: str) -> None:
if id in self._view_index:
self.sidebar.activate_item(id)
# Internal: route sidebar selection to the stack
def _on_view_selected(self, vid: str) -> None:
# Determine current view
current_index = self.stack.currentIndex()
current_view = (
self.stack.widget(current_index) if 0 <= current_index < self.stack.count() else None
)
# Ask current view whether we may leave
if current_view is not None and hasattr(current_view, "on_exit"):
may_leave = current_view.on_exit()
if may_leave is False:
# Veto: restore previous highlight without re-emitting selection
if self._current_view_id is not None:
self.sidebar.activate_item(self._current_view_id, emit_signal=False)
return
# Proceed with switch
idx = self._view_index.get(vid)
if idx is None or not (0 <= idx < self.stack.count()):
return
self.stack.setCurrentIndex(idx)
new_view = self.stack.widget(idx)
self._current_view_id = vid
if hasattr(new_view, "on_enter"):
new_view.on_enter()
def _setup_guided_tour(self):
"""
Setup the guided tour for the main application.
Registers key UI components and delegates to views for their internal components.
"""
tour_steps = []
# --- General Layout Components ---
# Register the sidebar toggle button
toggle_step = self.guided_tour.register_widget(
widget=self.sidebar.toggle,
title="Sidebar Toggle",
text="Click this button to expand or collapse the sidebar. When expanded, you can see full navigation item titles and section names.",
)
tour_steps.append(toggle_step)
# Register the sidebar icons
sidebar_dock_area = self.sidebar.components.get("dock_area")
if sidebar_dock_area:
dock_step = self.guided_tour.register_widget(
widget=sidebar_dock_area,
title="Dock Area View",
text="Click here to access the Dock Area view, where you can manage and arrange your dockable panels.",
)
tour_steps.append(dock_step)
sidebar_device_manager = self.sidebar.components.get("device_manager")
if sidebar_device_manager:
device_manager_step = self.guided_tour.register_widget(
widget=sidebar_device_manager,
title="Device Manager View",
text="Click here to open the Device Manager view, where you can view and manage device configs.",
)
tour_steps.append(device_manager_step)
sidebar_developer_view = self.sidebar.components.get("developer_view")
if sidebar_developer_view:
developer_view_step = self.guided_tour.register_widget(
widget=sidebar_developer_view,
title="Developer View",
text="Click here to access the Developer view to write scripts and makros.",
)
tour_steps.append(developer_view_step)
# Register the dark mode toggle
dark_mode_item = self.sidebar.components.get("dark_mode")
if dark_mode_item:
dark_mode_step = self.guided_tour.register_widget(
widget=dark_mode_item,
title="Theme Toggle",
text="Switch between light and dark themes. The theme preference is saved and will be applied when you restart the application.",
)
tour_steps.append(dark_mode_step)
# Register the client info label
if hasattr(self, "_client_info_hover"):
client_info_step = self.guided_tour.register_widget(
widget=self._client_info_hover,
title="Client Status",
text="Displays status messages and information from the BEC Server.",
)
tour_steps.append(client_info_step)
# Register the scan progress bar if available
if hasattr(self, "_scan_progress_hover"):
progress_step = self.guided_tour.register_widget(
widget=self._scan_progress_hover,
title="Scan Progress",
text="Monitor the progress of ongoing scans. Hover over the progress bar to see detailed information including elapsed time and estimated completion.",
)
tour_steps.append(progress_step)
# Register the notification indicator in the status bar
if hasattr(self, "notification_indicator"):
notif_step = self.guided_tour.register_widget(
widget=self.notification_indicator,
title="Notification Center",
text="View system notifications, errors, and status updates. Click to filter notifications by type or expand to see all details.",
)
tour_steps.append(notif_step)
# --- View-Specific Components ---
# Register all views that can extend the tour
for view_id, view_index in self._view_index.items():
view_widget = self.stack.widget(view_index)
if not view_widget or not hasattr(view_widget, "register_tour_steps"):
continue
# Get the view's tour steps
view_tour = view_widget.register_tour_steps(self.guided_tour, self)
if view_tour is None:
if hasattr(view_widget.content, "register_tour_steps"):
view_tour = view_widget.content.register_tour_steps(self.guided_tour, self)
if view_tour is None:
continue
# Get the corresponding sidebar navigation item
nav_item = self.sidebar.components.get(view_id)
if not nav_item:
continue
# Use the view's title for the navigation button
nav_step = self.guided_tour.register_widget(
widget=nav_item,
title=view_tour.view_title,
text=f"Let's explore the features of the {view_tour.view_title}.",
)
tour_steps.append(nav_step)
tour_steps.extend(view_tour.step_ids)
# Create the tour with all registered steps
if tour_steps:
self.guided_tour.create_tour(tour_steps)
def start_guided_tour(self):
"""
Public method to start the guided tour.
This can be called programmatically or connected to a menu/button action.
"""
self.guided_tour.start_tour()
def _add_guided_tour_to_menu(self):
"""
Add a 'Guided Tour' action to the Help menu.
"""
# Find the Help menu
menu_bar = self.menuBar()
help_menu = None
for action in menu_bar.actions():
if action.text() == "Help":
help_menu = action.menu()
break
if help_menu:
# Add separator before the tour action
help_menu.addSeparator()
# Create and add the guided tour action
tour_action = QAction("Start Guided Tour", self)
tour_action.setIcon(material_icon("help"))
tour_action.triggered.connect(self.start_guided_tour)
tour_action.setShortcut("F1") # Add keyboard shortcut
help_menu.addAction(tour_action)
def cleanup(self):
for view_id, idx in self._view_index.items():
view = self.stack.widget(idx)
view.close()
view.deleteLater()
super().cleanup()
def main(): # pragma: no cover
"""
Main function to run the BEC main application, exposed as a script entry point through
pyproject.toml.
"""
# pylint: disable=import-outside-toplevel
import argparse
import sys
parser = argparse.ArgumentParser(description="BEC Main Application")
parser.add_argument(
"--examples", action="store_true", help="Show the Examples section with waveform demo views"
)
# Let Qt consume the remaining args
args, qt_args = parser.parse_known_args(sys.argv[1:])
app = QApplication([sys.argv[0], *qt_args])
apply_theme("dark")
w = BECMainApp(show_examples=args.examples)
screen_geometry = available_screen_geometry()
if screen_geometry is not None:
width, height = main_app_size_for_screen(screen_geometry)
apply_centered_size(w, width, height, available=screen_geometry)
else:
w.resize(w.minimumSizeHint())
w.show()
sys.exit(app.exec())
if __name__ == "__main__": # pragma: no cover
main()
@@ -0,0 +1,114 @@
from __future__ import annotations
from qtpy.QtCore import QEasingCurve, QParallelAnimationGroup, QPropertyAnimation
from qtpy.QtWidgets import QGraphicsOpacityEffect, QWidget
ANIMATION_DURATION = 500 # ms
class RevealAnimator:
"""Animate reveal/hide for a single widget using opacity + max W/H.
This keeps the widget always visible to avoid jitter from setVisible().
Collapsed state: opacity=0, maxW=0, maxH=0.
Expanded state: opacity=1, maxW=sizeHint.width(), maxH=sizeHint.height().
"""
def __init__(
self,
widget: QWidget,
duration: int = ANIMATION_DURATION,
easing: QEasingCurve.Type = QEasingCurve.InOutCubic,
initially_revealed: bool = False,
*,
animate_opacity: bool = True,
animate_width: bool = True,
animate_height: bool = True,
):
self.widget = widget
self.animate_opacity = animate_opacity
self.animate_width = animate_width
self.animate_height = animate_height
# Opacity effect
self.fx = QGraphicsOpacityEffect(widget)
widget.setGraphicsEffect(self.fx)
# Animations
self.opacity_anim = (
QPropertyAnimation(self.fx, b"opacity") if self.animate_opacity else None
)
self.width_anim = (
QPropertyAnimation(widget, b"maximumWidth") if self.animate_width else None
)
self.height_anim = (
QPropertyAnimation(widget, b"maximumHeight") if self.animate_height else None
)
for anim in (self.opacity_anim, self.width_anim, self.height_anim):
if anim is not None:
anim.setDuration(duration)
anim.setEasingCurve(easing)
# Initialize to requested state
self.set_immediate(initially_revealed)
def _natural_sizes(self) -> tuple[int, int]:
sh = self.widget.sizeHint()
w = max(sh.width(), 1)
h = max(sh.height(), 1)
return w, h
def set_immediate(self, revealed: bool):
"""
Immediately set the widget to the target revealed/collapsed state.
Args:
revealed(bool): True to reveal, False to collapse.
"""
w, h = self._natural_sizes()
if self.animate_opacity:
self.fx.setOpacity(1.0 if revealed else 0.0)
if self.animate_width:
self.widget.setMaximumWidth(w if revealed else 0)
if self.animate_height:
self.widget.setMaximumHeight(h if revealed else 0)
def setup(self, reveal: bool):
"""
Prepare animations to transition to the target revealed/collapsed state.
Args:
reveal(bool): True to reveal, False to collapse.
"""
# Prepare animations from current state to target
target_w, target_h = self._natural_sizes()
if self.opacity_anim is not None:
self.opacity_anim.setStartValue(self.fx.opacity())
self.opacity_anim.setEndValue(1.0 if reveal else 0.0)
if self.width_anim is not None:
self.width_anim.setStartValue(self.widget.maximumWidth())
self.width_anim.setEndValue(target_w if reveal else 0)
if self.height_anim is not None:
self.height_anim.setStartValue(self.widget.maximumHeight())
self.height_anim.setEndValue(target_h if reveal else 0)
def add_to_group(self, group: QParallelAnimationGroup):
"""
Add the prepared animations to the given animation group.
Args:
group(QParallelAnimationGroup): The animation group to add to.
"""
if self.opacity_anim is not None:
group.addAnimation(self.opacity_anim)
if self.width_anim is not None:
group.addAnimation(self.width_anim)
if self.height_anim is not None:
group.addAnimation(self.height_anim)
def animations(self):
"""
Get a list of all animations (non-None) for adding to a group.
"""
return [
anim
for anim in (self.opacity_anim, self.height_anim, self.width_anim)
if anim is not None
]
@@ -0,0 +1,357 @@
from __future__ import annotations
from bec_qthemes import material_icon
from qtpy import QtWidgets
from qtpy.QtCore import QEasingCurve, QParallelAnimationGroup, QPropertyAnimation, Qt, Signal
from qtpy.QtWidgets import (
QGraphicsOpacityEffect,
QHBoxLayout,
QLabel,
QScrollArea,
QToolButton,
QVBoxLayout,
QWidget,
)
from bec_widgets import SafeProperty, SafeSlot
from bec_widgets.applications.navigation_centre.reveal_animator import ANIMATION_DURATION
from bec_widgets.applications.navigation_centre.side_bar_components import (
DarkModeNavItem,
NavigationItem,
SectionHeader,
SideBarSeparator,
)
class SideBar(QScrollArea):
view_selected = Signal(str)
toggled = Signal(bool)
def __init__(
self,
parent=None,
title: str = "Control Panel",
collapsed_width: int = 56,
expanded_width: int = 250,
anim_duration: int = ANIMATION_DURATION,
):
super().__init__(parent=parent)
self.setObjectName("SideBar")
# private attributes
self._is_expanded = False
self._collapsed_width = collapsed_width
self._expanded_width = expanded_width
self._anim_duration = anim_duration
# containers
self.components = {}
self._item_opts: dict[str, dict] = {}
# Scroll area properties
self.setWidgetResizable(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setFrameShape(QtWidgets.QFrame.NoFrame)
self.setFixedWidth(self._collapsed_width)
# Content widget holding buttons for switching views
self.content = QWidget(self)
self.content_layout = QVBoxLayout(self.content)
self.content_layout.setContentsMargins(0, 0, 0, 0)
self.content_layout.setSpacing(4)
self.setWidget(self.content)
# Track active navigation item
self._active_id = None
# Top row with title and toggle button
self.toggle_row = QWidget(self)
self.toggle_row_layout = QHBoxLayout(self.toggle_row)
self.title_label = QLabel(title, self)
self.title_label.setObjectName("TopTitle")
self.title_label.setStyleSheet("font-weight: 600;")
self.title_fx = QGraphicsOpacityEffect(self.title_label)
self.title_label.setGraphicsEffect(self.title_fx)
self.title_fx.setOpacity(0.0)
self.title_label.setVisible(False) # TODO dirty trick to avoid layout shift
self.toggle = QToolButton(self)
self.toggle.setCheckable(False)
self.toggle.setIcon(material_icon("keyboard_arrow_right", convert_to_pixmap=False))
self.toggle.clicked.connect(self.on_expand)
self.toggle_row_layout.addWidget(self.title_label, 1, Qt.AlignLeft | Qt.AlignVCenter)
self.toggle_row_layout.addWidget(self.toggle, 1, Qt.AlignHCenter | Qt.AlignVCenter)
# To push the content up always
self._bottom_spacer = QtWidgets.QSpacerItem(
0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding
)
# Add core widgets to layout
self.content_layout.addWidget(self.toggle_row)
self.content_layout.addItem(self._bottom_spacer)
# Animations
self.width_anim = QPropertyAnimation(self, b"bar_width")
self.width_anim.setDuration(self._anim_duration)
self.width_anim.setEasingCurve(QEasingCurve.InOutCubic)
self.title_anim = QPropertyAnimation(self.title_fx, b"opacity")
self.title_anim.setDuration(self._anim_duration)
self.title_anim.setEasingCurve(QEasingCurve.InOutCubic)
self.group = QParallelAnimationGroup(self)
self.group.addAnimation(self.width_anim)
self.group.addAnimation(self.title_anim)
self.group.finished.connect(self._on_anim_finished)
app = QtWidgets.QApplication.instance()
if app is not None and hasattr(app, "theme") and hasattr(app.theme, "theme_changed"):
app.theme.theme_changed.connect(self._on_theme_changed)
@SafeProperty(int)
def bar_width(self) -> int:
"""
Get the current width of the side bar.
Returns:
int: The current width of the side bar.
"""
return self.width()
@bar_width.setter
def bar_width(self, width: int):
"""
Set the width of the side bar.
Args:
width(int): The new width of the side bar.
"""
self.setFixedWidth(width)
@SafeProperty(bool)
def is_expanded(self) -> bool:
"""
Check if the side bar is expanded.
Returns:
bool: True if the side bar is expanded, False otherwise.
"""
return self._is_expanded
@SafeSlot()
@SafeSlot(bool)
def on_expand(self):
"""
Toggle the expansion state of the side bar.
"""
self._is_expanded = not self._is_expanded
self.toggle.setIcon(
material_icon(
"keyboard_arrow_left" if self._is_expanded else "keyboard_arrow_right",
convert_to_pixmap=False,
)
)
if self._is_expanded:
self.toggle_row_layout.setAlignment(self.toggle, Qt.AlignRight | Qt.AlignVCenter)
self.group.stop()
# Setting limits for animations of the side bar
self.width_anim.setStartValue(self.width())
self.width_anim.setEndValue(
self._expanded_width if self._is_expanded else self._collapsed_width
)
self.title_anim.setStartValue(self.title_fx.opacity())
self.title_anim.setEndValue(1.0 if self._is_expanded else 0.0)
# Setting limits for animations of the components
for comp in self.components.values():
if hasattr(comp, "setup_animations"):
comp.setup_animations(self._is_expanded)
self.group.start()
if self._is_expanded:
# TODO do not like this trick, but it is what it is for now
self.title_label.setVisible(self._is_expanded)
for comp in self.components.values():
if hasattr(comp, "set_visible"):
comp.set_visible(self._is_expanded)
self.toggled.emit(self._is_expanded)
@SafeSlot()
def _on_anim_finished(self):
if not self._is_expanded:
self.toggle_row_layout.setAlignment(self.toggle, Qt.AlignHCenter | Qt.AlignVCenter)
# TODO do not like this trick, but it is what it is for now
self.title_label.setVisible(self._is_expanded)
for comp in self.components.values():
if hasattr(comp, "set_visible"):
comp.set_visible(self._is_expanded)
@SafeSlot(str)
def _on_theme_changed(self, theme_name: str):
# Refresh toggle arrow icon so it picks up the new theme
self.toggle.setIcon(
material_icon(
"keyboard_arrow_left" if self._is_expanded else "keyboard_arrow_right",
convert_to_pixmap=False,
)
)
# Refresh each component that supports it
for comp in self.components.values():
if hasattr(comp, "refresh_theme"):
comp.refresh_theme()
else:
comp.style().unpolish(comp)
comp.style().polish(comp)
comp.update()
self.style().unpolish(self)
self.style().polish(self)
self.update()
def add_section(self, title: str, id: str, position: int | None = None) -> SectionHeader:
"""
Add a section header to the side bar.
Args:
title(str): The title of the section.
id(str): Unique ID for the section.
position(int, optional): Position to insert the section header.
Returns:
SectionHeader: The created section header.
"""
header = SectionHeader(self, title, anim_duration=self._anim_duration)
position = position if position is not None else self.content_layout.count() - 1
self.content_layout.insertWidget(position, header)
for anim in header.animations:
self.group.addAnimation(anim)
self.components[id] = header
return header
def add_separator(
self, *, from_top: bool = True, position: int | None = None
) -> SideBarSeparator:
"""
Add a separator line to the side bar. Separators are treated like regular
items; you can place multiple separators anywhere using `from_top` and `position`.
"""
line = SideBarSeparator(self)
line.setStyleSheet("margin:12px;")
self._insert_nav_item(line, from_top=from_top, position=position)
return line
def add_item(
self,
icon: str,
title: str,
id: str,
mini_text: str | None = None,
position: int | None = None,
*,
from_top: bool = True,
toggleable: bool = True,
exclusive: bool = True,
) -> NavigationItem:
"""
Add a navigation item to the side bar.
Args:
icon(str): Icon name for the nav item.
title(str): Title for the nav item.
id(str): Unique ID for the nav item.
mini_text(str, optional): Short text for the nav item when sidebar is collapsed.
position(int, optional): Position to insert the nav item.
from_top(bool, optional): Whether to count position from the top or bottom.
toggleable(bool, optional): Whether the nav item is toggleable.
exclusive(bool, optional): Whether the nav item is exclusive.
Returns:
NavigationItem: The created navigation item.
"""
item = NavigationItem(
parent=self,
title=title,
icon_name=icon,
mini_text=mini_text,
toggleable=toggleable,
exclusive=exclusive,
anim_duration=self._anim_duration,
)
self._insert_nav_item(item, from_top=from_top, position=position)
for anim in item.build_animations():
self.group.addAnimation(anim)
self.components[id] = item
# Connect activation to activation logic, passing id unchanged
item.activated.connect(lambda id=id: self.activate_item(id))
return item
def activate_item(self, target_id: str, *, emit_signal: bool = True):
target = self.components.get(target_id)
if target is None:
return
# Non-toggleable acts like an action: do not change any toggled states
if hasattr(target, "toggleable") and not target.toggleable:
self._active_id = target_id
if emit_signal:
self.view_selected.emit(target_id)
return
is_exclusive = getattr(target, "exclusive", True)
if is_exclusive:
# Radio-like behavior among exclusive items only
for comp_id, comp in self.components.items():
if not isinstance(comp, NavigationItem):
continue
if comp is target:
comp.set_active(True)
else:
# Only untoggle other items that are also exclusive
if getattr(comp, "exclusive", True):
comp.set_active(False)
# Leave non-exclusive items as they are
else:
# Non-exclusive toggles independently
target.set_active(not target.is_active())
self._active_id = target_id
if emit_signal:
self.view_selected.emit(target_id)
def add_dark_mode_item(
self, id: str = "dark_mode", position: int | None = None
) -> DarkModeNavItem:
"""
Add a dark mode toggle item to the side bar.
Args:
id(str): Unique ID for the dark mode item.
position(int, optional): Position to insert the dark mode item.
Returns:
DarkModeNavItem: The created dark mode navigation item.
"""
item = DarkModeNavItem(parent=self, id=id, anim_duration=self._anim_duration)
# compute bottom insertion point (same semantics as from_top=False)
self._insert_nav_item(item, from_top=False, position=position)
for anim in item.build_animations():
self.group.addAnimation(anim)
self.components[id] = item
item.activated.connect(lambda id=id: self.activate_item(id))
return item
def _insert_nav_item(
self, item: QWidget, *, from_top: bool = True, position: int | None = None
):
if from_top:
base_index = self.content_layout.indexOf(self._bottom_spacer)
pos = base_index if position is None else min(base_index, position)
else:
base = self.content_layout.indexOf(self._bottom_spacer) + 1
pos = base if position is None else base + max(0, position)
self.content_layout.insertWidget(pos, item)
@@ -0,0 +1,370 @@
from __future__ import annotations
from bec_qthemes import material_icon
from qtpy import QtCore
from qtpy.QtCore import QEasingCurve, QPropertyAnimation, Qt
from qtpy.QtWidgets import (
QApplication,
QFrame,
QHBoxLayout,
QLabel,
QSizePolicy,
QToolButton,
QVBoxLayout,
QWidget,
)
from bec_widgets import SafeProperty
from bec_widgets.applications.navigation_centre.reveal_animator import (
ANIMATION_DURATION,
RevealAnimator,
)
def get_on_primary():
app = QApplication.instance()
if app is not None and hasattr(app, "theme"):
return app.theme.color("ON_PRIMARY")
return "#FFFFFF"
def get_fg():
app = QApplication.instance()
if app is not None and hasattr(app, "theme"):
return app.theme.color("FG")
return "#FFFFFF"
class SideBarSeparator(QFrame):
"""A horizontal line separator for use in SideBar."""
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("SideBarSeparator")
self.setFrameShape(QFrame.NoFrame)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setFixedHeight(2)
self.setProperty("variant", "separator")
class SectionHeader(QWidget):
"""A section header with a label and a horizontal line below."""
def __init__(self, parent=None, text: str = None, anim_duration: int = ANIMATION_DURATION):
super().__init__(parent)
self.setObjectName("SectionHeader")
self.lbl = QLabel(text, self)
self.lbl.setObjectName("SectionHeaderLabel")
self.lbl.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self._reveal = RevealAnimator(self.lbl, duration=anim_duration, initially_revealed=False)
self.line = SideBarSeparator(self)
lay = QVBoxLayout(self)
# keep your margins/spacing preferences here if needed
lay.setContentsMargins(12, 0, 12, 0)
lay.setSpacing(6)
lay.addWidget(self.lbl)
lay.addWidget(self.line)
self.animations = self.build_animations()
def build_animations(self) -> list[QPropertyAnimation]:
"""
Build and return animations for expanding/collapsing the sidebar.
Returns:
list[QPropertyAnimation]: List of animations.
"""
return self._reveal.animations()
def setup_animations(self, expanded: bool):
"""
Setup animations for expanding/collapsing the sidebar.
Args:
expanded(bool): True if the sidebar is expanded, False if collapsed.
"""
self._reveal.setup(expanded)
class NavigationItem(QWidget):
"""A nav tile with an icon + labels and an optional expandable body.
Provides animations for collapsed/expanded sidebar states via
build_animations()/setup_animations(), similar to SectionHeader.
"""
activated = QtCore.Signal()
def __init__(
self,
parent=None,
*,
title: str,
icon_name: str,
mini_text: str | None = None,
toggleable: bool = True,
exclusive: bool = True,
anim_duration: int = ANIMATION_DURATION,
):
super().__init__(parent=parent)
self.setObjectName("NavigationItem")
# Private attributes
self._title = title
self._icon_name = icon_name
self._mini_text = mini_text or title
self._toggleable = toggleable
self._toggled = False
self._exclusive = exclusive
# Main Icon
self.icon_btn = QToolButton(self)
self.icon_btn.setIcon(material_icon(self._icon_name, filled=False, convert_to_pixmap=False))
self.icon_btn.setAutoRaise(True)
self._icon_size_collapsed = QtCore.QSize(20, 20)
self._icon_size_expanded = QtCore.QSize(26, 26)
self.icon_btn.setIconSize(self._icon_size_collapsed)
# Remove QToolButton hover/pressed background/outline
self.icon_btn.setStyleSheet("""
QToolButton:hover { background: transparent; border: none; }
QToolButton:pressed { background: transparent; border: none; }
""")
# Mini label below icon
self.mini_lbl = QLabel(self._mini_text, self)
self.mini_lbl.setObjectName("NavMiniLabel")
self.mini_lbl.setAlignment(Qt.AlignCenter)
self.mini_lbl.setStyleSheet("font-size: 10px;")
self.reveal_mini_lbl = RevealAnimator(
widget=self.mini_lbl,
initially_revealed=True,
animate_width=False,
duration=anim_duration,
)
# Container for icon + mini label
self.mini_icon = QWidget(self)
mini_lay = QVBoxLayout(self.mini_icon)
mini_lay.setContentsMargins(0, 2, 0, 2)
mini_lay.setSpacing(2)
mini_lay.addWidget(self.icon_btn, 0, Qt.AlignCenter)
mini_lay.addWidget(self.mini_lbl, 0, Qt.AlignCenter)
# Title label
self.title_lbl = QLabel(self._title, self)
self.title_lbl.setObjectName("NavTitleLabel")
self.title_lbl.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.title_lbl.setStyleSheet("font-size: 13px;")
self.reveal_title_lbl = RevealAnimator(
widget=self.title_lbl,
initially_revealed=False,
animate_height=False,
duration=anim_duration,
)
self.title_lbl.setVisible(False) # TODO dirty trick to avoid layout shift
lay = QHBoxLayout(self)
lay.setContentsMargins(12, 2, 12, 2)
lay.setSpacing(6)
lay.addWidget(self.mini_icon, 0, Qt.AlignHCenter | Qt.AlignTop)
lay.addWidget(self.title_lbl, 1, Qt.AlignLeft | Qt.AlignVCenter)
self.icon_size_anim = QPropertyAnimation(self.icon_btn, b"iconSize")
self.icon_size_anim.setDuration(anim_duration)
self.icon_size_anim.setEasingCurve(QEasingCurve.InOutCubic)
# Connect icon button to emit activation
self.icon_btn.clicked.connect(self._emit_activated)
self.setMouseTracking(True)
self.setAttribute(Qt.WA_StyledBackground, True)
def is_active(self) -> bool:
"""Return whether the item is currently active/selected."""
return self.property("toggled") is True
def build_animations(self) -> list[QPropertyAnimation]:
"""
Build and return animations for expanding/collapsing the sidebar.
Returns:
list[QPropertyAnimation]: List of animations.
"""
return (
self.reveal_title_lbl.animations()
+ self.reveal_mini_lbl.animations()
+ [self.icon_size_anim]
)
def setup_animations(self, expanded: bool):
"""
Setup animations for expanding/collapsing the sidebar.
Args:
expanded(bool): True if the sidebar is expanded, False if collapsed.
"""
self.reveal_mini_lbl.setup(not expanded)
self.reveal_title_lbl.setup(expanded)
self.icon_size_anim.setStartValue(self.icon_btn.iconSize())
self.icon_size_anim.setEndValue(
self._icon_size_expanded if expanded else self._icon_size_collapsed
)
def set_visible(self, visible: bool):
"""Set visibility of the title label."""
self.title_lbl.setVisible(visible)
def _emit_activated(self):
self.activated.emit()
def set_active(self, active: bool):
"""
Set the active/selected state of the item.
Args:
active(bool): True to set active, False to deactivate.
"""
self.setProperty("toggled", active)
self.toggled = active
# ensure style refresh
self.style().unpolish(self)
self.style().polish(self)
self.update()
def mousePressEvent(self, event):
self.activated.emit()
super().mousePressEvent(event)
@SafeProperty(bool)
def toggleable(self) -> bool:
"""
Whether the item is toggleable (like a button) or not (like an action).
Returns:
bool: True if toggleable, False otherwise.
"""
return self._toggleable
@toggleable.setter
def toggleable(self, value: bool):
"""
Set whether the item is toggleable (like a button) or not (like an action).
Args:
value(bool): True to make toggleable, False otherwise.
"""
self._toggleable = bool(value)
@SafeProperty(bool)
def toggled(self) -> bool:
"""
Whether the item is currently toggled/selected.
Returns:
bool: True if toggled, False otherwise.
"""
return self._toggled
@toggled.setter
def toggled(self, value: bool):
"""
Set whether the item is currently toggled/selected.
Args:
value(bool): True to set toggled, False to untoggle.
"""
self._toggled = value
if value:
new_icon = material_icon(
self._icon_name, filled=True, color=get_on_primary(), convert_to_pixmap=False
)
else:
new_icon = material_icon(
self._icon_name, filled=False, color=get_fg(), convert_to_pixmap=False
)
self.icon_btn.setIcon(new_icon)
# Re-polish so QSS applies correct colors to icon/labels
for w in (self, self.icon_btn, self.title_lbl, self.mini_lbl):
w.style().unpolish(w)
w.style().polish(w)
w.update()
@SafeProperty(bool)
def exclusive(self) -> bool:
"""
Whether the item is exclusive in its toggle group.
Returns:
bool: True if exclusive, False otherwise.
"""
return self._exclusive
@exclusive.setter
def exclusive(self, value: bool):
"""
Set whether the item is exclusive in its toggle group.
Args:
value(bool): True to make exclusive, False otherwise.
"""
self._exclusive = bool(value)
def refresh_theme(self):
# Recompute icon/label colors according to current theme and state
# Trigger the toggled setter to rebuild the icon with the correct color
self.toggled = self._toggled
# Ensure QSS-driven text/icon colors refresh
for w in (self, self.icon_btn, self.title_lbl, self.mini_lbl):
w.style().unpolish(w)
w.style().polish(w)
w.update()
class DarkModeNavItem(NavigationItem):
"""Bottom action item that toggles app theme and updates its icon/text."""
def __init__(
self, parent=None, *, id: str = "dark_mode", anim_duration: int = ANIMATION_DURATION
):
super().__init__(
parent=parent,
title="Dark mode",
icon_name="dark_mode",
mini_text="Dark",
toggleable=False, # action-like, no selection highlight changes
exclusive=False,
anim_duration=anim_duration,
)
self._id = id
self._sync_from_qapp_theme()
self.activated.connect(self.toggle_theme)
def _qapp_dark_enabled(self) -> bool:
qapp = QApplication.instance()
return bool(getattr(getattr(qapp, "theme", None), "theme", None) == "dark")
def _sync_from_qapp_theme(self):
is_dark = self._qapp_dark_enabled()
# Update labels
self.title_lbl.setText("Light mode" if is_dark else "Dark mode")
self.mini_lbl.setText("Light" if is_dark else "Dark")
# Update icon
self.icon_btn.setIcon(
material_icon("light_mode" if is_dark else "dark_mode", convert_to_pixmap=False)
)
def refresh_theme(self):
self._sync_from_qapp_theme()
for w in (self, self.icon_btn, self.title_lbl, self.mini_lbl):
w.style().unpolish(w)
w.style().polish(w)
w.update()
def toggle_theme(self):
"""Toggle application theme and update icon/text."""
from bec_widgets.utils.colors import apply_theme
is_dark = self._qapp_dark_enabled()
apply_theme("light" if is_dark else "dark")
self._sync_from_qapp_theme()
@@ -0,0 +1,140 @@
from qtpy.QtWidgets import QWidget
from bec_widgets.applications.views.developer_view.developer_widget import DeveloperWidget
from bec_widgets.applications.views.view import ViewBase, ViewTourSteps
class DeveloperView(ViewBase):
"""
A view for users to write scripts and macros and execute them within the application.
"""
def __init__(
self,
parent: QWidget | None = None,
content: QWidget | None = None,
*,
view_id: str | None = None,
title: str | None = None,
**kwargs,
):
super().__init__(parent=parent, content=content, view_id=view_id, title=title, **kwargs)
self.developer_widget = DeveloperWidget(parent=self)
self.set_content(self.developer_widget)
def register_tour_steps(self, guided_tour, main_app) -> ViewTourSteps | None:
"""Register Developer View components with the guided tour.
Args:
guided_tour: The GuidedTour instance to register with.
main_app: The main application instance (for accessing set_current).
Returns:
ViewTourSteps | None: Model containing view title and step IDs.
"""
step_ids = []
dev_widget = self.developer_widget
# IDE Toolbar
def get_ide_toolbar():
main_app.set_current("developer_view")
return (dev_widget.toolbar, None)
step_id = guided_tour.register_widget(
widget=get_ide_toolbar,
title="IDE Toolbar",
text="Quick access to save files, execute scripts, and configure IDE settings. Use the toolbar to manage your code and execution.",
)
step_ids.append(step_id)
# IDE Explorer
def get_ide_explorer():
main_app.set_current("developer_view")
return (dev_widget.explorer_dock.widget(), None)
step_id = guided_tour.register_widget(
widget=get_ide_explorer,
title="File Explorer",
text="Browse and manage your macro files. Create new files, open existing ones, and organize your scripts.",
)
step_ids.append(step_id)
# IDE Editor
def get_ide_editor():
main_app.set_current("developer_view")
return (dev_widget.monaco_dock.widget(), None)
step_id = guided_tour.register_widget(
widget=get_ide_editor,
title="Code Editor",
text="Write and edit Python code with syntax highlighting, auto-completion, and signature help. Monaco editor provides a modern coding experience.",
)
step_ids.append(step_id)
# IDE Console
def get_ide_console():
main_app.set_current("developer_view")
return (dev_widget.console_dock.widget(), None)
step_id = guided_tour.register_widget(
widget=get_ide_console,
title="BEC Shell Console",
text="Interactive Python console with BEC integration. Execute commands, test code snippets, and interact with the BEC system in real-time.",
)
step_ids.append(step_id)
# IDE Plotting Area
def get_ide_plotting():
main_app.set_current("developer_view")
return (dev_widget.plotting_ads, None)
step_id = guided_tour.register_widget(
widget=get_ide_plotting,
title="Plotting Area",
text="View plots and visualizations generated by your scripts. Arrange multiple plots in a flexible layout.",
)
step_ids.append(step_id)
return ViewTourSteps(view_title="Developer View", step_ids=step_ids)
if __name__ == "__main__":
import sys
from bec_qthemes import apply_theme
from qtpy.QtWidgets import QApplication
from bec_widgets.applications.main_app import BECMainApp
app = QApplication(sys.argv)
apply_theme("dark")
_app = BECMainApp()
screen = app.primaryScreen()
screen_geometry = screen.availableGeometry()
screen_width = screen_geometry.width()
screen_height = screen_geometry.height()
# 70% of screen height, keep 16:9 ratio
height = int(screen_height * 0.9)
width = int(height * (16 / 9))
# If width exceeds screen width, scale down
if width > screen_width * 0.9:
width = int(screen_width * 0.9)
height = int(width / (16 / 9))
_app.resize(width, height)
developer_view = DeveloperView()
_app.add_view(
icon="code_blocks",
title="IDE",
widget=developer_view,
view_id="developer_view",
exclusive=True,
)
_app.show()
# developer_view.show()
# developer_view.setWindowTitle("Developer View")
# developer_view.resize(1920, 1080)
# developer_view.set_stretch(horizontal=[1, 3, 2], vertical=[5, 5]) #can be set during runtime
sys.exit(app.exec_())
@@ -0,0 +1,432 @@
from __future__ import annotations
import re
import markdown
from bec_lib.endpoints import MessageEndpoints
from bec_lib.script_executor import upload_script
from bec_qthemes import material_icon
from qtpy.QtGui import QKeySequence, QShortcut
from qtpy.QtWidgets import QTextEdit
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.toolbars.actions import MaterialIconAction
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
from bec_widgets.widgets.containers.dock_area.basic_dock_area import DockAreaWidget
from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea
from bec_widgets.widgets.containers.qt_ads import CDockWidget
from bec_widgets.widgets.editors.monaco.monaco_dock import MonacoDock
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
from bec_widgets.widgets.editors.web_console.web_console import BECShell, WebConsole
from bec_widgets.widgets.utility.ide_explorer.ide_explorer import IDEExplorer
def markdown_to_html(md_text: str) -> str:
"""Convert Markdown with syntax highlighting to HTML (Qt-compatible)."""
# Preprocess: convert consecutive >>> lines to Python code blocks
def replace_python_examples(match):
indent = match.group(1)
examples = match.group(2)
# Remove >>> prefix and clean up the code
lines = []
for line in examples.strip().split("\n"):
line = line.strip()
if line.startswith(">>> "):
lines.append(line[4:]) # Remove '>>> '
elif line.startswith(">>>"):
lines.append(line[3:]) # Remove '>>>'
code = "\n".join(lines)
return f"{indent}```python\n{indent}{code}\n{indent}```"
# Match one or more consecutive >>> lines (with same indentation)
pattern = r"^(\s*)((?:>>> .+(?:\n|$))+)"
md_text = re.sub(pattern, replace_python_examples, md_text, flags=re.MULTILINE)
extensions = ["fenced_code", "codehilite", "tables", "sane_lists"]
html = markdown.markdown(
md_text,
extensions=extensions,
extension_configs={
"codehilite": {"linenums": False, "guess_lang": False, "noclasses": True}
},
output_format="html",
)
# Remove hardcoded background colors that conflict with themes
html = re.sub(r'style="background: #[^"]*"', 'style="background: transparent"', html)
html = re.sub(r"background: #[^;]*;", "", html)
# Add CSS to force code blocks to wrap
css = """
<style>
pre, code {
white-space: pre-wrap !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
}
.codehilite pre {
white-space: pre-wrap !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
}
</style>
"""
return css + html
class DeveloperWidget(DockAreaWidget):
RPC = False
PLUGIN = False
def __init__(self, parent=None, **kwargs):
super().__init__(parent=parent, variant="compact", **kwargs)
# Promote toolbar above the dock manager provided by the base class
self.toolbar = ModularToolBar(self)
self.init_developer_toolbar()
self._root_layout.insertWidget(0, self.toolbar)
# Initialize the widgets
self.explorer = IDEExplorer(self)
self.explorer.setObjectName("Explorer")
self.console = BECShell(self, rpc_exposed=False)
self.console.setObjectName("BEC Shell")
self.terminal = WebConsole(self, rpc_exposed=False)
self.terminal.setObjectName("Terminal")
self.monaco = MonacoDock(self, rpc_exposed=False, rpc_passthrough_children=False)
self.monaco.setObjectName("MonacoEditor")
self.monaco.save_enabled.connect(self._on_save_enabled_update)
self.plotting_ads = BECDockArea(
self,
mode="plot",
default_add_direction="bottom",
profile_namespace="developer_plotting",
auto_profile_namespace=False,
enable_profile_management=False,
variant="compact",
)
self.plotting_ads.setObjectName("PlottingArea")
self.signature_help = QTextEdit(self)
self.signature_help.setObjectName("Signature Help")
self.signature_help.setAcceptRichText(True)
self.signature_help.setReadOnly(True)
self.signature_help.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
opt = self.signature_help.document().defaultTextOption()
opt.setWrapMode(opt.WrapMode.WrapAnywhere)
self.signature_help.document().setDefaultTextOption(opt)
self.monaco.signature_help.connect(
lambda text: self.signature_help.setHtml(markdown_to_html(text))
)
self._current_script_id: str | None = None
self.script_editor_tab = None
self._initialize_layout()
# Connect editor signals
self.explorer.file_open_requested.connect(self._open_new_file)
self.monaco.macro_file_updated.connect(self.explorer.refresh_macro_file)
self.monaco.focused_editor.connect(self._on_focused_editor_changed)
self.toolbar.show_bundles(["save", "execution", "settings"])
def _initialize_layout(self) -> None:
"""Create the default dock arrangement for the developer workspace."""
# Monaco editor as the central dock
self.monaco_dock = self.new(
self.monaco,
closable=False,
floatable=False,
movable=False,
return_dock=True,
show_title_bar=False,
show_settings_action=False,
title_buttons={"float": False, "close": False, "menu": False},
# promote_central=True,
)
# Explorer on the left without a title bar
self.explorer_dock = self.new(
self.explorer,
where="left",
closable=False,
floatable=False,
movable=False,
return_dock=True,
show_title_bar=False,
)
# Console and terminal tabbed along the bottom
self.console_dock = self.new(
self.console,
relative_to=self.monaco_dock,
where="bottom",
closable=False,
floatable=False,
movable=False,
return_dock=True,
title_buttons={"float": True, "close": False},
)
self.terminal_dock = self.new(
self.terminal,
closable=False,
floatable=False,
movable=False,
tab_with=self.console_dock,
return_dock=True,
title_buttons={"float": False, "close": False},
)
# Plotting area on the right with signature help tabbed alongside
self.plotting_ads_dock = self.new(
self.plotting_ads,
where="right",
closable=False,
floatable=False,
movable=False,
return_dock=True,
title_buttons={"float": True},
)
self.signature_dock = self.new(
self.signature_help,
closable=False,
floatable=False,
movable=False,
tab_with=self.plotting_ads_dock,
return_dock=True,
title_buttons={"float": False, "close": False},
)
self.set_layout_ratios(horizontal=[2, 5, 3], vertical=[7, 3])
def init_developer_toolbar(self):
"""Initialize the developer toolbar with necessary actions and widgets."""
save_button = MaterialIconAction(
icon_name="save", tooltip="Save", label_text="Save", filled=True, parent=self
)
save_button.action.triggered.connect(self.on_save)
self.toolbar.components.add_safe("save", save_button)
save_as_button = MaterialIconAction(
icon_name="save_as", tooltip="Save As", label_text="Save As", parent=self
)
self.toolbar.components.add_safe("save_as", save_as_button)
save_as_button.action.triggered.connect(self.on_save_as)
save_bundle = ToolbarBundle("save", self.toolbar.components)
save_bundle.add_action("save")
save_bundle.add_action("save_as")
self.toolbar.add_bundle(save_bundle)
run_action = MaterialIconAction(
icon_name="play_arrow",
tooltip="Run current file",
label_text="Run",
filled=True,
parent=self,
)
run_action.action.triggered.connect(self.on_execute)
self.toolbar.components.add_safe("run", run_action)
stop_action = MaterialIconAction(
icon_name="stop",
tooltip="Stop current execution",
label_text="Stop",
filled=True,
parent=self,
)
stop_action.action.triggered.connect(self.on_stop)
self.toolbar.components.add_safe("stop", stop_action)
execution_bundle = ToolbarBundle("execution", self.toolbar.components)
execution_bundle.add_action("run")
execution_bundle.add_action("stop")
self.toolbar.add_bundle(execution_bundle)
vim_action = MaterialIconAction(
icon_name="vim",
tooltip="Toggle Vim Mode",
label_text="Vim",
filled=True,
parent=self,
checkable=True,
)
self.toolbar.components.add_safe("vim", vim_action)
vim_action.action.triggered.connect(self.on_vim_triggered)
settings_bundle = ToolbarBundle("settings", self.toolbar.components)
settings_bundle.add_action("vim")
self.toolbar.add_bundle(settings_bundle)
save_shortcut = QShortcut(QKeySequence("Ctrl+S"), self)
save_shortcut.activated.connect(self.on_save)
save_as_shortcut = QShortcut(QKeySequence("Ctrl+Shift+S"), self)
save_as_shortcut.activated.connect(self.on_save_as)
def _open_new_file(self, file_name: str, scope: str):
self.monaco.open_file(file_name, scope)
# Set read-only mode for shared files
if "shared" in scope:
self.monaco.set_file_readonly(file_name, True)
# Add appropriate icon based on file type
if "script" in scope:
# Use script icon for script files
icon = material_icon("script", size=(24, 24))
self.monaco.set_file_icon(file_name, icon)
elif "macro" in scope:
# Use function icon for macro files
icon = material_icon("function", size=(24, 24))
self.monaco.set_file_icon(file_name, icon)
@SafeSlot()
def on_save(self):
"""Save the currently focused file in the Monaco editor."""
self.monaco.save_file()
@SafeSlot()
def on_save_as(self):
"""Save the currently focused file in the Monaco editor with a 'Save As' dialog."""
self.monaco.save_file(force_save_as=True)
@SafeSlot()
def on_vim_triggered(self):
"""Toggle Vim mode in the Monaco editor."""
self.monaco.set_vim_mode(self.toolbar.components.get_action("vim").action.isChecked())
@SafeSlot(bool)
def _on_save_enabled_update(self, enabled: bool):
self.toolbar.components.get_action("save").action.setEnabled(enabled)
self.toolbar.components.get_action("save_as").action.setEnabled(enabled)
@SafeSlot()
def on_execute(self):
"""Upload and run the currently focused script in the Monaco editor."""
self.script_editor_tab = self.monaco.last_focused_editor
if not self.script_editor_tab:
return
widget = self.script_editor_tab.widget()
if not isinstance(widget, MonacoWidget):
return
if widget.modified:
# Save the file before execution if there are unsaved changes
self.monaco.save_file()
if widget.modified:
# If still modified, user likely cancelled save dialog
return
self.current_script_id = upload_script(self.client.connector, widget.get_text())
self.console.write(f'bec._run_script("{self.current_script_id}")')
print(f"Uploaded script with ID: {self.current_script_id}")
@SafeSlot()
def on_stop(self):
"""Stop the execution of the currently running script"""
if not self.current_script_id:
return
self.console.send_ctrl_c()
@property
def current_script_id(self):
"""Get the ID of the currently running script."""
return self._current_script_id
@current_script_id.setter
def current_script_id(self, value: str | None):
"""
Set the ID of the currently running script.
Args:
value (str | None): The script ID to set.
Raises:
ValueError: If the provided value is not a string or None.
"""
if value is not None and not isinstance(value, str):
raise ValueError("Script ID must be a string.")
old_script_id = self._current_script_id
self._current_script_id = value
self._update_subscription(value, old_script_id)
def _update_subscription(self, new_script_id: str | None, old_script_id: str | None):
if old_script_id is not None:
self.bec_dispatcher.disconnect_slot(
self.on_script_execution_info, MessageEndpoints.script_execution_info(old_script_id)
)
if new_script_id is not None:
self.bec_dispatcher.connect_slot(
self.on_script_execution_info, MessageEndpoints.script_execution_info(new_script_id)
)
@SafeSlot(CDockWidget)
def _on_focused_editor_changed(self, tab_widget: CDockWidget):
"""
Disable the run / stop buttons if the focused editor is a macro file.
Args:
tab_widget: The currently focused tab widget in the Monaco editor.
"""
if not isinstance(tab_widget, CDockWidget):
return
widget = tab_widget.widget()
if not isinstance(widget, MonacoWidget):
return
file_scope = widget.metadata.get("scope", "")
run_action = self.toolbar.components.get_action("run")
stop_action = self.toolbar.components.get_action("stop")
if "macro" in file_scope:
run_action.action.setEnabled(False)
stop_action.action.setEnabled(False)
else:
run_action.action.setEnabled(True)
stop_action.action.setEnabled(True)
@SafeSlot(dict, dict)
def on_script_execution_info(self, content: dict, metadata: dict):
"""
Handle script execution info messages to update the editor highlights.
Args:
content (dict): The content of the message containing execution info.
metadata (dict): Additional metadata for the message.
"""
print(f"Script execution info: {content}")
current_lines = content.get("current_lines")
if self.script_editor_tab is None:
return
widget = self.script_editor_tab.widget()
if not isinstance(widget, MonacoWidget):
return
if not current_lines:
widget.clear_highlighted_lines()
return
line_number = current_lines[0]
widget.clear_highlighted_lines()
widget.set_highlighted_lines(line_number, line_number)
def cleanup(self):
"""Clean up resources used by the developer widget."""
self.delete_all()
return super().cleanup()
if __name__ == "__main__":
import sys
from bec_qthemes import apply_theme
from qtpy.QtWidgets import QApplication
from bec_widgets.applications.main_app import BECMainApp
app = QApplication(sys.argv)
apply_theme("dark")
_app = BECMainApp()
_app.show()
# developer_view.show()
# developer_view.setWindowTitle("Developer View")
# developer_view.resize(1920, 1080)
# developer_view.set_stretch(horizontal=[1, 3, 2], vertical=[5, 5]) #can be set during runtime
sys.exit(app.exec_())
@@ -0,0 +1,2 @@
from .config_choice_dialog import ConfigChoiceDialog
from .device_form_dialog import DeviceFormDialog
@@ -0,0 +1,49 @@
"""Dialog to choose config loading method: replace, add or cancel."""
from enum import IntEnum
from qtpy.QtWidgets import QDialog, QDialogButtonBox, QLabel, QSizePolicy, QVBoxLayout
class ConfigChoiceDialog(QDialog):
class Result(IntEnum):
CANCEL = QDialog.Rejected
ADD = 2
REPLACE = 3
def __init__(
self,
parent=None,
custom_label: str = "Do you want to replace the current config or add to it?",
):
super().__init__(parent)
self.setWindowTitle("Load Config")
layout = QVBoxLayout(self)
label = QLabel(custom_label)
label.setWordWrap(True)
layout.addWidget(label)
# Use QDialogButtonBox for native layout
self.button_box = QDialogButtonBox(self)
self.cancel_btn = self.button_box.addButton(
"Cancel", QDialogButtonBox.ButtonRole.ActionRole # RejectRole will be next to Accept...
)
self.replace_btn = self.button_box.addButton(
"Replace", QDialogButtonBox.ButtonRole.AcceptRole
)
self.add_btn = self.button_box.addButton("Add", QDialogButtonBox.ButtonRole.AcceptRole)
layout.addWidget(self.button_box)
for btn in [self.replace_btn, self.add_btn, self.cancel_btn]:
btn.setMinimumWidth(80)
btn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
# Connections using native done(int)
self.replace_btn.clicked.connect(lambda: self.done(self.Result.REPLACE))
self.add_btn.clicked.connect(lambda: self.done(self.Result.ADD))
self.cancel_btn.clicked.connect(lambda: self.done(self.Result.CANCEL))
self.replace_btn.setFocus()
@@ -0,0 +1,447 @@
"""Dialogs for device configuration forms and ophyd testing."""
from typing import Any, Iterable, Tuple
from bec_lib.atlas_models import Device as DeviceModel
from bec_lib.logger import bec_logger
from ophyd_devices.interfaces.device_config_templates.ophyd_templates import OPHYD_DEVICE_TEMPLATES
from qtpy import QtCore, QtWidgets
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.widgets.control.device_manager.components import OphydValidation
from bec_widgets.widgets.control.device_manager.components.device_config_template.device_config_template import (
DeviceConfigTemplate,
)
from bec_widgets.widgets.control.device_manager.components.device_config_template.template_items import (
validate_name,
)
from bec_widgets.widgets.control.device_manager.components.ophyd_validation import (
ConfigStatus,
ConnectionStatus,
format_error_to_md,
)
DEFAULT_DEVICE = "CustomDevice"
_ValidationResultIter = Iterable[Tuple[dict[str, Any], ConfigStatus, ConnectionStatus, str]]
logger = bec_logger.logger
class DeviceManagerOphydValidationDialog(QtWidgets.QDialog):
"""Popup dialog to test Ophyd device configurations interactively."""
def __init__(self, parent=None, config: dict | None = None): # type: ignore
super().__init__(parent)
self.setWindowTitle("Device Manager Ophyd Test")
self._config_status = ConfigStatus.UNKNOWN.value
self._connection_status = ConnectionStatus.UNKNOWN.value
self._validated_config: dict = {}
self._validation_msg: str = ""
layout = QtWidgets.QVBoxLayout(self)
# Core test widget
self.device_manager_ophyd_test = OphydValidation()
layout.addWidget(self.device_manager_ophyd_test)
# Log/Markdown box for messages
self.text_box = QtWidgets.QTextEdit()
self.text_box.setReadOnly(True)
layout.addWidget(self.text_box)
# Load and apply configuration
config = config or {}
device_name = config.get("name", None)
if device_name:
self.device_manager_ophyd_test.add_device_to_keep_visible_after_validation(device_name)
# Dialog Buttons: equal size, stacked horizontally
button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.StandardButton.Close)
for button in button_box.buttons():
button.setSizePolicy(
QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed
)
button.clicked.connect(self.accept)
# button_box.setCenterButtons(False)
layout.addWidget(button_box)
self.device_manager_ophyd_test.validation_completed.connect(self._on_device_validated)
self._resize_dialog()
self.finished.connect(self._finished)
# Add and test device config
self.device_manager_ophyd_test.change_device_configs([config], added=True, connect=True)
def _resize_dialog(self):
"""Resize the dialog based on the screen size."""
app: QtCore.QCoreApplication = QtWidgets.QApplication.instance()
screen = app.primaryScreen()
screen_geometry = screen.availableGeometry()
screen_width = screen_geometry.width()
screen_height = screen_geometry.height()
# 70% of screen height, keep 4:3 ratio
height = int(screen_height * 0.7)
width = int(height * (4 / 3))
# If width exceeds screen width, scale down
if width > screen_width * 0.9:
width = int(screen_width * 0.9)
height = int(width / (4 / 3))
self.resize(width, height)
def _on_device_validated(
self, device_config: dict, config_status: int, connection_status: int, validation_msg: str
):
device_name = device_config.get("name", "")
self._config_status = config_status
self._connection_status = connection_status
self._validated_config = device_config
self._validation_msg = validation_msg
self.text_box.setMarkdown(format_error_to_md(device_name, validation_msg))
@SafeSlot(int)
def _finished(self, state: int):
self.device_manager_ophyd_test.close()
self.device_manager_ophyd_test.deleteLater()
@property
def validation_result(self) -> tuple[dict, int, int, str]:
"""
Return the result of the validation as a tuple of
Returns:
result (Tuple[dict, int, int]): A tuple containing:
validated_config (dict): The validated device configuration.
config_status (int): The configuration status.
connection_status (int): The connection status.
"""
return (
self._validated_config,
self._config_status,
self._connection_status,
self._validation_msg,
)
class DeviceFormDialog(QtWidgets.QDialog):
# Signal emitted when device configuration is accepted, only
# emitted when the user clicks the "Add Device" button
# The integer values indicate if the device config was
# validated: config_status, connection_status
accepted_data = QtCore.Signal(dict, int, int, str, str)
def __init__(self, parent=None, add_btn_text: str = "Add Device"): # type: ignore
super().__init__(parent)
# Track old device name if config is edited
self._old_device_name: str = ""
# Config validation result
self._validation_result: tuple[dict, int, int, str] = (
{},
ConfigStatus.UNKNOWN.value,
ConnectionStatus.UNKNOWN.value,
"",
)
# Group to variants mapping
self._group_variants: dict[str, list[str]] = {
group: [variant for variant in variants.keys()]
for group, variants in OPHYD_DEVICE_TEMPLATES.items()
}
self._control_widgets: dict[str, QtWidgets.QWidget] = {}
# Setup layout
self.setWindowTitle("Device Config Dialog")
layout = QtWidgets.QVBoxLayout(self)
# Control panel
self._control_box = self.create_control_panel()
layout.addWidget(self._control_box)
# Device config template display
self._device_config_template = DeviceConfigTemplate(parent=self)
self._frame = QtWidgets.QFrame()
self._frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
self._frame.setFrameShadow(QtWidgets.QFrame.Raised)
frame_layout = QtWidgets.QVBoxLayout(self._frame)
frame_layout.addWidget(self._device_config_template)
layout.addWidget(self._frame)
# Custom buttons
self.add_btn = QtWidgets.QPushButton(add_btn_text)
self.test_connection_btn = QtWidgets.QPushButton("Test Connection")
self.cancel_btn = QtWidgets.QPushButton("Cancel")
self.reset_btn = QtWidgets.QPushButton("Reset Form")
btn_box = QtWidgets.QDialogButtonBox(self)
btn_box.addButton(self.cancel_btn, QtWidgets.QDialogButtonBox.ButtonRole.RejectRole)
btn_box.addButton(self.reset_btn, QtWidgets.QDialogButtonBox.ButtonRole.ActionRole)
btn_box.addButton(
self.test_connection_btn, QtWidgets.QDialogButtonBox.ButtonRole.ActionRole
)
btn_box.addButton(self.add_btn, QtWidgets.QDialogButtonBox.ButtonRole.AcceptRole)
for btn in btn_box.buttons():
btn.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
layout.addWidget(btn_box)
frame_layout.addWidget(btn_box)
# Connect signals to explicit slots
self.add_btn.clicked.connect(self._add_config)
self.test_connection_btn.clicked.connect(self._test_connection)
self.reset_btn.clicked.connect(self._reset_config)
self.cancel_btn.clicked.connect(self._reject_config)
# layout.addWidget(self._device_config_template)
self.update_variant_combo(self._control_widgets["group_combo"].currentText())
self.finished.connect(self._finished)
# Wait dialog when adding config
self._wait_dialog: QtWidgets.QProgressDialog | None = None
@SafeSlot(int)
def _finished(self, state: int):
for widget in self._control_widgets.values():
widget.close()
widget.deleteLater()
if self._wait_dialog is not None:
self._wait_dialog.close()
self._wait_dialog.deleteLater()
@property
def config_validation_result(self) -> tuple[dict, int, int, str]:
"""Return the result of the last configuration validation."""
return self._validation_result
@config_validation_result.setter
def config_validation_result(self, result: tuple[dict, int, int, str]):
self._validation_result = result
def set_device_config(self, device_config: dict):
"""Set the device configuration in the template form."""
# Figure out which group and variant this config belongs to
device_class = device_config.get("deviceClass", None)
for group, variants in OPHYD_DEVICE_TEMPLATES.items():
for variant, template_info in variants.items():
if template_info.get("deviceClass", None) == device_class:
# Found the matching group and variant
self._control_widgets["group_combo"].setCurrentText(group)
self.update_variant_combo(group)
self._control_widgets["variant_combo"].setCurrentText(variant)
self._device_config_template.set_config_fields(device_config)
return
# If no match found, set to default
self._control_widgets["group_combo"].setCurrentText(DEFAULT_DEVICE)
self.update_variant_combo(DEFAULT_DEVICE)
self._device_config_template.set_config_fields(device_config)
self._old_device_name = device_config.get("name", "")
def sizeHint(self) -> QtCore.QSize:
return QtCore.QSize(1600, 1000)
def create_control_panel(self) -> QtWidgets.QGroupBox:
self._control_box = QtWidgets.QGroupBox("Choose a Device Group")
layout = QtWidgets.QGridLayout(self._control_box)
group_label = QtWidgets.QLabel("Device Group:")
layout.addWidget(group_label, 0, 0)
group_combo = QtWidgets.QComboBox()
group_combo.addItems(self._group_variants.keys())
self._control_widgets["group_combo"] = group_combo
layout.addWidget(group_combo, 1, 0)
variant_label = QtWidgets.QLabel("Variants:")
layout.addWidget(variant_label, 0, 1)
variant_combo = QtWidgets.QComboBox()
self._control_widgets["variant_combo"] = variant_combo
layout.addWidget(variant_combo, 1, 1)
group_combo.currentTextChanged.connect(self.update_variant_combo)
variant_combo.currentTextChanged.connect(self.update_device_config_template)
return self._control_box
def update_variant_combo(self, group_name: str):
variant_combo = self._control_widgets["variant_combo"]
variant_combo.clear()
variant_combo.addItems(self._group_variants.get(group_name, []))
if variant_combo.count() <= 1:
variant_combo.setEnabled(False)
else:
variant_combo.setEnabled(True)
def update_device_config_template(self, variant_name: str):
group_name = self._control_widgets["group_combo"].currentText()
template_info = OPHYD_DEVICE_TEMPLATES.get(group_name, {}).get(variant_name, {})
if template_info:
self._device_config_template.change_template(template_info)
else:
self._device_config_template.change_template(
OPHYD_DEVICE_TEMPLATES[DEFAULT_DEVICE][DEFAULT_DEVICE]
)
def _create_validation_dialog(self) -> QtWidgets.QProgressDialog:
"""
Create and show a validation progress dialog while validating the device configuration.
The dialog will be modal and prevent user interaction until validation is complete.
"""
wait_dialog = QtWidgets.QProgressDialog(
"Validating config... please wait", None, 0, 0, parent=self
)
wait_dialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
wait_dialog.setCancelButton(None)
wait_dialog.setMinimumDuration(0)
return wait_dialog
def _create_and_run_ophyd_validation(self, config: dict[str, Any]) -> OphydValidation:
"""Run ophyd validation test on the current device configuration."""
ophyd_validation = OphydValidation(parent=self)
ophyd_validation.validation_completed.connect(self._handle_validation_result)
ophyd_validation.multiple_validations_completed.connect(
self._handle_devices_already_in_session_results
)
# NOTE Use singleShot here to ensure that the signal is emitted after all other scheduled
# tasks in the event loop are processed. This avoids potential deadlocks. In particular,
# this is relevant for the _wait_dialog exec which opens a modal dialog during validation
# and therefore must not have the signal emitted immediately in the same event loop iteration.
# Otherwise, the callback may be scheduled before the dialog is shown resulting in a deadlock.
QtCore.QTimer.singleShot(
0, lambda: ophyd_validation.change_device_configs([config], True, False)
)
return ophyd_validation
@SafeSlot(list)
def _handle_devices_already_in_session_results(
self, validation_results: _ValidationResultIter
) -> None:
"""Handle completion if device is already in session."""
if len(validation_results) != 1:
logger.error(
"Expected a single device validation result, but got multiple. Using first result."
)
result = validation_results[0] if len(validation_results) > 0 else None
if result is None:
logger.error(
f"Received validation results: {validation_results} of unexpected length 0. Returning."
)
return
device_config, config_status, connection_status, validation_msg = result
self._handle_validation_result(
device_config, config_status, connection_status, validation_msg
)
@SafeSlot(dict, int, int, str)
def _handle_validation_result(
self, device_config: dict, config_status: int, connection_status: int, validation_msg: str
):
"""Handle completion of validation."""
try:
if (
DeviceModel.model_validate(device_config)
== DeviceModel.model_validate(self._validation_result[0])
and connection_status == ConnectionStatus.UNKNOWN.value
):
# Config unchanged, we can reuse previous connection status. Only do this if the new
# connection status is UNKNOWN as the current validation should not test the connection.
connection_status = self._validation_result[2]
validation_msg = self._validation_result[3]
except Exception:
logger.debug(
f"Device config validation changed for config: {device_config} compared to previous validation. Using status from recent validation."
)
self._validation_result = (device_config, config_status, connection_status, validation_msg)
if self._wait_dialog is not None:
self._wait_dialog.accept()
self._wait_dialog.close()
self._wait_dialog.deleteLater()
self._wait_dialog = None
def _add_config(self):
"""
Adding a config will always run a validation check of the config without a connection test.
We will check if tests have already run, and reuse the information in case they also tested the connection to the device.
"""
config = self._device_config_template.get_config_fields()
# I. First we validate that the device name is valid, as this may create issues within the OphydValidation widget.
# Validate device name first. If invalid, this should immediately block adding the device.
if not validate_name(config.get("name", "")):
msg_box = self._create_warning_message_box(
"Invalid Device Name",
f"Device is invalid, cannot be empty or contain spaces. Please provide a valid name. {config.get('name', '')!r}",
)
msg_box.exec()
return
# II. Next we will run the validation check of the config without connection test.
# We will show a wait dialog while this is happening, and compare the results with the last known validation results.
# If the config is unchanged, we will use the connection status results from the last validation.
self._wait_dialog = self._create_validation_dialog()
ophyd_validation: OphydValidation | None = None
try:
ophyd_validation = self._create_and_run_ophyd_validation(config)
# NOTE If dialog was already closed, this means that a validation callback was already received
# which closed the dialog. In this case, we skip exec to avoid deadlock. With the singleShot above,
# this should not happen, but we keep the check for safety.
if self._wait_dialog is not None:
self._wait_dialog.exec() # This will block until the validation is complete
config, config_status, connection_status, validation_msg = self._validation_result
if config_status == ConfigStatus.INVALID.value:
msg_box = self._create_warning_message_box(
"Invalid Device Configuration",
f"Device configuration is invalid. Last known validation message:\n\nErrors:\n{self._validation_result[3]}",
)
msg_box.exec()
return
self.accepted_data.emit(
config, config_status, connection_status, validation_msg, self._old_device_name
)
self.accept()
finally:
if ophyd_validation is not None:
ophyd_validation.close()
ophyd_validation.deleteLater()
def _create_warning_message_box(self, title: str, text: str) -> QtWidgets.QMessageBox:
msg_box = QtWidgets.QMessageBox(self)
msg_box.setIcon(QtWidgets.QMessageBox.Warning)
msg_box.setWindowTitle(title)
msg_box.setText(text)
return msg_box
def _test_connection(self):
config = self._device_config_template.get_config_fields()
dialog = DeviceManagerOphydValidationDialog(self, config=config)
result = dialog.exec()
if result in (QtWidgets.QDialog.Accepted, QtWidgets.QDialog.Rejected):
self.config_validation_result = dialog.validation_result
def _reset_config(self):
self._device_config_template.reset_to_defaults()
def _reject_config(self):
self.reject()
if __name__ == "__main__": # pragma: no cover
import sys
from bec_qthemes import apply_theme
app = QtWidgets.QApplication(sys.argv)
apply_theme("light")
dialog = DeviceFormDialog()
dialog.resize(1200, 800)
dialog.show()
sys.exit(app.exec())
@@ -0,0 +1,691 @@
"""Module for the upload redis dialog in the device manager view."""
from __future__ import annotations
from enum import IntEnum
from functools import partial
from typing import TYPE_CHECKING, Any, List, Tuple
from bec_lib.logger import bec_logger
from bec_qthemes import apply_theme, material_icon
from qtpy import QtCore, QtGui, QtWidgets
from bec_widgets.utils.colors import get_accent_colors
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.widgets.control.device_manager.components.ophyd_validation import (
ConfigStatus,
ConnectionStatus,
get_validation_icons,
)
if TYPE_CHECKING:
from bec_widgets.utils.colors import AccentColor
from bec_widgets.widgets.control.device_manager.components.device_table.device_table import (
_ValidationResultIter,
)
logger = bec_logger.logger
class DeviceStatusItem(QtWidgets.QWidget):
"""Individual device status item widget for the validation display."""
def __init__(
self, device_config: dict, config_status: int, connection_status: int, parent=None
):
super().__init__(parent)
self.device_name = device_config.get("name", "")
self.device_config: dict = device_config
self.config_status = ConfigStatus(config_status)
self.connection_status = ConnectionStatus(connection_status)
self._transparent_button_style = "background-color: transparent; border: none;"
# Get validation icons
self.colors = get_accent_colors()
self._icon_size = (20, 20)
self.icons = get_validation_icons(self.colors, self._icon_size)
self._setup_ui()
self._update_display()
def _setup_ui(self):
"""Setup the UI for the device status item."""
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(8, 4, 8, 4)
layout.setSpacing(8)
# Device name label
self.name_label = QtWidgets.QLabel(self.device_name)
self.name_label.setMinimumWidth(150)
layout.addWidget(self.name_label)
layout.addStretch()
# Config status icon
self.config_icon_label = self._create_status_icon_label(self._icon_size)
layout.addWidget(self.config_icon_label)
# Connection status icon
self.connection_icon_label = self._create_status_icon_label(self._icon_size)
layout.addWidget(self.connection_icon_label)
def _create_status_icon_label(self, icon_size: tuple[int, int]) -> QtWidgets.QPushButton:
button = QtWidgets.QPushButton()
button.setFlat(True)
button.setEnabled(False)
button.setStyleSheet(self._transparent_button_style)
button.setFixedSize(icon_size[0], icon_size[1])
return button
def _update_display(self):
"""Update the visual display based on current status."""
# Update config status
config_icon = self.icons["config_status"].get(self.config_status.value)
if config_icon:
self.config_icon_label.setIcon(config_icon)
# Update connection status
connection_icon = self.icons["connection_status"].get(self.connection_status.value)
if connection_icon:
self.connection_icon_label.setIcon(connection_icon)
def update_status(self, config_status: int, connection_status: int):
"""Update the status and refresh display."""
self.config_status = ConfigStatus(config_status)
self.connection_status = ConnectionStatus(connection_status)
self._update_display()
class SortTableItem(QtWidgets.QTableWidgetItem):
"""Custom TableWidgetItem with hidden __column_data attribute for sorting."""
def __lt__(self, other: QtWidgets.QTableWidgetItem) -> bool:
"""Override less-than operator for sorting."""
if not isinstance(other, QtWidgets.QTableWidgetItem):
return NotImplemented
self_data = self.data(QtCore.Qt.ItemDataRole.UserRole)
other_data = other.data(QtCore.Qt.ItemDataRole.UserRole)
if self_data is not None and other_data is not None:
self_data: DeviceStatusItem
other_data: DeviceStatusItem
if self_data.config_status != other_data.config_status:
return self_data.config_status < other_data.config_status
else:
return self_data.connection_status < other_data.connection_status
return super().__lt__(other)
def __gt__(self, other: QtWidgets.QTableWidgetItem) -> bool:
"""Override less-than operator for sorting."""
if not isinstance(other, QtWidgets.QTableWidgetItem):
return NotImplemented
self_data = self.data(QtCore.Qt.ItemDataRole.UserRole)
other_data = other.data(QtCore.Qt.ItemDataRole.UserRole)
if self_data is not None and other_data is not None:
self_data: DeviceStatusItem
other_data: DeviceStatusItem
if self_data.config_status != other_data.config_status:
return self_data.config_status > other_data.config_status
else:
return self_data.connection_status > other_data.connection_status
return super().__gt__(other)
class ValidationSection(QtWidgets.QGroupBox):
"""Section widget for displaying validation results."""
def __init__(self, title: str, parent=None):
super().__init__(title, parent=parent)
self._setup_ui()
# self.device_items: Dict[str, DeviceStatusItem] = {}
def _setup_ui(self):
"""Setup the UI for the validation section."""
layout = QtWidgets.QVBoxLayout(self)
# Status summary label
summary_layout = QtWidgets.QHBoxLayout()
self.summary_icon = QtWidgets.QLabel()
self.summary_icon.setFixedSize(24, 24)
self.summary_label = QtWidgets.QLabel()
self.summary_label.setWordWrap(True)
summary_layout.addWidget(self.summary_icon)
summary_layout.addWidget(self.summary_label)
layout.addLayout(summary_layout)
# Scroll area for device items
self.table = QtWidgets.QTableWidget()
self.table.setColumnCount(1)
self.table.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
self.table.horizontalHeader().hide()
self.table.verticalHeader().hide()
self.table.setShowGrid(False) # r
self.table.sortItems(0, QtCore.Qt.SortOrder.AscendingOrder)
layout.addWidget(self.table)
QtCore.QTimer.singleShot(0, self.adjustSize)
def add_device(self, device_config: dict, config_status: int, connection_status: int):
"""
Add a device to the validation section.
Args:
device_config (dict): The device configuration dictionary.
config_status (int): The configuration status.
connection_status (int): The connection status.
"""
self.table.setSortingEnabled(False)
device_name = device_config.get("name", "")
row = self._find_row_by_name(device_name)
if row is not None:
widget: DeviceStatusItem = self.table.cellWidget(row, 0)
widget.update_status(config_status, connection_status)
else:
row_position = self.table.rowCount()
self.table.insertRow(row_position)
sort_item = SortTableItem(device_name)
sort_item.setText("")
self.table.setItem(row_position, 0, sort_item)
device_item = DeviceStatusItem(device_config, config_status, connection_status)
sort_item.setData(QtCore.Qt.ItemDataRole.UserRole, device_item)
self.table.setCellWidget(row_position, 0, device_item)
self.table.resizeRowsToContents()
self.table.setSortingEnabled(True)
def _find_row_by_name(self, device_name: str) -> int | None:
"""
Find a row by device name.
Args:
name (str): The name of the device to find.
Returns:
int | None: The row index if found, else None.
"""
for row in range(self.table.rowCount()):
item: SortTableItem = self.table.item(row, 0)
widget: DeviceStatusItem = self.table.cellWidget(row, 0)
if widget.device_name == device_name:
return row
return None
def remove_device(self, device_name: str):
"""Remove a device from the table by name."""
self.table.setSortingEnabled(False)
row = self._find_row_by_name(device_name)
if row is not None:
self.table.removeRow(row)
self.table.setSortingEnabled(True)
def clear_devices(self):
"""Clear all device items."""
self.table.setSortingEnabled(False)
while self.table.rowCount() > 0:
self.table.removeRow(0)
self.table.setSortingEnabled(True)
def update_summary(self, text: str, icon: QtGui.QPixmap = None):
"""Update the summary label."""
self.summary_label.setText(text)
if icon:
self.summary_icon.setPixmap(icon)
class UploadRedisDialog(QtWidgets.QDialog):
"""
Dialog for uploading device configurations to BEC server with validation checks.
"""
class UploadAction(IntEnum):
"""Enum for upload actions."""
CANCEL = QtWidgets.QDialog.DialogCode.Rejected
OK = QtWidgets.QDialog.DialogCode.Accepted
CONNECTION_TEST_REQUESTED = 999
# Request ophyd validation for all untested device connections
# list of device configs, added: bool, connect: bool
request_ophyd_validation = QtCore.Signal(list, bool, bool)
def __init__(self, parent, device_configs: dict[str, Tuple[dict, int, int]] | None = None):
super().__init__(parent=parent)
self.device_configs: dict[str, Tuple[dict, int, int]] = device_configs or {}
self._transparent_button_style = "background-color: transparent; border: none;"
self.colors = get_accent_colors()
self.icons = get_validation_icons(self.colors, (20, 20))
material_icon_partial = partial(material_icon, size=(24, 24), filled=True)
self._label_icons = {
"success": material_icon_partial("check_circle", color=self.colors.success),
"warning": material_icon_partial("warning", color=self.colors.warning),
"error": material_icon_partial("error", color=self.colors.emergency),
"reload": material_icon_partial("refresh", color=self.colors.default),
"upload": material_icon_partial("cloud_upload", color=self.colors.default),
}
# Track validation states
self.has_invalid_configs: int = 0
self.has_untested_connections: int = 0
self.has_cannot_connect: int = 0
self._setup_ui()
self._update_ui()
def set_device_config(self, device_configs: dict[str, Tuple[dict, int, int]]):
"""
Update the device configuration in the dialog.
Args:
device_configs (dict[str, Tuple[dict, int, int]]): New device configurations with structure
{device_name: (config_dict, config_status, connection_status)}.
"""
self.config_section.clear_devices()
self.device_configs = device_configs
self._update_ui()
def _setup_ui(self):
"""Setup the main UI for the dialog."""
self.setWindowTitle("Upload Configuration to BEC Server")
self.setModal(True) # Blocks interaction with other parts of the app
layout = QtWidgets.QVBoxLayout(self)
layout.setSpacing(16)
# Header
header_label = QtWidgets.QLabel("Review Configuration Before Upload")
header_label.setStyleSheet("font-size: 16px; font-weight: bold; margin-bottom: 8px;")
layout.addWidget(header_label)
# Description
desc_label = QtWidgets.QLabel(
"Please review the configuration and connection status of all devices before uploading to BEC Server."
)
desc_label.setWordWrap(True)
desc_label.setStyleSheet("color: #666; margin-bottom: 16px;")
layout.addWidget(desc_label)
# Config validation section
sections_layout = QtWidgets.QHBoxLayout()
self.config_section = ValidationSection("Configuration Validation")
sections_layout.addWidget(self.config_section)
layout.addLayout(sections_layout)
# Action buttons section
self._setup_action_buttons(layout)
# Dialog buttons
self._setup_dialog_buttons(layout)
self.adjustSize()
def _setup_action_buttons(self, parent_layout: QtWidgets.QLayout):
"""Setup the action buttons section."""
action_group = QtWidgets.QGroupBox("Actions")
action_layout = QtWidgets.QVBoxLayout(action_group)
# Validate connections button
button_layout = QtWidgets.QHBoxLayout()
self.validate_connections_btn = QtWidgets.QPushButton("Validate All Connections")
self.validate_connections_btn.setIcon(self._label_icons["reload"])
self.validate_connections_btn.clicked.connect(self._validate_connections)
button_layout.addWidget(self.validate_connections_btn)
button_layout.addStretch()
button_layout.addSpacing(16)
action_layout.addLayout(button_layout)
# Status indicator
status_layout = QtWidgets.QHBoxLayout()
self.status_icon = QtWidgets.QPushButton()
self.status_icon.setFlat(True)
self.status_icon.setEnabled(False)
self.status_icon.setStyleSheet(self._transparent_button_style)
self.status_icon.setFixedSize(24, 24)
self.status_label = QtWidgets.QLabel()
self.status_label.setWordWrap(True)
status_layout.addWidget(self.status_icon)
status_layout.addWidget(self.status_label)
action_layout.addLayout(status_layout)
parent_layout.addWidget(action_group)
def _setup_dialog_buttons(self, parent_layout: QtWidgets.QLayout):
"""Setup the dialog buttons."""
button_layout = QtWidgets.QHBoxLayout()
# Cancel button
self.cancel_btn = QtWidgets.QPushButton("Cancel")
self.cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.cancel_btn)
button_layout.addStretch()
# Upload button
self.upload_btn = QtWidgets.QPushButton("Upload to BEC Server")
self.upload_btn.setIcon(self._label_icons["upload"])
self.upload_btn.clicked.connect(self._handle_upload)
button_layout.addWidget(self.upload_btn)
parent_layout.addLayout(button_layout)
def _populate_device_data(self):
"""Populate the dialog with device configuration data."""
if not self.device_configs:
return
self.has_invalid_configs = 0
self.has_untested_connections = 0
self.has_cannot_connect = 0
for device_name, (config, config_status, connection_status) in self.device_configs.items():
# Add to appropriate sections
self.config_section.add_device(config, config_status, connection_status)
# Track statistics
if config_status == ConfigStatus.INVALID.value:
self.has_invalid_configs += 1
if connection_status == ConnectionStatus.UNKNOWN.value:
self.has_untested_connections += 1
if connection_status == ConnectionStatus.CANNOT_CONNECT.value:
self.has_cannot_connect += 1
# Update section summaries
num_devices = len(self.device_configs)
# Config validation summary
if self.has_invalid_configs > 0:
icon = self._label_icons["error"]
text = f"{self.has_invalid_configs} of {num_devices} device configurations are invalid."
else:
icon = self._label_icons["success"]
text = f"All {num_devices} device configurations are valid."
if self.has_untested_connections > 0:
icon = self._label_icons["warning"]
text += f"{self.has_untested_connections} device connections are not tested."
if self.has_cannot_connect > 0:
icon = self._label_icons["warning"]
text += f"{self.has_cannot_connect} device connections cannot be established."
self.config_section.update_summary(text, icon)
def _update_ui(self):
"""Update UI state based on validation results."""
# Update first the device data
self._populate_device_data()
# Invalid configuration have highest priority, upload disabled
if self.has_invalid_configs:
self.status_icon.setIcon(self._label_icons["error"])
self.status_label.setText(
"\n".join(
[
f"{self.has_invalid_configs} device configurations are invalid.",
"Please fix configuration errors before uploading.",
]
)
)
self.upload_btn.setEnabled(False)
self.validate_connections_btn.setEnabled(False)
self.validate_connections_btn.setText("Invalid Configurations")
# Next priority: connections that cannot be established, error but upload is enabled
elif self.has_cannot_connect:
self.status_icon.setIcon(self._label_icons["warning"])
self.status_label.setText(
"\n".join(
[
f"{self.has_cannot_connect} connections cannot be established.",
"Please fix connection issues before uploading.",
]
)
)
self.upload_btn.setEnabled(True)
self.validate_connections_btn.setEnabled(True)
self.validate_connections_btn.setText(
f"Validate {self.has_untested_connections + self.has_cannot_connect} Connections"
)
# Next priority: untested connections, warning but upload is enabled
elif self.has_untested_connections:
self.status_icon.setIcon(self._label_icons["warning"])
self.status_label.setText(
"\n".join(
[
f"{self.has_untested_connections} connections have not been tested.",
"Consider validating connections before uploading.",
]
)
)
self.upload_btn.setEnabled(True)
self.validate_connections_btn.setEnabled(True)
self.validate_connections_btn.setText(
f"Validate {self.has_untested_connections + self.has_cannot_connect} Connections"
)
# All good, upload enabled
else:
self.status_icon.setIcon(self._label_icons["success"])
self.status_label.setText(
"\n".join(
[
"All device configurations are valid.",
"All connections have been successfully tested.",
]
)
)
self.upload_btn.setEnabled(True)
self.validate_connections_btn.setEnabled(False)
self.validate_connections_btn.setText("All Connections Validated")
@SafeSlot()
def _validate_connections(self):
"""Request validation of all untested connections. This will close the dialog."""
testable_devices: List[dict] = []
for _, (config, _, connection_status) in self.device_configs.items():
if connection_status == ConnectionStatus.UNKNOWN.value:
testable_devices.append(config)
elif connection_status == ConnectionStatus.CANNOT_CONNECT.value:
testable_devices.append(config)
if len(testable_devices) > 0:
self.request_ophyd_validation.emit(testable_devices, True, True)
self.done(self.UploadAction.CONNECTION_TEST_REQUESTED)
@SafeSlot()
def _handle_upload(self):
"""Handle the upload button click with appropriate confirmations."""
# First priority: invalid configurations, block upload
if self.has_invalid_configs:
detailed_text = (
f"There is {self.has_invalid_configs} device with an invalid configuration."
if self.has_invalid_configs == 1
else f"There are {self.has_invalid_configs} devices with invalid configurations."
)
text = " ".join(
[detailed_text, "Invalid configuration can not be uploaded to the BEC Server."]
)
QtWidgets.QMessageBox.critical(self, "Device Configurations Invalid", text)
self.done(self.UploadAction.CANCEL)
return
# Next priority: connections that cannot be established, show warning, but allow to proceed
if self.has_cannot_connect:
detailed_text = (
f"There is {self.has_cannot_connect} device that cannot connect"
if self.has_cannot_connect == 1
else f"There are {self.has_cannot_connect} devices that cannot connect."
)
text = " ".join(
[
detailed_text,
"These devices may not be reachable and disabled BEC upon loading the config.",
"Consider validating these connections before proceeding.\n\n",
"Continue anyway?",
]
)
reply = QtWidgets.QMessageBox.critical(
self,
"Devices cannot Connect",
text,
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
QtWidgets.QMessageBox.No,
)
if reply == QtWidgets.QMessageBox.No:
return
# If some connections are untested, warn the user
if self.has_untested_connections:
detailed_text = (
f"There is {self.has_untested_connections} device with untested connections."
if self.has_untested_connections == 1
else f"There are {self.has_untested_connections} devices with untested connections."
)
text = " ".join(
[
detailed_text,
"Uploading without validating connections may result in devices that cannot be reached when the configuration is applied.",
]
)
reply = QtWidgets.QMessageBox.question(
self,
"Untested Connections",
text,
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
QtWidgets.QMessageBox.No,
)
if reply == QtWidgets.QMessageBox.No:
return
# Final confirmation
text = " ".join(
["You are about to upload the device configurations to BEC Server.", "Please confirm."]
)
reply = QtWidgets.QMessageBox.question(
self,
"Upload to BEC Server",
text,
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
QtWidgets.QMessageBox.Yes,
)
if reply == QtWidgets.QMessageBox.Yes:
self.done(self.UploadAction.OK)
else:
self.done(self.UploadAction.CANCEL)
@SafeSlot(dict, int, int, str)
def _update_from_ophyd_device_tests(
self,
device_config: dict,
config_status: int,
connection_status: int,
validation_message: str = "",
):
"""
Update device status from ophyd device tests. This has to be with a connection_status that was updated.
"""
if connection_status == ConnectionStatus.UNKNOWN.value:
return
self.update_device_status(device_config, config_status, connection_status)
@SafeSlot(list)
def _multiple_updates_from_ophyd_device_tests(self, validation_results: _ValidationResultIter):
"""
Callback slot for receiving multiple validation result updates from the ophyd test widget.
Args:
validation_results (list): List of tuples containing (device_config, config_status, connection_status, validation_msg).
"""
for cfg, cfg_status, conn_status, val_msg in validation_results:
self.update_device_status(cfg, cfg_status, conn_status)
self._update_ui()
@SafeSlot(dict, int, int)
def update_device_status(self, device_config: dict, config_status: int, connection_status: int):
"""Update the status of a specific device."""
# Update device config status
self._update_device_configs(device_config, config_status, connection_status, "")
# Recalculate summaries and UI state
self._update_ui()
def _update_device_configs(
self,
device_config: dict[str, Any],
config_status: int,
connection_status: int,
validation_msg: str,
):
device_name = device_config.get("name", "")
old_config, _, _ = self.device_configs.get(device_name, (None, None, None))
if old_config is not None:
self.device_configs[device_name] = (device_config, config_status, connection_status)
else:
# If device not found, add it
self.config_section.add_device(device_config, config_status, connection_status)
def main(): # pragma: no cover
"""Test the upload redis dialog."""
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
# Sample device configurations for testing
sample_configs = [
(
{"name": "motor_x", "deviceClass": "EpicsMotor"},
ConfigStatus.VALID.value,
ConnectionStatus.CONNECTED.value,
),
(
{"name": "detector_1", "deviceClass": "EpicsSignal"},
ConfigStatus.VALID.value,
ConnectionStatus.CONNECTED.value,
),
(
{"name": "detector_2", "deviceClass": "EpicsSignal"},
ConfigStatus.VALID.value,
ConnectionStatus.UNKNOWN.value,
),
(
{"name": "motor_y", "deviceClass": "EpicsMotor"},
ConfigStatus.VALID.value,
ConnectionStatus.CONNECTED.value,
),
(
{"name": "motor_z", "deviceClass": "EpicsMotor"},
ConfigStatus.VALID.value,
ConnectionStatus.CONNECTED.value,
),
(
{"name": "motor_x1", "deviceClass": "EpicsMotor"},
ConfigStatus.VALID.value,
ConnectionStatus.CONNECTED.value,
),
(
{"name": "detector_11", "deviceClass": "EpicsSignal"},
ConfigStatus.VALID.value,
ConnectionStatus.CANNOT_CONNECT.value,
),
(
{"name": "detector_21", "deviceClass": "EpicsSignal"},
ConfigStatus.INVALID.value,
ConnectionStatus.UNKNOWN.value,
),
(
{"name": "motor_y1", "deviceClass": "EpicsMotor"},
ConfigStatus.VALID.value,
ConnectionStatus.CANNOT_CONNECT.value,
),
(
{"name": "motor_z1", "deviceClass": "EpicsMotor"},
ConfigStatus.VALID.value,
ConnectionStatus.CONNECTED.value,
),
]
configs = {cfg[0]["name"]: cfg for cfg in sample_configs}
apply_theme("dark")
dialog = UploadRedisDialog(parent=None, device_configs=configs)
dialog.show()
sys.exit(app.exec_())
if __name__ == "__main__": # pragma: no cover
main()
@@ -0,0 +1,864 @@
from __future__ import annotations
import os
from functools import partial
from typing import TYPE_CHECKING, List, Literal, get_args
import yaml
from bec_lib import config_helper
from bec_lib.bec_yaml_loader import yaml_load
from bec_lib.callback_handler import EventType
from bec_lib.endpoints import MessageEndpoints
from bec_lib.file_utils import DeviceConfigWriter
from bec_lib.logger import bec_logger
from bec_lib.messages import ConfigAction, ScanStatusMessage
from bec_lib.plugin_helper import plugin_package_name, plugin_repo_path
from bec_qthemes import apply_theme, material_icon
from qtpy.QtCore import QMetaObject, Qt, QThreadPool, Signal
from qtpy.QtGui import QColor
from qtpy.QtWidgets import (
QApplication,
QFileDialog,
QMessageBox,
QPushButton,
QTextEdit,
QVBoxLayout,
QWidget,
)
from bec_widgets.applications.views.device_manager_view.device_manager_dialogs import (
ConfigChoiceDialog,
DeviceFormDialog,
)
from bec_widgets.applications.views.device_manager_view.device_manager_dialogs.upload_redis_dialog import (
UploadRedisDialog,
)
from bec_widgets.utils.colors import get_accent_colors
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.toolbars.actions import MaterialIconAction
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
from bec_widgets.widgets.containers.dock_area.basic_dock_area import DockAreaWidget
from bec_widgets.widgets.control.device_manager.components import (
DeviceTable,
DMConfigView,
DocstringView,
OphydValidation,
)
from bec_widgets.widgets.control.device_manager.components._util import SharedSelectionSignal
from bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophyd_validation_utils import (
ConfigStatus,
ConnectionStatus,
)
from bec_widgets.widgets.progress.device_initialization_progress_bar.device_initialization_progress_bar import (
DeviceInitializationProgressBar,
)
from bec_widgets.widgets.services.device_browser.device_item.config_communicator import (
CommunicateConfigAction,
)
from bec_widgets.widgets.utility.spinner.spinner import SpinnerWidget
if TYPE_CHECKING: # pragma: no cover
from bec_lib.client import BECClient
logger = bec_logger.logger
_yes_no_question = partial(
QMessageBox.question,
buttons=QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
defaultButton=QMessageBox.StandardButton.No,
)
class CustomBusyWidget(QWidget):
"""Custom busy widget to show during device config upload."""
cancel_requested = Signal()
def __init__(self, parent=None, client: BECClient | None = None):
super().__init__(parent=parent)
# Widgets
self.progress = QWidget(parent=self)
self.progress_layout = QVBoxLayout(self.progress)
self.progress_layout.setContentsMargins(6, 6, 6, 6)
self.progress_inner = DeviceInitializationProgressBar(parent=self.progress, client=client)
self.progress_layout.addWidget(self.progress_inner)
self.progress.setMinimumWidth(320)
# Spinner
self.spinner = SpinnerWidget(parent=self)
scale = self._ui_scale()
spinner_size = int(scale * 0.12) if scale else 1
spinner_size = max(32, min(spinner_size, 96))
self.spinner.setFixedSize(spinner_size, spinner_size)
# Cancel button
self.cancel_button = QPushButton("Cancel Upload", parent=self)
self.cancel_button.setIcon(material_icon("cancel"))
self.cancel_button.clicked.connect(self.cancel_requested.emit)
button_height = int(spinner_size * 0.9)
button_height = max(36, min(button_height, 72))
aspect_ratio = 3.8 # width / height, visually stable for text buttons
button_width = int(button_height * aspect_ratio)
self.cancel_button.setFixedSize(button_width, button_height)
color = get_accent_colors()
self.cancel_button.setStyleSheet(f"""
QPushButton {{
background-color: {color.emergency.name()};
color: white;
font-weight: 600;
border-radius: 6px;
}}
""")
# Layout
content_layout = QVBoxLayout(self)
content_layout.setContentsMargins(24, 24, 24, 24)
content_layout.setSpacing(16)
content_layout.addStretch()
content_layout.addWidget(self.spinner, 0, Qt.AlignmentFlag.AlignHCenter)
content_layout.addWidget(self.progress, 0, Qt.AlignmentFlag.AlignHCenter)
content_layout.addStretch()
content_layout.addWidget(self.cancel_button, 0, Qt.AlignmentFlag.AlignHCenter)
if hasattr(color, "_colors"):
bg_color = color._colors.get("BG", None)
if bg_color is None: # Fallback if missing
bg_color = QColor(50, 50, 50, 255)
self.setStyleSheet(f"""
background-color: {bg_color.name()};
border-radius: 12px;
""")
def _ui_scale(self) -> int:
parent = self.parent()
if not parent:
return 0
return min(parent.width(), parent.height())
def showEvent(self, event):
"""Show event to start the spinner."""
super().showEvent(event)
self.spinner.start()
def hideEvent(self, event):
"""Hide event to stop the spinner."""
super().hideEvent(event)
self.spinner.stop()
class DeviceManagerDisplayWidget(DockAreaWidget):
"""Device Manager main display widget. This contains all sub-widgets and the toolbar."""
RPC = False
request_ophyd_validation = Signal(list, bool, bool)
def __init__(self, parent=None, *args, **kwargs):
super().__init__(parent=parent, variant="compact", *args, **kwargs)
# State variable for config upload
self._config_upload_active: bool = False
self._config_in_sync: bool = False
scan_status = self.bec_dispatcher.client.connector.get(MessageEndpoints.scan_status())
initial_status = scan_status.status if scan_status is not None else "closed"
self._scan_is_running: bool = initial_status in ["open", "paused"]
# Push to Redis dialog
self._upload_redis_dialog: UploadRedisDialog | None = None
self._dialog_validation_connection: QMetaObject.Connection | None = None
# NOTE: We need here a seperate config helper instance to avoid conflicts with
# other communications to REDIS as uploading a config through a CommunicationConfigAction
# will block if we use the config_helper from self.client.config._config_helper
self._config_helper = config_helper.ConfigHelper(self.client.connector)
self._shared_selection = SharedSelectionSignal()
# Device Table View widget
self.device_table_view = DeviceTable(self)
# Device Config View widget
self.dm_config_view = DMConfigView(self)
# Docstring View
self.dm_docs_view = DocstringView(self)
# Ophyd Test view
self.ophyd_widget_view = QWidget(self)
layout = QVBoxLayout(self.ophyd_widget_view)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
self.ophyd_test_view = OphydValidation(self, hide_legend=False)
layout.addWidget(self.ophyd_test_view)
# Validation Results view
self.validation_results = QTextEdit(self)
self.validation_results.setReadOnly(True)
self.validation_results.setPlaceholderText("Validation results will appear here...")
layout.addWidget(self.validation_results)
self.ophyd_test_view.item_clicked.connect(self._ophyd_test_item_clicked_cb)
for signal, slots in [
(
self.device_table_view.selected_devices,
(self.dm_config_view.on_select_config, self.dm_docs_view.on_select_config),
),
(
self.ophyd_test_view.validation_completed,
(self.device_table_view.update_device_validation,),
),
(
self.ophyd_test_view.multiple_validations_completed,
(self.device_table_view.update_multiple_device_validations,),
),
(self.request_ophyd_validation, (self.ophyd_test_view.change_device_configs,)),
(
self.device_table_view.device_configs_changed,
(self.ophyd_test_view.device_table_config_changed,),
),
(
self.device_table_view.device_config_in_sync_with_redis,
(self._update_config_in_sync,),
),
(self.device_table_view.device_row_dbl_clicked, (self._edit_device_action,)),
]:
for slot in slots:
signal.connect(slot)
self._scan_status_callback_id = self.bec_dispatcher.client.callbacks.register(
EventType.SCAN_STATUS, self._update_scan_running
)
# Add toolbar
self._add_toolbar()
# Build dock layout using shared helpers
self._build_docks()
def cleanup(self):
self.bec_dispatcher.client.callbacks.remove(self._scan_status_callback_id)
super().cleanup()
def closeEvent(self, event):
"""If config upload is active when application is exiting, cancel it."""
logger.info("Application is quitting, checking for active config upload...")
if self._config_upload_active:
logger.info("Application is quitting, cancelling active config upload...")
self._config_helper.send_config_request(
action="cancel", config=None, wait_for_response=True, timeout_s=10
)
logger.info("Config upload cancelled.")
super().closeEvent(event)
##############################
### Custom set busy widget ###
##############################
def create_busy_state_widget(self) -> QWidget:
"""Create a custom busy state widget for uploading device configurations."""
widget = CustomBusyWidget(parent=self, client=self.client)
widget.cancel_requested.connect(self._cancel_device_config_upload)
return widget
def _set_busy_wrapper(self, enabled: bool):
"""Thin wrapper around set_busy to flip the state variable."""
self._busy_overlay.set_opacity(0.92)
self._config_upload_active = enabled
self.set_busy(enabled=enabled)
##############################
### Toolbar and Dock setup ###
##############################
def _add_toolbar(self):
self.toolbar = ModularToolBar(self)
# Add IO actions
self._add_io_actions()
self._add_table_actions()
self.toolbar.show_bundles(["IO", "Table"])
self._root_layout.insertWidget(0, self.toolbar)
def _build_docks(self) -> None:
# Central device table
self.device_table_view_dock = self.new(
self.device_table_view,
return_dock=True,
closable=False,
floatable=False,
movable=False,
show_title_bar=False,
)
# Bottom area: docstrings
self.dm_docs_view_dock = self.new(
self.dm_docs_view,
where="bottom",
relative_to=self.device_table_view_dock,
return_dock=True,
closable=False,
floatable=False,
movable=False,
show_title_bar=False,
)
# Config view left of docstrings
self.dm_config_view_dock = self.new(
self.dm_config_view,
where="left",
relative_to=self.dm_docs_view_dock,
return_dock=True,
closable=False,
floatable=False,
movable=False,
show_title_bar=False,
)
# Right area: ophyd test + validation
self.ophyd_test_dock_view = self.new(
self.ophyd_widget_view,
where="right",
relative_to=self.device_table_view_dock,
return_dock=True,
closable=False,
floatable=False,
movable=False,
show_title_bar=False,
)
self.set_layout_ratios(splitter_overrides={0: [7, 3], 1: [3, 7]})
def _add_io_actions(self):
# Create IO bundle
io_bundle = ToolbarBundle("IO", self.toolbar.components)
# Load from disk
load = MaterialIconAction(
text_position="under",
icon_name="file_open",
parent=self,
tooltip="Load configuration file from disk",
label_text="Load Config",
)
self.toolbar.components.add_safe("load", load)
load.action.triggered.connect(self._load_file_action)
io_bundle.add_action("load")
# Add safe to disk
save_to_disk = MaterialIconAction(
text_position="under",
icon_name="file_save",
parent=self,
tooltip="Save config to disk",
label_text="Save Config",
)
self.toolbar.components.add_safe("save_to_disk", save_to_disk)
save_to_disk.action.triggered.connect(self._save_to_disk_action)
io_bundle.add_action("save_to_disk")
# Add flush config in redis
flush_redis = MaterialIconAction(
text_position="under",
icon_name="delete_sweep",
parent=self,
tooltip="Flush current config in BEC Server",
label_text="Flush loaded Config",
)
flush_redis.action.triggered.connect(self._flush_redis_action)
self.toolbar.components.add_safe("flush_redis", flush_redis)
io_bundle.add_action("flush_redis")
# Add load config from redis
load_redis = MaterialIconAction(
text_position="under",
icon_name="cached",
parent=self,
tooltip="Load current config from BEC Server",
label_text="Get loaded Config",
)
load_redis.action.triggered.connect(self._load_redis_action)
self.toolbar.components.add_safe("load_redis", load_redis)
io_bundle.add_action("load_redis")
# Update config action
update_config_redis = MaterialIconAction(
text_position="under",
icon_name="cloud_upload",
parent=self,
tooltip="Update current config in BEC Server",
label_text="Update Config",
)
update_config_redis.action.setEnabled(False)
update_config_redis.action.triggered.connect(self._update_redis_action)
self.toolbar.components.add_safe("update_config_redis", update_config_redis)
io_bundle.add_action("update_config_redis")
# Add load config from plugin dir
self.toolbar.add_bundle(io_bundle)
# Table actions
def _add_table_actions(self) -> None:
table_bundle = ToolbarBundle("Table", self.toolbar.components)
# Reset composed view
reset_composed = MaterialIconAction(
text_position="under",
icon_name="delete_sweep",
parent=self,
tooltip="Reset current composed config view",
label_text="Reset Config View",
)
reset_composed.action.triggered.connect(self._reset_composed_view)
self.toolbar.components.add_safe("reset_composed", reset_composed)
table_bundle.add_action("reset_composed")
# Add device
add_device = MaterialIconAction(
text_position="under",
icon_name="add",
parent=self,
tooltip="Add new device",
label_text="Add Device",
)
add_device.action.triggered.connect(self._add_device_action)
self.toolbar.components.add_safe("add_device", add_device)
table_bundle.add_action("add_device")
# Remove device
remove_device = MaterialIconAction(
text_position="under",
icon_name="remove",
parent=self,
tooltip="Remove device",
label_text="Remove Device",
)
remove_device.action.triggered.connect(self._remove_device_action)
self.toolbar.components.add_safe("remove_device", remove_device)
table_bundle.add_action("remove_device")
# Rerun validation
rerun_validation = MaterialIconAction(
text_position="under",
icon_name="checklist",
parent=self,
tooltip="Run device validation with 'connect' on selected devices",
label_text="Validate Connection",
)
rerun_validation.action.triggered.connect(self._run_validate_connection)
self.toolbar.components.add_safe("rerun_validation", rerun_validation)
table_bundle.add_action("rerun_validation")
# Add load config from plugin dir
self.toolbar.add_bundle(table_bundle)
######################################
### Update button state management ###
######################################
@SafeSlot(dict, dict)
def _update_scan_running(self, scan_info: dict, _: dict):
"""disable editing when scans are running and enable editing when they are finished"""
msg = ScanStatusMessage.model_validate(scan_info)
self._scan_is_running = msg.status in ["open", "paused"]
self._update_config_enabled_button()
def _update_config_in_sync(self, in_sync: bool):
self._config_in_sync = in_sync
self._update_config_enabled_button()
def _update_config_enabled_button(self):
action = self.toolbar.components.get_action("update_config_redis")
enabled = not self._config_in_sync and not self._scan_is_running
action.action.setEnabled(enabled)
if enabled: # button is enabled
action.action.setToolTip("Push current config to BEC Server")
elif self._scan_is_running:
action.action.setToolTip("Scan is currently running, config updates disabled.")
else:
action.action.setToolTip("Current config is in sync with BEC Server, updates disabled.")
#######################
### Action Handlers ###
#######################
@SafeSlot()
@SafeSlot(bool)
def _run_validate_connection(self, connect: bool = True):
"""Action for the 'rerun_validation' action to rerun validation on selected devices."""
configs = list(self.device_table_view.get_selected_device_configs())
if not configs:
configs = self.device_table_view.get_device_config()
# Adjust the state of the icons in the device table view
self.device_table_view.update_multiple_device_validations(
[
(cfg, ConfigStatus.UNKNOWN.value, ConnectionStatus.UNKNOWN.value, "")
for cfg in configs
]
)
self.request_ophyd_validation.emit(configs, True, connect)
@SafeSlot()
def _load_file_action(self):
"""Action for the 'load' action to load a config from disk for the io_bundle of the toolbar."""
config_path = self._get_config_base_path()
# Implement the file loading logic here
start_dir = os.path.abspath(config_path)
file_path = self._get_file_path(start_dir, "open_file")
if file_path:
self._load_config_from_file(file_path)
def _get_config_base_path(self) -> str:
"""Get the base path for device configurations."""
try:
plugin_path = plugin_repo_path()
plugin_name = plugin_package_name()
config_path = os.path.join(plugin_path, plugin_name, "device_configs")
except ValueError:
# Get the recovery config path as fallback
config_path = self._get_recovery_config_path()
logger.warning(
f"No plugin repository installed, fallback to recovery config path: {config_path}"
)
return config_path
def _get_file_path(self, start_dir: str, mode: Literal["open_file", "save_file"]) -> str:
ALLOWED_EXTS = [".yaml", ".yml"]
filter_str = "YAML files (*.yaml *.yml);;All Files (*)"
initial_filter = "YAML files (*.yaml *.yml);;"
if mode == "open_file":
file_path, _ = QFileDialog.getOpenFileName(
self,
caption="Select Config File",
dir=start_dir,
filter=filter_str,
selectedFilter=initial_filter,
)
else:
file_path, _ = QFileDialog.getSaveFileName(
self,
caption="Save Config File",
dir=start_dir,
filter=filter_str,
selectedFilter=initial_filter,
)
if not file_path:
return ""
_, ext = os.path.splitext(file_path)
if ext.lower() not in ALLOWED_EXTS:
file_path += ".yaml"
return file_path
def _load_config_from_file(self, file_path: str):
"""
Load device config from a given file path and update the device table view.
Args:
file_path (str): Path to the configuration file.
"""
try:
config = [{"name": k, **v} for k, v in yaml_load(file_path).items()]
except Exception as e:
logger.error(f"Failed to load config from file {file_path}. Error: {e}")
return
self._open_config_choice_dialog(config)
def _open_config_choice_dialog(self, config: List[dict]):
"""
Open a dialog to choose whether to replace or add the loaded config.
Args:
config (List[dict]): List of device configurations loaded from the file.
"""
if len(self.device_table_view.get_device_config()) == 0:
# If no config is composed yet, load directly
self.device_table_view.set_device_config(config)
return
dialog = ConfigChoiceDialog(self)
result = dialog.exec()
if result == ConfigChoiceDialog.Result.REPLACE:
self.device_table_view.set_device_config(config)
elif result == ConfigChoiceDialog.Result.ADD:
self.device_table_view.add_device_configs(config)
@SafeSlot()
def _flush_redis_action(self):
"""Action to flush the current config in Redis."""
if self.client.device_manager is None:
logger.error("No device manager connected, cannot load config from BEC Server.")
return
if len(self.client.device_manager.devices) == 0:
logger.info("No devices in BEC Server, nothing to flush.")
QMessageBox.information(
self, "No Devices", "There is currently no config loaded on the BEC Server."
)
return
reply = _yes_no_question(
self,
"Flush BEC Server Config",
"Do you really want to flush the current config in BEC Server?",
)
if reply == QMessageBox.StandardButton.Yes:
self.client.config.reset_config()
logger.info("Successfully flushed configuration in BEC Server.")
# Check if config is in sync, enable load redis button
self.device_table_view.device_config_in_sync_with_redis.emit(
self.device_table_view._is_config_in_sync_with_redis()
)
validation_results = self.device_table_view.get_validation_results()
for config, config_status, connnection_status in validation_results.values():
if connnection_status == ConnectionStatus.CONNECTED.value:
self.device_table_view.update_device_validation(
config, config_status, ConnectionStatus.CAN_CONNECT, ""
)
@SafeSlot()
def _load_redis_action(self):
"""Action for the 'load_redis' action to load the current config from Redis for the io_bundle of the toolbar."""
if self.client.device_manager is None:
logger.error("No device manager connected, cannot load config from BEC Server.")
return
if not self.device_table_view.get_device_config():
# If no config is composed yet, load directly
self.device_table_view.set_device_config(
self.client.device_manager._get_redis_device_config()
)
return
reply = _yes_no_question(
self,
"Load currently active config in BEC Server",
"Do you really want to discard the current config and reload?",
)
if reply == QMessageBox.StandardButton.Yes:
self.device_table_view.set_device_config(
self.client.device_manager._get_redis_device_config()
)
@SafeSlot()
def _update_redis_action(self) -> None | QMessageBox.StandardButton:
"""Action to push the current composition to Redis using the upload dialog."""
# Check if validations are still running
if self.ophyd_test_view.running_ophyd_tests is True:
return QMessageBox.warning(
self, "Validation in Progress", "Please wait for the validation to finish."
)
# Get all device configurations with their validation status
validation_results = self.device_table_view.get_validation_results()
# Create and show upload dialog
self._upload_redis_dialog = UploadRedisDialog(
parent=self, device_configs=validation_results
)
self._upload_redis_dialog.request_ophyd_validation.connect(
self.request_ophyd_validation.emit
)
# Show dialog
reply = self._upload_redis_dialog.exec_()
if reply == UploadRedisDialog.UploadAction.OK:
self._push_composition_to_redis(action="set")
elif reply == UploadRedisDialog.UploadAction.CANCEL:
self.ophyd_test_view.cancel_all_validations()
elif reply == UploadRedisDialog.UploadAction.CONNECTION_TEST_REQUESTED:
return QMessageBox.information(
self, "Connection Test Requested", "Running connection test on untested devices."
)
def _push_composition_to_redis(self, action: ConfigAction):
"""Push the current device composition to Redis."""
if action not in get_args(ConfigAction):
logger.error(f"Invalid config action: {action} for uploading to BEC Server.")
return
config = {cfg.pop("name"): cfg for cfg in self.device_table_view.get_device_config()}
threadpool = QThreadPool.globalInstance()
comm = CommunicateConfigAction(self._config_helper, None, config, action)
comm.signals.done.connect(self._handle_push_complete_to_communicator)
comm.signals.error.connect(self._handle_exception_from_communicator)
threadpool.start(comm)
self._set_busy_wrapper(enabled=True)
def _cancel_device_config_upload(self):
"""Cancel the device configuration upload process."""
threadpool = QThreadPool.globalInstance()
comm = CommunicateConfigAction(self._config_helper, None, {}, "cancel")
# Cancelling will raise an exception in the communicator, so we connect to the failure handler
comm.signals.error.connect(self._handle_cancel_config_upload_failed)
threadpool.start(comm)
def _handle_cancel_config_upload_failed(self, exception: Exception):
"""Handle failure to cancel the config upload."""
self._set_busy_wrapper(enabled=False)
validation_results = self.device_table_view.get_validation_results()
devices_to_update = []
for config, config_status, connection_status in validation_results.values():
devices_to_update.append(
(config, config_status, ConnectionStatus.UNKNOWN.value, "Upload Cancelled")
)
# Rerun validation of all devices after cancellation
self.device_table_view.update_multiple_device_validations(devices_to_update)
self.ophyd_test_view.change_device_configs(
[cfg for cfg, _, _, _ in devices_to_update], added=True, skip_validation=False
)
# Config is in sync with BEC, so we update the state
self.device_table_view.device_config_in_sync_with_redis.emit(False)
def _handle_push_complete_to_communicator(self):
"""Handle completion of the config push to Redis."""
self._set_busy_wrapper(enabled=False)
def _handle_exception_from_communicator(self, exception: Exception):
"""Handle exceptions from the config communicator."""
QMessageBox.critical(
self,
"Error Uploading Config",
f"An error occurred while uploading the configuration to BEC Server:\n{str(exception)}",
)
self._set_busy_wrapper(enabled=False)
@SafeSlot()
def _save_to_disk_action(self):
"""Action for the 'save_to_disk' action to save the current config to disk."""
# Check if plugin repo is installed...
try:
config_path = self._get_recovery_config_path()
except ValueError:
# Get the recovery config path as fallback
config_path = os.path.abspath(os.path.expanduser("~"))
logger.warning(f"Failed to find recovery config path, fallback to: {config_path}")
# Implement the file loading logic here
file_path = self._get_file_path(config_path, "save_file")
if file_path:
config = {cfg.pop("name"): cfg for cfg in self.device_table_view.get_device_config()}
if os.path.exists(file_path):
reply = _yes_no_question(
self,
"Overwrite File",
f"The file '{file_path}' already exists. Do you want to overwrite it?",
)
if reply != QMessageBox.StandardButton.Yes:
return
with open(file_path, "w") as file:
file.write(yaml.dump(config))
# Table actions
@SafeSlot()
def _reset_composed_view(self):
"""Action for the 'reset_composed_view' action to reset the composed view."""
reply = _yes_no_question(
self,
"Clear View",
"You are about to clear the current composed config view, please confirm...",
)
if reply == QMessageBox.StandardButton.Yes:
self.device_table_view.clear_device_configs()
@SafeSlot(dict)
def _edit_device_action(self, device_config: dict):
"""Action to edit a selected device configuration."""
dialog = DeviceFormDialog(parent=self, add_btn_text="Apply Changes")
dialog.accepted_data.connect(self._update_device_to_table_from_dialog)
dialog.set_device_config(device_config)
dialog.open()
@SafeSlot()
def _add_device_action(self):
"""Action for the 'add_device' action to add a new device."""
dialog = DeviceFormDialog(parent=self, add_btn_text="Add Device")
dialog.accepted_data.connect(self._add_to_table_from_dialog)
dialog.open()
@SafeSlot(dict, int, int, str, str)
def _update_device_to_table_from_dialog(
self,
data: dict,
config_status: int,
connection_status: int,
msg: str,
old_device_name: str = "",
):
if old_device_name and old_device_name != data.get("name", ""):
self.device_table_view.remove_device(old_device_name)
self._add_to_table_from_dialog(data, config_status, connection_status, msg, old_device_name)
@SafeSlot(dict, int, int, str, str)
def _add_to_table_from_dialog(
self,
data: dict,
config_status: int,
connection_status: int,
msg: str,
old_device_name: str = "",
):
if connection_status == ConnectionStatus.UNKNOWN.value:
self.device_table_view.update_device_configs([data], skip_validation=False)
else: # Connection status was tested in dialog
# If device is connected, we remove it from the ophyd validation view
self.device_table_view.update_device_configs([data], skip_validation=True)
# Update validation status in device table view and ophyd validation view
self.ophyd_test_view._on_device_test_completed(
data, config_status, connection_status, msg
)
@SafeSlot()
def _remove_device_action(self):
"""Action for the 'remove_device' action to remove a device."""
configs = self.device_table_view.get_selected_device_configs()
if not configs:
QMessageBox.warning(
self, "No devices selected", "Please select devices from the table to remove."
)
return
if self.device_table_view._remove_configs_dialog([cfg["name"] for cfg in configs]):
self.device_table_view.remove_device_configs(configs)
@SafeSlot(dict, int, int, str, str)
def _ophyd_test_item_clicked_cb(
self, device_config: dict, config_status: int, connection_status: int, msg: str, md_msg: str
) -> None:
self.validation_results.setMarkdown(md_msg)
def _get_recovery_config_path(self) -> str:
"""Get the recovery config path from the log_writer config."""
# pylint: disable=protected-access
log_writer_config = self.client._service_config.config.get("log_writer", {})
writer = DeviceConfigWriter(service_config=log_writer_config)
return os.path.abspath(os.path.expanduser(writer.get_recovery_directory()))
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
app = QApplication(sys.argv)
w = QWidget()
l = QVBoxLayout()
w.setLayout(l)
apply_theme("dark")
button = DarkModeButton()
l.addWidget(button)
device_manager_view = DeviceManagerDisplayWidget()
l.addWidget(device_manager_view)
w.show()
w.setWindowTitle("Device Manager View")
screen = app.primaryScreen()
screen_geometry = screen.availableGeometry()
screen_width = screen_geometry.width()
screen_height = screen_geometry.height()
# 70% of screen height, keep 16:9 ratio
height = int(screen_height * 0.9)
width = int(height * (16 / 9))
# If width exceeds screen width, scale down
if width > screen_width * 0.9:
width = int(screen_width * 0.9)
height = int(width / (16 / 9))
w.resize(width, height)
sys.exit(app.exec_())
@@ -0,0 +1,188 @@
"""Module for Device Manager View."""
from qtpy.QtCore import QRect
from qtpy.QtWidgets import QWidget
from bec_widgets.applications.views.device_manager_view.device_manager_widget import (
DeviceManagerWidget,
)
from bec_widgets.applications.views.view import ViewBase, ViewTourSteps
from bec_widgets.utils.error_popups import SafeSlot
class DeviceManagerView(ViewBase):
"""
A view for users to manage devices within the application.
"""
def __init__(
self,
parent: QWidget | None = None,
content: QWidget | None = None,
*,
view_id: str | None = None,
title: str | None = None,
**kwargs,
):
super().__init__(
parent=parent,
content=content,
view_id=view_id,
title=title,
rpc_passthrough_children=False,
**kwargs,
)
self.device_manager_widget = DeviceManagerWidget(
parent=self, rpc_exposed=False, rpc_passthrough_children=False
)
self.set_content(self.device_manager_widget)
@SafeSlot()
def on_enter(self) -> None:
"""Called after the view becomes current/visible.
Default implementation does nothing. Override in subclasses.
"""
self.device_manager_widget.on_enter()
def register_tour_steps(self, guided_tour, main_app) -> ViewTourSteps | None:
"""Register Device Manager components with the guided tour.
Args:
guided_tour: The GuidedTour instance to register with.
main_app: The main application instance (for accessing set_current).
Returns:
ViewTourSteps | None: Model containing view title and step IDs.
"""
step_ids = []
dm_widget = self.device_manager_widget
# The device_manager_widget is not yet initialized, so we will register
# tour steps for its uninitialized state.
# Register Load Current Config button
def get_load_current():
main_app.set_current("device_manager")
if dm_widget._initialized is True:
return (None, None)
return (dm_widget.button_load_current_config, None)
step_id = guided_tour.register_widget(
widget=get_load_current,
title="Load Current Config",
text="Load the current device configuration from the BEC server.",
)
step_ids.append(step_id)
# Register Load Config From File button
def get_load_file():
main_app.set_current("device_manager")
if dm_widget._initialized is True:
return (None, None)
return (dm_widget.button_load_config_from_file, None)
step_id = guided_tour.register_widget(
widget=get_load_file,
title="Load Config From File",
text="Load a device configuration from a YAML file on disk.",
)
step_ids.append(step_id)
## Register steps for the initialized state
# Register main device table
def get_device_table():
main_app.set_current("device_manager")
if dm_widget._initialized is False:
return (None, None)
return (dm_widget.device_manager_display.device_table_view, None)
step_id = guided_tour.register_widget(
widget=get_device_table,
title="Device Table",
text="This table displays the config that is prepared to be uploaded to the BEC server. It allows users to review and modify device config settings, and also validate them before uploading to the BEC server.",
)
step_ids.append(step_id)
col_text_mapping = {
0: "Shows if a device configuration is valid. Automatically validated when adding a new device.",
1: "Shows if a device is connectable. Validated on demand.",
2: "Device name, unique across all devices within a config.",
3: "Device class used to initialize the device on the BEC server.",
4: "Defines how BEC treats readings of the device during scans. The options are 'monitored', 'baseline', 'async', 'continuous' or 'on_demand'.",
5: "Defines how BEC reacts if a device readback fails. Options are 'raise', 'retry', or 'buffer'.",
6: "User-defined tags associated with the device.",
7: "A brief description of the device.",
8: "Device is enabled when the configuration is loaded.",
9: "Device is set to read-only.",
10: "This flag allows to configure if the 'trigger' method of the device is called during scans.",
}
# We have at least one device registered
def get_device_table_row(column: int):
main_app.set_current("device_manager")
if dm_widget._initialized is False:
return (None, None)
table = dm_widget.device_manager_display.device_table_view.table
header = table.horizontalHeader()
x = header.sectionViewportPosition(column)
table.horizontalScrollBar().setValue(x)
# Recompute after scrolling
x = header.sectionViewportPosition(column)
w = header.sectionSize(column)
h = header.height()
rect = QRect(x, 0, w, h)
top_left = header.viewport().mapTo(main_app, rect.topLeft())
return (QRect(top_left, rect.size()), col_text_mapping.get(column, ""))
for col, text in col_text_mapping.items():
step_id = guided_tour.register_widget(
widget=lambda col=col: get_device_table_row(col),
title=f"{dm_widget.device_manager_display.device_table_view.table.horizontalHeaderItem(col).text()}",
text=text,
)
step_ids.append(step_id)
if not step_ids:
return None
return ViewTourSteps(view_title="Device Manager", step_ids=step_ids)
if __name__ == "__main__": # pragma: no cover
import sys
from bec_qthemes import apply_theme
from qtpy.QtWidgets import QApplication
from bec_widgets.applications.main_app import BECMainApp
app = QApplication(sys.argv)
apply_theme("dark")
_app = BECMainApp()
screen = app.primaryScreen()
screen_geometry = screen.availableGeometry()
screen_width = screen_geometry.width()
screen_height = screen_geometry.height()
# 70% of screen height, keep 16:9 ratio
height = int(screen_height * 0.9)
width = int(height * (16 / 9))
# If width exceeds screen width, scale down
if width > screen_width * 0.9:
width = int(screen_width * 0.9)
height = int(width / (16 / 9))
_app.resize(width, height)
device_manager_view = DeviceManagerView()
_app.add_view(
icon="display_settings",
title="Device Manager",
view_id="device_manager",
widget=device_manager_view.device_manager_widget,
mini_text="DM",
)
_app.show()
sys.exit(app.exec_())
@@ -0,0 +1,112 @@
"""Top Level wrapper for device_manager widget"""
from __future__ import annotations
import os
from bec_lib.bec_yaml_loader import yaml_load
from bec_lib.logger import bec_logger
from bec_qthemes import material_icon
from qtpy import QtCore, QtWidgets
from bec_widgets.applications.views.device_manager_view.device_manager_display_widget import (
DeviceManagerDisplayWidget,
)
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeSlot
logger = bec_logger.logger
class DeviceManagerWidget(BECWidget, QtWidgets.QWidget):
RPC = False
def __init__(self, parent=None, client=None, **kwargs):
super().__init__(parent=parent, client=client, **kwargs)
self.stacked_layout = QtWidgets.QStackedLayout()
self.stacked_layout.setContentsMargins(0, 0, 0, 0)
self.stacked_layout.setSpacing(0)
self.stacked_layout.setStackingMode(QtWidgets.QStackedLayout.StackAll)
self.setLayout(self.stacked_layout)
# Add device manager view
self.device_manager_display = DeviceManagerDisplayWidget(parent=self, client=self.client)
self.stacked_layout.addWidget(self.device_manager_display)
# Add overlay widget
self._overlay_widget = QtWidgets.QWidget(self)
self._customize_overlay()
self.stacked_layout.addWidget(self._overlay_widget)
self._initialized = False
def on_enter(self) -> None:
"""Called after the widget becomes visible."""
if self._initialized is False:
self.stacked_layout.setCurrentWidget(self._overlay_widget)
def _customize_overlay(self):
self._overlay_widget.setAutoFillBackground(True)
self._overlay_layout = QtWidgets.QVBoxLayout()
self._overlay_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self._overlay_widget.setLayout(self._overlay_layout)
self._overlay_widget.setSizePolicy(
QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding
)
# Load current config
self.button_load_current_config = QtWidgets.QPushButton("Load Current Config")
icon = material_icon(icon_name="database", size=(24, 24), convert_to_pixmap=False)
self.button_load_current_config.setIcon(icon)
self._overlay_layout.addWidget(self.button_load_current_config)
self.button_load_current_config.clicked.connect(self._load_config_clicked)
# Load config from disk
self.button_load_config_from_file = QtWidgets.QPushButton("Load Config From File")
icon = material_icon(icon_name="folder", size=(24, 24), convert_to_pixmap=False)
self.button_load_config_from_file.setIcon(icon)
self._overlay_layout.addWidget(self.button_load_config_from_file)
self.button_load_config_from_file.clicked.connect(self._load_config_from_file_clicked)
self._overlay_widget.setVisible(True)
def _load_config_from_file_clicked(self):
"""Handle click on 'Load Config From File' button."""
self.device_manager_display._load_file_action()
self._initialized = True # Set initialized to True after first load
self.stacked_layout.setCurrentWidget(self.device_manager_display)
@SafeSlot()
def _load_config_clicked(self):
"""Handle click on 'Load Current Config' button."""
config = self.client.device_manager._get_redis_device_config()
self.device_manager_display.device_table_view.set_device_config(config)
self._initialized = True # Set initialized to True after first load
self.stacked_layout.setCurrentWidget(self.device_manager_display)
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
from bec_widgets.utils.colors import apply_theme
apply_theme("light")
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(widget)
widget.setLayout(layout)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
device_manager = DeviceManagerWidget()
# config = device_manager.client.device_manager._get_redis_device_config()
# device_manager.device_table_view.set_device_config(config)
layout.addWidget(device_manager)
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
dark_mode_button = DarkModeButton()
layout.addWidget(dark_mode_button)
widget.show()
device_manager.setWindowTitle("Device Manager View")
device_manager.resize(1600, 1200)
# developer_view.set_stretch(horizontal=[1, 3, 2], vertical=[5, 5]) #can be set during runtime
sys.exit(app.exec_())
@@ -0,0 +1,31 @@
from qtpy.QtWidgets import QWidget
from bec_widgets.applications.views.view import ViewBase
from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea
class DockAreaView(ViewBase):
"""
Modular dock area view for arranging and managing multiple dockable widgets.
"""
RPC_CONTENT_CLASS = BECDockArea
def __init__(
self,
parent: QWidget | None = None,
content: QWidget | None = None,
*,
view_id: str | None = None,
title: str | None = None,
**kwargs,
):
super().__init__(parent=parent, content=content, view_id=view_id, title=title, **kwargs)
self.dock_area = BECDockArea(
self,
profile_namespace="bec",
auto_profile_namespace=False,
object_name="DockArea",
rpc_exposed=False,
)
self.set_content(self.dock_area)
+320
View File
@@ -0,0 +1,320 @@
from __future__ import annotations
from typing import List
from pydantic import BaseModel
from qtpy.QtCore import QEventLoop
from qtpy.QtWidgets import (
QDialog,
QDialogButtonBox,
QFormLayout,
QHBoxLayout,
QLabel,
QMessageBox,
QPushButton,
QStackedLayout,
QVBoxLayout,
QWidget,
)
from bec_widgets import BECWidget
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox
from bec_widgets.widgets.plots.waveform.waveform import Waveform
class ViewTourSteps(BaseModel):
"""Model representing tour steps for a view.
Attributes:
view_title: The human-readable title of the view.
step_ids: List of registered step IDs in the order they should appear.
"""
view_title: str
step_ids: List[str]
class ViewBase(BECWidget, QWidget):
"""Wrapper for a content widget used inside the main app's stacked view.
Subclasses can implement `on_enter` and `on_exit` to run custom logic when the view becomes visible or is about to be hidden.
Args:
content (QWidget): The actual view widget to display.
parent (QWidget | None): Parent widget.
view_id (str | None): Optional view view_id, useful for debugging or introspection.
title (str | None): Optional human-readable title.
"""
RPC = True
PLUGIN = False
USER_ACCESS = ["activate"]
RPC_CONTENT_CLASS: type[QWidget] | None = None
RPC_CONTENT_ATTR = "content"
def __init__(
self,
parent: QWidget | None = None,
content: QWidget | None = None,
*,
view_id: str | None = None,
title: str | None = None,
**kwargs,
):
super().__init__(parent=parent, **kwargs)
self.content: QWidget | None = None
self.view_id = view_id
self.view_title = title
lay = QVBoxLayout(self)
lay.setContentsMargins(0, 0, 0, 0)
lay.setSpacing(0)
if content is not None:
self.set_content(content)
def set_content(self, content: QWidget) -> None:
"""Replace the current content widget with a new one."""
if self.content is not None:
self.content.setParent(None)
self.content = content
self.layout().addWidget(content)
@SafeSlot()
def on_enter(self) -> None:
"""Called after the view becomes current/visible.
Default implementation does nothing. Override in subclasses.
"""
pass
@SafeSlot()
def on_exit(self) -> bool:
"""Called before the view is switched away/hidden.
Return True to allow switching, or False to veto.
Default implementation allows switching.
"""
return True
@SafeSlot()
def activate(self) -> None:
"""Switch the parent application to this view."""
if not self.view_id:
raise ValueError("Cannot switch view without a view_id.")
parent = self.parent()
while parent is not None:
if hasattr(parent, "set_current"):
parent.set_current(self.view_id)
return
parent = parent.parent()
raise RuntimeError("Could not find a parent application with set_current().")
def cleanup(self):
if self.content is not None:
self.content.close()
self.content.deleteLater()
super().cleanup()
def register_tour_steps(self, guided_tour, main_app) -> ViewTourSteps | None:
"""Register this view's components with the guided tour.
Args:
guided_tour: The GuidedTour instance to register with.
main_app: The main application instance (for accessing set_current).
Returns:
ViewTourSteps | None: A model containing the view title and step IDs,
or None if this view has no tour steps.
Override this method in subclasses to register view-specific components.
"""
return None
####################################################################################################
# Example views for demonstration/testing purposes
####################################################################################################
# --- Popup UI version ---
class WaveformViewPopup(ViewBase): # pragma: no cover
def __init__(self, parent=None, *args, **kwargs):
super().__init__(parent=parent, *args, **kwargs)
self.waveform = Waveform(parent=self)
self.set_content(self.waveform)
@SafeSlot()
def on_enter(self) -> None:
dialog = QDialog(self)
dialog.setWindowTitle("Configure Waveform View")
label = QLabel("Select device and signal for the waveform plot:", parent=dialog)
# same as in the CurveRow used in waveform
self.device_edit = DeviceComboBox(parent=self)
self.device_edit.insertItem(0, "")
self.device_edit.setEditable(True)
self.device_edit.setCurrentIndex(0)
self.signal_edit = SignalComboBox(parent=self)
self.signal_edit.include_config_signals = False
self.signal_edit.insertItem(0, "")
self.signal_edit.setEditable(True)
self.device_edit.currentTextChanged.connect(self.signal_edit.set_device)
self.device_edit.device_reset.connect(self.signal_edit.reset_selection)
form = QFormLayout()
form.addRow(label)
form.addRow("Device", self.device_edit)
form.addRow("Signal", self.signal_edit)
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, parent=dialog)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
v = QVBoxLayout(dialog)
v.addLayout(form)
v.addWidget(buttons)
if dialog.exec_() == QDialog.Accepted:
self.waveform.plot(
device_y=self.device_edit.currentText(), signal_y=self.signal_edit.currentText()
)
@SafeSlot()
def on_exit(self) -> bool:
ans = QMessageBox.question(
self,
"Switch and clear?",
"Do you want to switch views and clear the plot?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
)
if ans == QMessageBox.Yes:
self.waveform.clear_all()
return True
return False
# --- Inline stacked UI version ---
class WaveformViewInline(ViewBase): # pragma: no cover
def __init__(self, parent=None, *args, **kwargs):
super().__init__(parent=parent, *args, **kwargs)
# Root layout for this view uses a stacked layout
self.stack = QStackedLayout()
container = QWidget(self)
container.setLayout(self.stack)
self.set_content(container)
# --- Page 0: Settings page (inline form)
self.settings_page = QWidget()
sp_layout = QVBoxLayout(self.settings_page)
sp_layout.setContentsMargins(16, 16, 16, 16)
sp_layout.setSpacing(12)
title = QLabel("Select device and signal for the waveform plot:", parent=self.settings_page)
self.device_edit = DeviceComboBox(parent=self.settings_page)
self.device_edit.insertItem(0, "")
self.device_edit.setEditable(True)
self.device_edit.setCurrentIndex(0)
self.entry_edit = SignalComboBox(parent=self.settings_page)
self.entry_edit.include_config_signals = False
self.entry_edit.insertItem(0, "")
self.entry_edit.setEditable(True)
self.device_edit.currentTextChanged.connect(self.entry_edit.set_device)
self.device_edit.device_reset.connect(self.entry_edit.reset_selection)
form = QFormLayout()
form.addRow(title)
form.addRow("Device", self.device_edit)
form.addRow("Signal", self.entry_edit)
btn_row = QHBoxLayout()
ok_btn = QPushButton("OK", parent=self.settings_page)
cancel_btn = QPushButton("Cancel", parent=self.settings_page)
btn_row.addStretch(1)
btn_row.addWidget(cancel_btn)
btn_row.addWidget(ok_btn)
sp_layout.addLayout(form)
sp_layout.addLayout(btn_row)
# --- Page 1: Waveform page
self.waveform_page = QWidget()
wf_layout = QVBoxLayout(self.waveform_page)
wf_layout.setContentsMargins(0, 0, 0, 0)
self.waveform = Waveform(parent=self.waveform_page)
wf_layout.addWidget(self.waveform)
# --- Page 2: Exit confirmation page (inline)
self.confirm_page = QWidget()
cp_layout = QVBoxLayout(self.confirm_page)
cp_layout.setContentsMargins(16, 16, 16, 16)
cp_layout.setSpacing(12)
qlabel = QLabel("Do you want to switch views and clear the plot?", parent=self.confirm_page)
cp_buttons = QHBoxLayout()
no_btn = QPushButton("No", parent=self.confirm_page)
yes_btn = QPushButton("Yes", parent=self.confirm_page)
cp_buttons.addStretch(1)
cp_buttons.addWidget(no_btn)
cp_buttons.addWidget(yes_btn)
cp_layout.addWidget(qlabel)
cp_layout.addLayout(cp_buttons)
# Add pages to the stack
self.stack.addWidget(self.settings_page) # index 0
self.stack.addWidget(self.waveform_page) # index 1
self.stack.addWidget(self.confirm_page) # index 2
# Wire settings buttons
ok_btn.clicked.connect(self._apply_settings_and_show_waveform)
cancel_btn.clicked.connect(self._show_waveform_without_changes)
# Prepare result holder for the inline confirmation
self._exit_choice_yes = None
yes_btn.clicked.connect(lambda: self._exit_reply(True))
no_btn.clicked.connect(lambda: self._exit_reply(False))
@SafeSlot()
def on_enter(self) -> None:
# Always start on the settings page when entering
self.stack.setCurrentIndex(0)
@SafeSlot()
def on_exit(self) -> bool:
# Show inline confirmation page and synchronously wait for a choice
# -> trick to make the choice blocking, however popup would be cleaner solution
self._exit_choice_yes = None
self.stack.setCurrentIndex(2)
loop = QEventLoop()
self._exit_loop = loop
loop.exec_()
if self._exit_choice_yes:
self.waveform.clear_all()
return True
# Revert to waveform view if user cancelled switching
self.stack.setCurrentIndex(1)
return False
def _apply_settings_and_show_waveform(self):
dev = self.device_edit.currentText()
sig = self.entry_edit.currentText()
if dev and sig:
self.waveform.plot(device_y=dev, signal_y=sig)
self.stack.setCurrentIndex(1)
def _show_waveform_without_changes(self):
# Just show waveform page without plotting
self.stack.setCurrentIndex(1)
def _exit_reply(self, yes: bool):
self._exit_choice_yes = bool(yes)
if hasattr(self, "_exit_loop") and self._exit_loop.isRunning():
self._exit_loop.quit()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#EA3323">
<path d="M479.85-265.87q19.8 0 32.69-12.46 12.9-12.46 12.9-32.26 0-19.8-12.75-32.98-12.74-13.17-32.54-13.17-19.8 0-32.69 13.16-12.9 13.15-12.9 32.95 0 19.8 12.75 32.28 12.74 12.48 32.54 12.48Zm-36.46-166.56h79.22v-262.61h-79.22v262.61Zm36.95 366.56q-86.2 0-161.5-32.39-75.3-32.4-131.74-88.84-56.44-56.44-88.84-131.73-32.39-75.3-32.39-161.59t32.39-161.67q32.4-75.37 88.75-131.34t131.69-88.62q75.34-32.65 161.67-32.65 86.34 0 161.78 32.61 75.45 32.6 131.37 88.5 55.93 55.89 88.55 131.45 32.63 75.56 32.63 161.87 0 86.29-32.65 161.58t-88.62 131.48q-55.97 56.18-131.42 88.76-75.46 32.58-161.67 32.58Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 718 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#EA3323">
<path d="m759.04-283.09-63.13-62q49.31-9.43 84.44-46.02 35.13-36.59 35.13-86.14 0-55.49-39.42-95.08-39.43-39.58-94.93-39.58h-153.3v-79.79h152.74q89.28 0 151.7 62.71Q894.7-566.28 894.7-477q0 63.7-38.26 115.96-38.27 52.26-97.4 77.95ZM596.83-443.61l-65.66-66.78h110.05v66.78h-44.39ZM804.96-56 58.48-802.48 106-850l746.48 746.48L804.96-56ZM443.22-265.87H279.43q-89.28 0-151.7-62.42Q65.3-390.72 65.3-480q0-72.57 43.09-129.54 43.09-56.98 112.09-76.07l70.13 70.7h-11.18q-55.73 0-95.32 39.3-39.59 39.31-39.59 95.61t39.66 95.61q39.66 39.3 95.5 39.3h163.54v79.22ZM319.35-446.61v-66.78h77.3l66.78 66.78H319.35Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 721 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFF55">
<path d="M478.3-145.87q-138.65 0-236.39-97.74-97.74-97.74-97.74-236.25t97.74-236.68q97.74-98.16 236.39-98.16 88.4 0 155.45 35.76 67.04 35.76 115.86 98.9V-814.7h66.78v274.92H540.91V-606h165.74q-38.56-57.74-95.3-93.33-56.74-35.58-133.05-35.58-106.88 0-180.89 73.98-74.02 73.99-74.02 180.83 0 106.84 74.02 180.93 74.02 74.08 180.91 74.08 80.16 0 147.74-46.08 67.59-46.09 95.16-121.83H803q-29.56 110.65-119.67 178.89-90.1 68.24-205.03 68.24Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 559 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#75FB4C">
<path d="m419.87-289.52 289.22-289.22-57.31-56.87L419.87-403.7 304.96-518.61l-56.31 56.87 171.22 172.22Zm60.21 223.65q-85.47 0-161.01-32.39-75.53-32.4-131.97-88.84-56.44-56.44-88.84-131.89-32.39-75.46-32.39-160.93 0-86.47 32.39-162.01 32.4-75.53 88.75-131.5t131.85-88.62q75.5-32.65 161.01-32.65 86.52 0 162.12 32.61 75.61 32.6 131.53 88.5 55.93 55.89 88.55 131.45Q894.7-566.58 894.7-480q0 85.55-32.65 161.07-32.65 75.53-88.62 131.9-55.97 56.37-131.42 88.77-75.46 32.39-161.93 32.39Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 604 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#F19E39">
<path d="M27.56-112.65 480-894.7l452.44 782.05H27.56Zm456.62-125.48q13.15 0 22.61-9.64 9.47-9.65 9.47-22.8t-9.64-22.33q-9.65-9.19-22.8-9.19t-22.61 9.36q-9.47 9.36-9.47 22.51 0 13.15 9.64 22.62 9.65 9.47 22.8 9.47ZM454-348h60v-219.48h-60V-348Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 364 B

-3
View File
@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M440.39-440.39H185.87v-79.22h254.52V-774.7h79.22v255.09H774.7v79.22H519.61v254.52h-79.22v-254.52Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 228 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="m145.26-88.13-57.13-57.13 137.39-137.39H105.87v-79.22h256v256h-79.22v-119.65L145.26-88.13Zm669.48 0L677.91-225.52v119.65h-79.78v-256H854.7v79.22H734.48l137.39 137.39-57.13 57.13Zm-708.87-510v-79.78h119.65L88.13-814.74l57.13-57.13 137.39 137.39V-854.7h79.22v256.57h-256Zm492.26 0V-854.7h79.78v120.22l137.83-138.39 57.13 57.13-138.39 137.83H854.7v79.78H598.13Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 489 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M114.02-114.02v-308.13h68.13v192.02l547.72-547.72H537.85v-68.37h308.37v308.37h-68.37v-192.02L230.13-182.15h192.02v68.13H114.02Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 258 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="m311.5-154.02-47.74-47.74 116.94-116.7H74.02v-68.37H380.7L263.76-503.76l47.74-47.74 198.98 198.74L311.5-154.02Zm337-254.72L449.76-607.48 648.5-806.22l47.74 47.74-116.7 116.94h306.68v68.37H579.54l116.7 116.69-47.74 47.74Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 351 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#FFFFFF">
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M17 7h-4v2h4c1.65 0 3 1.35 3 3s-1.35 3-3 3h-4v2h4c2.76 0 5-2.24 5-5s-2.24-5-5-5zm-6 8H7c-1.65 0-3-1.35-3-3s1.35-3 3-3h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-2zm-3-4h8v2H8z"/>
</svg>

Before

Width:  |  Height:  |  Size: 341 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M180-404.87v-50.26h289.75v50.26H180Zm0-162.57v-50.25h454.62v50.25H180Zm0-162.3V-780h454.62v50.26H180ZM524.62-180v-105.69l217.15-216.16q7.46-7.07 16.11-10.3 8.65-3.23 17.3-3.23 9.43 0 18.25 3.53 8.82 3.54 16.03 10.62l37 37.38q6.87 7.47 10.21 16.16Q860-439 860-430.31t-3.37 17.69q-3.37 9-10.52 16.46L630.31-180H524.62Zm250.69-211.69 37-38.62-37-37.38-38 38 38 38Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 492 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M480-54 303.43-230.56 361-288.13l79.39 78.83v-231.09H209.3L283.13-366l-57.57 57.57L54-480l172.56-172.57L284.13-595l-74.83 75.39h231.09v-231.65L366-676.87l-57.57-57.57L480-906l171.57 171.56L594-676.87l-74.39-74.39v231.65h231.65L676.87-594l57.57-57.57L906-480 734.44-308.43 676.87-366l74.39-74.39H519.61v231.09L599-288.13l57.57 57.57L480-54Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 470 B

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="48px" viewBox="0 0 24 24" width="48px"
fill="#FFFFFF">
<g>
<rect fill="none" height="24" width="24"/>
</g>
<g>
<path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M7,9l1.41,1.41L11,7.83V16h2V7.83l2.59,2.58L17,9l-5-5L7,9z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 371 B

-11
View File
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 100 96" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<rect id="Artboard1" x="0" y="0" width="100" height="96.486" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<path d="M11.379,24.832C11.379,24.261 11.843,23.798 12.414,23.798C18.11,23.798 20.117,19.072 22.06,14.503C23.704,10.634 25.403,6.634 29.483,6.634C33.902,6.634 35.376,12.91 36.934,19.555C38.473,26.113 40.065,32.893 44.138,32.893C48.25,32.893 50.78,28.962 53.457,24.8C56.279,20.412 59.198,15.876 64.31,15.876C69.322,15.876 72.165,20.305 74.915,24.588C77.707,28.935 80.343,33.04 84.999,33.04C85.571,33.04 86.034,33.503 86.034,34.075C86.034,34.647 85.571,35.11 84.999,35.11C79.212,35.11 76.004,30.115 73.175,25.708C70.612,21.716 68.192,17.946 64.31,17.946C60.328,17.946 57.835,21.819 55.197,25.92C52.338,30.366 49.381,34.963 44.138,34.963C38.425,34.963 36.643,27.371 34.92,20.028C33.613,14.46 32.262,8.703 29.482,8.703C26.953,8.703 25.71,11.2 23.963,15.311C21.964,20.014 19.479,25.867 12.413,25.867C11.843,25.867 11.379,25.403 11.379,24.832M44.361,44.584C43.504,44.584 42.557,44.882 42.557,45.739L42.586,50.703L39.522,50.703L43.922,61.878L48.807,50.703L45.691,50.703L45.604,46.255C45.602,45.398 45.218,44.584 44.361,44.584ZM6.034,37.487L6.034,6.674L5,6.674L5,38.522L95,38.522L95,37.487L6.034,37.487M77.414,91.881L77.414,63.849C77.414,63.277 76.951,62.814 76.379,62.814C75.808,62.814 75.345,63.277 75.345,63.849L75.345,91.881C75.345,92.045 75.391,92.194 75.458,92.332L61.955,92.332C62.022,92.194 62.068,92.045 62.068,91.881L62.068,82.718C62.068,82.146 61.605,81.683 61.034,81.683C60.462,81.683 59.999,82.146 59.999,82.718L59.999,91.881C59.999,92.045 60.045,92.194 60.112,92.332L45.059,92.332C45.126,92.194 45.172,92.045 45.172,91.881L45.172,75.943C45.172,75.372 44.709,74.909 44.138,74.909C43.567,74.909 43.104,75.372 43.104,75.943L43.104,91.881C43.104,92.045 43.15,92.194 43.217,92.332L23.852,92.332C23.92,92.194 23.966,92.045 23.966,91.881L23.966,63.849C23.966,63.277 23.502,62.814 22.931,62.814C22.36,62.814 21.897,63.277 21.897,63.849L21.897,91.881C21.897,92.045 21.943,92.194 22.011,92.332L6.034,92.332L6.034,62.881L5,62.881L5,93.366L95,93.366L95,92.332L77.301,92.332C77.368,92.194 77.414,92.045 77.414,91.881"
style="fill:white;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="m78.89-112.59 263.02-367.17h202l303.5-354.22v721.39H78.89Zm62.24-262.74-54.7-39.78 166.59-233.02h201L640.46-864.8l51.45 44.26-205.82 240.78H287.33l-146.2 204.43Zm70.54 194.37h567.37v-468.78L574.98-411.63H376.22L211.67-180.96Zm567.37 0Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 366 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#FFFFFF">
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.25 2.52.77-1.28-3.52-2.09V8z"/>
</svg>

Before

Width:  |  Height:  |  Size: 392 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M185.09-105.87q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87v-589.82q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h589.82q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v589.82q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H185.09Zm43.56-166.04h503.7L578-481.48l-132 171-93-127-124.35 165.57Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 413 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M283.85-289.91h67.41l42.32-112.66h172.59l43.33 112.66h68.85l-164-428h-67.5l-163 428Zm127.74-165.72 67.34-182.87H481l68.41 182.87H411.59Zm68.53 381.61q-86.32 0-160.51-31t-128.89-85.7q-54.7-54.7-85.7-128.89-31-74.19-31-160.51 0-85.31 30.94-159.4t85.7-128.9q54.76-54.8 128.95-86.3t160.51-31.5q85.31 0 159.42 31.47 74.1 31.47 128.91 86.27 54.82 54.8 86.29 128.88 31.48 74.08 31.48 159.6 0 86.2-31.5 160.39-31.5 74.19-86.3 128.95-54.81 54.76-128.9 85.7-74.09 30.94-159.4 30.94ZM480-480Zm-.04 337.85q144.08 0 240.99-96.74 96.9-96.74 96.9-241.07 0-144.32-96.86-241.11-96.86-96.78-240.95-96.78-144.08 0-240.99 96.74-96.9 96.74-96.9 241.07 0 144.32 96.86 241.11 96.86 96.78 240.95 96.78Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 809 B

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="48px" viewBox="0 0 24 24" width="48px"
fill="#FFFFFF">
<g>
<rect fill="none" height="24" width="24"/>
</g>
<g>
<path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 377 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M110.39-110.39v-89.57l77.52-77.52v167.09h-77.52Zm165.57 0v-250.13l77.52-77.52v327.65h-77.52Zm165.56 0v-327.65l77.52 77.95v249.7h-77.52Zm165.57 0v-250.83l77.52-76.96v327.79h-77.52Zm165.56 0v-411.83l76.96-76.96v488.79h-76.96ZM110.39-335.65v-112.7L400-735.96l160 160 289.61-290.61v112.14L560-463.26l-160-160-289.61 287.61Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 450 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M725.93-155.93q0-118.18-45-222.09t-122-180.91q-77-77-180.91-122t-222.09-45v-68.14q132.68 0 248.61 50.23 115.92 50.23 202.5 136.75 86.57 86.53 136.8 202.53 50.23 116.01 50.23 248.63h-68.14Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 319 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#FFFFFF">
<path d="M571.91-279.09h191v-194h-60v134h-131v60ZM198.09-486.91h60v-134h131v-60h-191v194Zm-53 341.04q-32.51 0-55.87-23.35-23.35-23.36-23.35-55.87v-509.82q0-32.74 23.35-56.26 23.36-23.53 55.87-23.53h669.82q32.74 0 56.26 23.53 23.53 23.52 23.53 56.26v509.82q0 32.51-23.53 55.87-23.52 23.35-56.26 23.35H145.09Zm0-79.22h669.82v-509.82H145.09v509.82Zm0 0v-509.82 509.82Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 487 B

Some files were not shown because too many files have changed in this diff Show More