diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 000000000..8750daeb5 --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: b1829fb5dbc4892678cef7a4f08618cb +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.doctrees/404.doctree b/.doctrees/404.doctree new file mode 100644 index 000000000..9b8f6fc98 Binary files /dev/null and b/.doctrees/404.doctree differ diff --git a/.doctrees/acknowledgements.doctree b/.doctrees/acknowledgements.doctree new file mode 100644 index 000000000..367823db0 Binary files /dev/null and b/.doctrees/acknowledgements.doctree differ diff --git a/.doctrees/actions/action_helpers.doctree b/.doctrees/actions/action_helpers.doctree new file mode 100644 index 000000000..021c15ab8 Binary files /dev/null and b/.doctrees/actions/action_helpers.doctree differ diff --git a/.doctrees/actions/coolant_actions.doctree b/.doctrees/actions/coolant_actions.doctree new file mode 100644 index 000000000..4bd357a80 Binary files /dev/null and b/.doctrees/actions/coolant_actions.doctree differ diff --git a/.doctrees/actions/index.doctree b/.doctrees/actions/index.doctree new file mode 100644 index 000000000..b5ec06c65 Binary files /dev/null and b/.doctrees/actions/index.doctree differ diff --git a/.doctrees/actions/machine_actions.doctree b/.doctrees/actions/machine_actions.doctree new file mode 100644 index 000000000..60ce18bab Binary files /dev/null and b/.doctrees/actions/machine_actions.doctree differ diff --git a/.doctrees/actions/program_actions.doctree b/.doctrees/actions/program_actions.doctree new file mode 100644 index 000000000..eeade38bf Binary files /dev/null and b/.doctrees/actions/program_actions.doctree differ diff --git a/.doctrees/actions/spindle_actions.doctree b/.doctrees/actions/spindle_actions.doctree new file mode 100644 index 000000000..1ef2bf9fe Binary files /dev/null and b/.doctrees/actions/spindle_actions.doctree differ diff --git a/.doctrees/actions/tool_actions.doctree b/.doctrees/actions/tool_actions.doctree new file mode 100644 index 000000000..6c250683d Binary files /dev/null and b/.doctrees/actions/tool_actions.doctree differ diff --git a/.doctrees/application.doctree b/.doctrees/application.doctree new file mode 100644 index 000000000..181c4e870 Binary files /dev/null and b/.doctrees/application.doctree differ diff --git a/.doctrees/components/action_buttons.doctree b/.doctrees/components/action_buttons.doctree new file mode 100644 index 000000000..5fea58791 Binary files /dev/null and b/.doctrees/components/action_buttons.doctree differ diff --git a/.doctrees/components/action_sliders.doctree b/.doctrees/components/action_sliders.doctree new file mode 100644 index 000000000..667b6c82a Binary files /dev/null and b/.doctrees/components/action_sliders.doctree differ diff --git a/.doctrees/components/backplot.doctree b/.doctrees/components/backplot.doctree new file mode 100644 index 000000000..123d2d50f Binary files /dev/null and b/.doctrees/components/backplot.doctree differ diff --git a/.doctrees/components/containers.doctree b/.doctrees/components/containers.doctree new file mode 100644 index 000000000..0efdc2dd8 Binary files /dev/null and b/.doctrees/components/containers.doctree differ diff --git a/.doctrees/components/index.doctree b/.doctrees/components/index.doctree new file mode 100644 index 000000000..a10e8129c Binary files /dev/null and b/.doctrees/components/index.doctree differ diff --git a/.doctrees/components/stacked_widget.doctree b/.doctrees/components/stacked_widget.doctree new file mode 100644 index 000000000..8b9e35d6d Binary files /dev/null and b/.doctrees/components/stacked_widget.doctree differ diff --git a/.doctrees/components/status_items.doctree b/.doctrees/components/status_items.doctree new file mode 100644 index 000000000..0f639f10f Binary files /dev/null and b/.doctrees/components/status_items.doctree differ diff --git a/.doctrees/components/subcallbutton.doctree b/.doctrees/components/subcallbutton.doctree new file mode 100644 index 000000000..3e6cd8728 Binary files /dev/null and b/.doctrees/components/subcallbutton.doctree differ diff --git a/.doctrees/configuration/index.doctree b/.doctrees/configuration/index.doctree new file mode 100644 index 000000000..8c84f5598 Binary files /dev/null and b/.doctrees/configuration/index.doctree differ diff --git a/.doctrees/configuration/ini_options.doctree b/.doctrees/configuration/ini_options.doctree new file mode 100644 index 000000000..a918994ec Binary files /dev/null and b/.doctrees/configuration/ini_options.doctree differ diff --git a/.doctrees/configuration/yml_config.doctree b/.doctrees/configuration/yml_config.doctree new file mode 100644 index 000000000..1a38139b3 Binary files /dev/null and b/.doctrees/configuration/yml_config.doctree differ diff --git a/.doctrees/designer/plugins/clock.doctree b/.doctrees/designer/plugins/clock.doctree new file mode 100644 index 000000000..9bb8d6653 Binary files /dev/null and b/.doctrees/designer/plugins/clock.doctree differ diff --git a/.doctrees/designer/plugins/index.doctree b/.doctrees/designer/plugins/index.doctree new file mode 100644 index 000000000..1c0977c9c Binary files /dev/null and b/.doctrees/designer/plugins/index.doctree differ diff --git a/.doctrees/designer/plugins/notifications.doctree b/.doctrees/designer/plugins/notifications.doctree new file mode 100644 index 000000000..09a33bc17 Binary files /dev/null and b/.doctrees/designer/plugins/notifications.doctree differ diff --git a/.doctrees/designer/plugins/position.doctree b/.doctrees/designer/plugins/position.doctree new file mode 100644 index 000000000..cb1e47d4c Binary files /dev/null and b/.doctrees/designer/plugins/position.doctree differ diff --git a/.doctrees/designer/plugins/status.doctree b/.doctrees/designer/plugins/status.doctree new file mode 100644 index 000000000..12d426ed4 Binary files /dev/null and b/.doctrees/designer/plugins/status.doctree differ diff --git a/.doctrees/designer/plugins/tool_table.doctree b/.doctrees/designer/plugins/tool_table.doctree new file mode 100644 index 000000000..f11392290 Binary files /dev/null and b/.doctrees/designer/plugins/tool_table.doctree differ diff --git a/.doctrees/designer/widgets/index.doctree b/.doctrees/designer/widgets/index.doctree new file mode 100644 index 000000000..7811bb591 Binary files /dev/null and b/.doctrees/designer/widgets/index.doctree differ diff --git a/.doctrees/development/contribution_guide.doctree b/.doctrees/development/contribution_guide.doctree new file mode 100644 index 000000000..fd9c1332b Binary files /dev/null and b/.doctrees/development/contribution_guide.doctree differ diff --git a/.doctrees/development/dev_environment.doctree b/.doctrees/development/dev_environment.doctree new file mode 100644 index 000000000..e5fc91d60 Binary files /dev/null and b/.doctrees/development/dev_environment.doctree differ diff --git a/.doctrees/environment.pickle b/.doctrees/environment.pickle new file mode 100644 index 000000000..88931b03f Binary files /dev/null and b/.doctrees/environment.pickle differ diff --git a/.doctrees/hal.doctree b/.doctrees/hal.doctree new file mode 100644 index 000000000..143bd3882 Binary files /dev/null and b/.doctrees/hal.doctree differ diff --git a/.doctrees/index.doctree b/.doctrees/index.doctree new file mode 100644 index 000000000..72dd3c35a Binary files /dev/null and b/.doctrees/index.doctree differ diff --git a/.doctrees/install/apt_install.doctree b/.doctrees/install/apt_install.doctree new file mode 100644 index 000000000..eddd11951 Binary files /dev/null and b/.doctrees/install/apt_install.doctree differ diff --git a/.doctrees/install/basic_usage.doctree b/.doctrees/install/basic_usage.doctree new file mode 100644 index 000000000..2de439308 Binary files /dev/null and b/.doctrees/install/basic_usage.doctree differ diff --git a/.doctrees/install/bookworm.doctree b/.doctrees/install/bookworm.doctree new file mode 100644 index 000000000..23acf8fd4 Binary files /dev/null and b/.doctrees/install/bookworm.doctree differ diff --git a/.doctrees/install/build_debs.doctree b/.doctrees/install/build_debs.doctree new file mode 100644 index 000000000..9108c40cc Binary files /dev/null and b/.doctrees/install/build_debs.doctree differ diff --git a/.doctrees/install/bullseye.doctree b/.doctrees/install/bullseye.doctree new file mode 100644 index 000000000..23ed2d9e8 Binary files /dev/null and b/.doctrees/install/bullseye.doctree differ diff --git a/.doctrees/install/dev_install.doctree b/.doctrees/install/dev_install.doctree new file mode 100644 index 000000000..e4b351dcb Binary files /dev/null and b/.doctrees/install/dev_install.doctree differ diff --git a/.doctrees/install/index.doctree b/.doctrees/install/index.doctree new file mode 100644 index 000000000..781483e15 Binary files /dev/null and b/.doctrees/install/index.doctree differ diff --git a/.doctrees/install/prerequisites.doctree b/.doctrees/install/prerequisites.doctree new file mode 100644 index 000000000..ba326e8b2 Binary files /dev/null and b/.doctrees/install/prerequisites.doctree differ diff --git a/.doctrees/install/pypi_install.doctree b/.doctrees/install/pypi_install.doctree new file mode 100644 index 000000000..c22dfb4d3 Binary files /dev/null and b/.doctrees/install/pypi_install.doctree differ diff --git a/.doctrees/install/python2.doctree b/.doctrees/install/python2.doctree new file mode 100644 index 000000000..57ebdce19 Binary files /dev/null and b/.doctrees/install/python2.doctree differ diff --git a/.doctrees/plugins/base_plugins.doctree b/.doctrees/plugins/base_plugins.doctree new file mode 100644 index 000000000..10785642b Binary files /dev/null and b/.doctrees/plugins/base_plugins.doctree differ diff --git a/.doctrees/plugins/clock.doctree b/.doctrees/plugins/clock.doctree new file mode 100644 index 000000000..671198d27 Binary files /dev/null and b/.doctrees/plugins/clock.doctree differ diff --git a/.doctrees/plugins/index.doctree b/.doctrees/plugins/index.doctree new file mode 100644 index 000000000..d17475342 Binary files /dev/null and b/.doctrees/plugins/index.doctree differ diff --git a/.doctrees/plugins/notifications.doctree b/.doctrees/plugins/notifications.doctree new file mode 100644 index 000000000..159315e1d Binary files /dev/null and b/.doctrees/plugins/notifications.doctree differ diff --git a/.doctrees/plugins/positions.doctree b/.doctrees/plugins/positions.doctree new file mode 100644 index 000000000..63b194e4b Binary files /dev/null and b/.doctrees/plugins/positions.doctree differ diff --git a/.doctrees/plugins/status.doctree b/.doctrees/plugins/status.doctree new file mode 100644 index 000000000..902dc33d3 Binary files /dev/null and b/.doctrees/plugins/status.doctree differ diff --git a/.doctrees/plugins/tool_table.doctree b/.doctrees/plugins/tool_table.doctree new file mode 100644 index 000000000..01e293f08 Binary files /dev/null and b/.doctrees/plugins/tool_table.doctree differ diff --git a/.doctrees/showcase/index.doctree b/.doctrees/showcase/index.doctree new file mode 100644 index 000000000..682498e44 Binary files /dev/null and b/.doctrees/showcase/index.doctree differ diff --git a/.doctrees/showcase/lathe_vcps.doctree b/.doctrees/showcase/lathe_vcps.doctree new file mode 100644 index 000000000..63fbda29d Binary files /dev/null and b/.doctrees/showcase/lathe_vcps.doctree differ diff --git a/.doctrees/showcase/mill_vcps.doctree b/.doctrees/showcase/mill_vcps.doctree new file mode 100644 index 000000000..55810b93b Binary files /dev/null and b/.doctrees/showcase/mill_vcps.doctree differ diff --git a/.doctrees/showcase/plasma_vcps.doctree b/.doctrees/showcase/plasma_vcps.doctree new file mode 100644 index 000000000..7f11b225c Binary files /dev/null and b/.doctrees/showcase/plasma_vcps.doctree differ diff --git a/.doctrees/tools/editvcp.doctree b/.doctrees/tools/editvcp.doctree new file mode 100644 index 000000000..9c8be8b64 Binary files /dev/null and b/.doctrees/tools/editvcp.doctree differ diff --git a/.doctrees/tools/index.doctree b/.doctrees/tools/index.doctree new file mode 100644 index 000000000..1a9ef8d95 Binary files /dev/null and b/.doctrees/tools/index.doctree differ diff --git a/.doctrees/tools/qcompile.doctree b/.doctrees/tools/qcompile.doctree new file mode 100644 index 000000000..451a27188 Binary files /dev/null and b/.doctrees/tools/qcompile.doctree differ diff --git a/.doctrees/tutorials/basic_vcp/basic_vcp_01.doctree b/.doctrees/tutorials/basic_vcp/basic_vcp_01.doctree new file mode 100644 index 000000000..00c274438 Binary files /dev/null and b/.doctrees/tutorials/basic_vcp/basic_vcp_01.doctree differ diff --git a/.doctrees/tutorials/basic_vcp/basic_vcp_02.doctree b/.doctrees/tutorials/basic_vcp/basic_vcp_02.doctree new file mode 100644 index 000000000..5e0ab759d Binary files /dev/null and b/.doctrees/tutorials/basic_vcp/basic_vcp_02.doctree differ diff --git a/.doctrees/tutorials/basic_vcp/basic_vcp_03.doctree b/.doctrees/tutorials/basic_vcp/basic_vcp_03.doctree new file mode 100644 index 000000000..0a3ee2cde Binary files /dev/null and b/.doctrees/tutorials/basic_vcp/basic_vcp_03.doctree differ diff --git a/.doctrees/tutorials/basic_vcp/basic_vcp_04.doctree b/.doctrees/tutorials/basic_vcp/basic_vcp_04.doctree new file mode 100644 index 000000000..62dc1dea3 Binary files /dev/null and b/.doctrees/tutorials/basic_vcp/basic_vcp_04.doctree differ diff --git a/.doctrees/tutorials/basic_vcp/basic_vcp_05.doctree b/.doctrees/tutorials/basic_vcp/basic_vcp_05.doctree new file mode 100644 index 000000000..fe62876d1 Binary files /dev/null and b/.doctrees/tutorials/basic_vcp/basic_vcp_05.doctree differ diff --git a/.doctrees/tutorials/basic_vcp/basic_vcp_06.doctree b/.doctrees/tutorials/basic_vcp/basic_vcp_06.doctree new file mode 100644 index 000000000..b451be27a Binary files /dev/null and b/.doctrees/tutorials/basic_vcp/basic_vcp_06.doctree differ diff --git a/.doctrees/tutorials/basic_vcp/basic_vcp_07.doctree b/.doctrees/tutorials/basic_vcp/basic_vcp_07.doctree new file mode 100644 index 000000000..bb33d1952 Binary files /dev/null and b/.doctrees/tutorials/basic_vcp/basic_vcp_07.doctree differ diff --git a/.doctrees/tutorials/basic_vcp/basic_vcp_08.doctree b/.doctrees/tutorials/basic_vcp/basic_vcp_08.doctree new file mode 100644 index 000000000..fe0b36a2a Binary files /dev/null and b/.doctrees/tutorials/basic_vcp/basic_vcp_08.doctree differ diff --git a/.doctrees/tutorials/basic_vcp/index.doctree b/.doctrees/tutorials/basic_vcp/index.doctree new file mode 100644 index 000000000..dc3219002 Binary files /dev/null and b/.doctrees/tutorials/basic_vcp/index.doctree differ diff --git a/.doctrees/tutorials/components/button_groups.doctree b/.doctrees/tutorials/components/button_groups.doctree new file mode 100644 index 000000000..879443dc1 Binary files /dev/null and b/.doctrees/tutorials/components/button_groups.doctree differ diff --git a/.doctrees/tutorials/components/dialogs.doctree b/.doctrees/tutorials/components/dialogs.doctree new file mode 100644 index 000000000..01a65961d Binary files /dev/null and b/.doctrees/tutorials/components/dialogs.doctree differ diff --git a/.doctrees/tutorials/components/dro_tutorial.doctree b/.doctrees/tutorials/components/dro_tutorial.doctree new file mode 100644 index 000000000..07b6b367e Binary files /dev/null and b/.doctrees/tutorials/components/dro_tutorial.doctree differ diff --git a/.doctrees/tutorials/components/index.doctree b/.doctrees/tutorials/components/index.doctree new file mode 100644 index 000000000..201f4545f Binary files /dev/null and b/.doctrees/tutorials/components/index.doctree differ diff --git a/.doctrees/tutorials/components/machine_buttons.doctree b/.doctrees/tutorials/components/machine_buttons.doctree new file mode 100644 index 000000000..00c82eddc Binary files /dev/null and b/.doctrees/tutorials/components/machine_buttons.doctree differ diff --git a/.doctrees/tutorials/components/mdi.doctree b/.doctrees/tutorials/components/mdi.doctree new file mode 100644 index 000000000..ea53e2986 Binary files /dev/null and b/.doctrees/tutorials/components/mdi.doctree differ diff --git a/.doctrees/tutorials/components/overrides.doctree b/.doctrees/tutorials/components/overrides.doctree new file mode 100644 index 000000000..f50f3393f Binary files /dev/null and b/.doctrees/tutorials/components/overrides.doctree differ diff --git a/.doctrees/tutorials/components/touch_screen.doctree b/.doctrees/tutorials/components/touch_screen.doctree new file mode 100644 index 000000000..4c09496c0 Binary files /dev/null and b/.doctrees/tutorials/components/touch_screen.doctree differ diff --git a/.doctrees/tutorials/first_vcp.doctree b/.doctrees/tutorials/first_vcp.doctree new file mode 100644 index 000000000..52ef6615d Binary files /dev/null and b/.doctrees/tutorials/first_vcp.doctree differ diff --git a/.doctrees/tutorials/index.doctree b/.doctrees/tutorials/index.doctree new file mode 100644 index 000000000..4a25fd403 Binary files /dev/null and b/.doctrees/tutorials/index.doctree differ diff --git a/.doctrees/tutorials/misc.doctree b/.doctrees/tutorials/misc.doctree new file mode 100644 index 000000000..faca4c881 Binary files /dev/null and b/.doctrees/tutorials/misc.doctree differ diff --git a/.doctrees/tutorials/vcp_template.doctree b/.doctrees/tutorials/vcp_template.doctree new file mode 100644 index 000000000..e6c35db73 Binary files /dev/null and b/.doctrees/tutorials/vcp_template.doctree differ diff --git a/.doctrees/tutorials/widget_rules.doctree b/.doctrees/tutorials/widget_rules.doctree new file mode 100644 index 000000000..f8115d834 Binary files /dev/null and b/.doctrees/tutorials/widget_rules.doctree differ diff --git a/.doctrees/widgets/base_widget.doctree b/.doctrees/widgets/base_widget.doctree new file mode 100644 index 000000000..c058b14bd Binary files /dev/null and b/.doctrees/widgets/base_widget.doctree differ diff --git a/.doctrees/widgets/buttons/index.doctree b/.doctrees/widgets/buttons/index.doctree new file mode 100644 index 000000000..1b9e95caa Binary files /dev/null and b/.doctrees/widgets/buttons/index.doctree differ diff --git a/.doctrees/widgets/containers/index.doctree b/.doctrees/widgets/containers/index.doctree new file mode 100644 index 000000000..efa83811f Binary files /dev/null and b/.doctrees/widgets/containers/index.doctree differ diff --git a/.doctrees/widgets/dialogs/index.doctree b/.doctrees/widgets/dialogs/index.doctree new file mode 100644 index 000000000..e41543ab4 Binary files /dev/null and b/.doctrees/widgets/dialogs/index.doctree differ diff --git a/.doctrees/widgets/hal/index.doctree b/.doctrees/widgets/hal/index.doctree new file mode 100644 index 000000000..9dd43da62 Binary files /dev/null and b/.doctrees/widgets/hal/index.doctree differ diff --git a/.doctrees/widgets/index.doctree b/.doctrees/widgets/index.doctree new file mode 100644 index 000000000..a4210785a Binary files /dev/null and b/.doctrees/widgets/index.doctree differ diff --git a/.doctrees/widgets/input/index.doctree b/.doctrees/widgets/input/index.doctree new file mode 100644 index 000000000..7a2496c0c Binary files /dev/null and b/.doctrees/widgets/input/index.doctree differ diff --git a/.doctrees/widgets/menus.doctree b/.doctrees/widgets/menus.doctree new file mode 100644 index 000000000..42df8d5f3 Binary files /dev/null and b/.doctrees/widgets/menus.doctree differ diff --git a/.doctrees/widgets/rules.doctree b/.doctrees/widgets/rules.doctree new file mode 100644 index 000000000..456424b4d Binary files /dev/null and b/.doctrees/widgets/rules.doctree differ diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/404.html b/404.html new file mode 100644 index 000000000..320302e87 --- /dev/null +++ b/404.html @@ -0,0 +1,151 @@ + + + + + + + 404 Error - Page Not Found — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

404 Error - Page Not Found

+

The requested page has either moved, or does not exist. +Please try using the links from the sidebar to find what you are looking for.

+

If you think this page should exit, please contact one of the site administrators or open an issue on GitHub.

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 000000000..11e5ecb8a --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +www.qtpyvcp.com diff --git a/_images/backplot-01.png b/_images/backplot-01.png new file mode 100644 index 000000000..11e9babf8 Binary files /dev/null and b/_images/backplot-01.png differ diff --git a/_images/backplot-02.png b/_images/backplot-02.png new file mode 100644 index 000000000..e8e9a7fa1 Binary files /dev/null and b/_images/backplot-02.png differ diff --git a/_images/backplot-03.png b/_images/backplot-03.png new file mode 100644 index 000000000..0ca22664d Binary files /dev/null and b/_images/backplot-03.png differ diff --git a/_images/backplot-04.png b/_images/backplot-04.png new file mode 100644 index 000000000..62ceef62d Binary files /dev/null and b/_images/backplot-04.png differ diff --git a/_images/backplot-05.png b/_images/backplot-05.png new file mode 100644 index 000000000..870297d8d Binary files /dev/null and b/_images/backplot-05.png differ diff --git a/_images/backplot-06.png b/_images/backplot-06.png new file mode 100644 index 000000000..b6f3591bb Binary files /dev/null and b/_images/backplot-06.png differ diff --git a/_images/backplot-07.png b/_images/backplot-07.png new file mode 100644 index 000000000..9bbdcc3e6 Binary files /dev/null and b/_images/backplot-07.png differ diff --git a/_images/backplot-08.png b/_images/backplot-08.png new file mode 100644 index 000000000..967c1ce6c Binary files /dev/null and b/_images/backplot-08.png differ diff --git a/_images/backplot-09.png b/_images/backplot-09.png new file mode 100644 index 000000000..5bbf26b8a Binary files /dev/null and b/_images/backplot-09.png differ diff --git a/_images/backplot-10.png b/_images/backplot-10.png new file mode 100644 index 000000000..8a29b66d2 Binary files /dev/null and b/_images/backplot-10.png differ diff --git a/_images/btn-grp-designer-01.png b/_images/btn-grp-designer-01.png new file mode 100644 index 000000000..00cda940f Binary files /dev/null and b/_images/btn-grp-designer-01.png differ diff --git a/_images/btn-grp-designer-02.png b/_images/btn-grp-designer-02.png new file mode 100644 index 000000000..fee78a23d Binary files /dev/null and b/_images/btn-grp-designer-02.png differ diff --git a/_images/btn-grp-designer-03.png b/_images/btn-grp-designer-03.png new file mode 100644 index 000000000..b5487bf64 Binary files /dev/null and b/_images/btn-grp-designer-03.png differ diff --git a/_images/btn-grp-designer-04.png b/_images/btn-grp-designer-04.png new file mode 100644 index 000000000..dc6670e06 Binary files /dev/null and b/_images/btn-grp-designer-04.png differ diff --git a/_images/btn-grp-designer-05.png b/_images/btn-grp-designer-05.png new file mode 100644 index 000000000..584b94205 Binary files /dev/null and b/_images/btn-grp-designer-05.png differ diff --git a/_images/btn-grp-designer-06.png b/_images/btn-grp-designer-06.png new file mode 100644 index 000000000..743515bf2 Binary files /dev/null and b/_images/btn-grp-designer-06.png differ diff --git a/_images/btn-grp-designer-07.png b/_images/btn-grp-designer-07.png new file mode 100644 index 000000000..bffc74860 Binary files /dev/null and b/_images/btn-grp-designer-07.png differ diff --git a/_images/btn-grp-designer-08.png b/_images/btn-grp-designer-08.png new file mode 100644 index 000000000..c5ee12a8d Binary files /dev/null and b/_images/btn-grp-designer-08.png differ diff --git a/_images/btn-grp-designer-09.png b/_images/btn-grp-designer-09.png new file mode 100644 index 000000000..0416959e5 Binary files /dev/null and b/_images/btn-grp-designer-09.png differ diff --git a/_images/btn-grp-designer-10.png b/_images/btn-grp-designer-10.png new file mode 100644 index 000000000..0b2fe7de7 Binary files /dev/null and b/_images/btn-grp-designer-10.png differ diff --git a/_images/btn-grp-designer-11.png b/_images/btn-grp-designer-11.png new file mode 100644 index 000000000..dfeb753ed Binary files /dev/null and b/_images/btn-grp-designer-11.png differ diff --git a/_images/btn-grp-designer-12.png b/_images/btn-grp-designer-12.png new file mode 100644 index 000000000..df0fc0c39 Binary files /dev/null and b/_images/btn-grp-designer-12.png differ diff --git a/_images/btn-grp-designer-13.png b/_images/btn-grp-designer-13.png new file mode 100644 index 000000000..57f9c960a Binary files /dev/null and b/_images/btn-grp-designer-13.png differ diff --git a/_images/btn-grp-run-01.png b/_images/btn-grp-run-01.png new file mode 100644 index 000000000..a0025adc8 Binary files /dev/null and b/_images/btn-grp-run-01.png differ diff --git a/_images/btn-grp-run-02.png b/_images/btn-grp-run-02.png new file mode 100644 index 000000000..e9b7d5238 Binary files /dev/null and b/_images/btn-grp-run-02.png differ diff --git a/_images/btn-grp-run-03.png b/_images/btn-grp-run-03.png new file mode 100644 index 000000000..b4dc3c435 Binary files /dev/null and b/_images/btn-grp-run-03.png differ diff --git a/_images/btn-grp-run-04.png b/_images/btn-grp-run-04.png new file mode 100644 index 000000000..a8a064880 Binary files /dev/null and b/_images/btn-grp-run-04.png differ diff --git a/_images/config-selector.png b/_images/config-selector.png new file mode 100644 index 000000000..3d214e03f Binary files /dev/null and b/_images/config-selector.png differ diff --git a/_images/containers-01.png b/_images/containers-01.png new file mode 100644 index 000000000..67efd9090 Binary files /dev/null and b/_images/containers-01.png differ diff --git a/_images/containers-02.png b/_images/containers-02.png new file mode 100644 index 000000000..7b637284e Binary files /dev/null and b/_images/containers-02.png differ diff --git a/_images/containers-03.png b/_images/containers-03.png new file mode 100644 index 000000000..6f4f94d1c Binary files /dev/null and b/_images/containers-03.png differ diff --git a/_images/containers-04.png b/_images/containers-04.png new file mode 100644 index 000000000..2d4872c99 Binary files /dev/null and b/_images/containers-04.png differ diff --git a/_images/dro-properties.png b/_images/dro-properties.png new file mode 100644 index 000000000..e67df607e Binary files /dev/null and b/_images/dro-properties.png differ diff --git a/_images/dro-rule-01.png b/_images/dro-rule-01.png new file mode 100644 index 000000000..b4eeedb50 Binary files /dev/null and b/_images/dro-rule-01.png differ diff --git a/_images/dro-rule-02.png b/_images/dro-rule-02.png new file mode 100644 index 000000000..45689c36b Binary files /dev/null and b/_images/dro-rule-02.png differ diff --git a/_images/edit-style-sheet.png b/_images/edit-style-sheet.png new file mode 100644 index 000000000..424458215 Binary files /dev/null and b/_images/edit-style-sheet.png differ diff --git a/_images/home-rule1.png b/_images/home-rule1.png new file mode 100644 index 000000000..c8a1c39dc Binary files /dev/null and b/_images/home-rule1.png differ diff --git a/_images/home-rule2-ss.png b/_images/home-rule2-ss.png new file mode 100644 index 000000000..8c5f2c4b9 Binary files /dev/null and b/_images/home-rule2-ss.png differ diff --git a/_images/home-rule2.png b/_images/home-rule2.png new file mode 100644 index 000000000..8946a5be8 Binary files /dev/null and b/_images/home-rule2.png differ diff --git a/_images/homed_label_example.png b/_images/homed_label_example.png new file mode 100644 index 000000000..21dc83c24 Binary files /dev/null and b/_images/homed_label_example.png differ diff --git a/_images/homing_menu.png b/_images/homing_menu.png new file mode 100644 index 000000000..9a3750535 Binary files /dev/null and b/_images/homing_menu.png differ diff --git a/_images/jcnc.png b/_images/jcnc.png new file mode 100644 index 000000000..5c224552d Binary files /dev/null and b/_images/jcnc.png differ diff --git a/_images/monokrom.png b/_images/monokrom.png new file mode 100644 index 000000000..2c3f75f85 Binary files /dev/null and b/_images/monokrom.png differ diff --git a/_images/open_editor.gif b/_images/open_editor.gif new file mode 100644 index 000000000..f28a32ac1 Binary files /dev/null and b/_images/open_editor.gif differ diff --git a/_images/probebasic-lathe.png b/_images/probebasic-lathe.png new file mode 100644 index 000000000..abe5b0491 Binary files /dev/null and b/_images/probebasic-lathe.png differ diff --git a/_images/recent_files_menu.png b/_images/recent_files_menu.png new file mode 100644 index 000000000..09b530e0f Binary files /dev/null and b/_images/recent_files_menu.png differ diff --git a/_images/rules-01.png b/_images/rules-01.png new file mode 100644 index 000000000..a3ba95c54 Binary files /dev/null and b/_images/rules-01.png differ diff --git a/_images/rules-02.png b/_images/rules-02.png new file mode 100644 index 000000000..7f6f296d9 Binary files /dev/null and b/_images/rules-02.png differ diff --git a/_images/rules-03.png b/_images/rules-03.png new file mode 100644 index 000000000..e3090bb85 Binary files /dev/null and b/_images/rules-03.png differ diff --git a/_images/rules-04.png b/_images/rules-04.png new file mode 100644 index 000000000..398c78ea8 Binary files /dev/null and b/_images/rules-04.png differ diff --git a/_images/rules-041.png b/_images/rules-041.png new file mode 100644 index 000000000..ca91719e5 Binary files /dev/null and b/_images/rules-041.png differ diff --git a/_images/rules-05.png b/_images/rules-05.png new file mode 100644 index 000000000..cdead684a Binary files /dev/null and b/_images/rules-05.png differ diff --git a/_images/rules-06.png b/_images/rules-06.png new file mode 100644 index 000000000..729736d85 Binary files /dev/null and b/_images/rules-06.png differ diff --git a/_images/rules-08.png b/_images/rules-08.png new file mode 100644 index 000000000..58175c251 Binary files /dev/null and b/_images/rules-08.png differ diff --git a/_images/rules-09.png b/_images/rules-09.png new file mode 100644 index 000000000..80ecde331 Binary files /dev/null and b/_images/rules-09.png differ diff --git a/_images/rules-10.png b/_images/rules-10.png new file mode 100644 index 000000000..94cfae923 Binary files /dev/null and b/_images/rules-10.png differ diff --git a/_images/rules-11.png b/_images/rules-11.png new file mode 100644 index 000000000..1f257cd9c Binary files /dev/null and b/_images/rules-11.png differ diff --git a/_images/rules_editor.png b/_images/rules_editor.png new file mode 100644 index 000000000..3f8547339 Binary files /dev/null and b/_images/rules_editor.png differ diff --git a/_images/slider-rule-01.png b/_images/slider-rule-01.png new file mode 100644 index 000000000..a2b7ddb8a Binary files /dev/null and b/_images/slider-rule-01.png differ diff --git a/_images/slider-style-01.png b/_images/slider-style-01.png new file mode 100644 index 000000000..1e8a3cf38 Binary files /dev/null and b/_images/slider-style-01.png differ diff --git a/_images/stacked-01.png b/_images/stacked-01.png new file mode 100644 index 000000000..f804fea0c Binary files /dev/null and b/_images/stacked-01.png differ diff --git a/_images/stacked-02.png b/_images/stacked-02.png new file mode 100644 index 000000000..9ffee9900 Binary files /dev/null and b/_images/stacked-02.png differ diff --git a/_images/stacked-03.png b/_images/stacked-03.png new file mode 100644 index 000000000..64bdfaf2f Binary files /dev/null and b/_images/stacked-03.png differ diff --git a/_images/stacked-04.png b/_images/stacked-04.png new file mode 100644 index 000000000..432b919e9 Binary files /dev/null and b/_images/stacked-04.png differ diff --git a/_images/tab-bar-style.png b/_images/tab-bar-style.png new file mode 100644 index 000000000..9022947c0 Binary files /dev/null and b/_images/tab-bar-style.png differ diff --git a/_images/vcp-chooser.png b/_images/vcp-chooser.png new file mode 100644 index 000000000..15bfd2f6d Binary files /dev/null and b/_images/vcp-chooser.png differ diff --git a/_images/vcp-chooser1.png b/_images/vcp-chooser1.png new file mode 100644 index 000000000..7b496873d Binary files /dev/null and b/_images/vcp-chooser1.png differ diff --git a/_images/vcp-template-01.png b/_images/vcp-template-01.png new file mode 100644 index 000000000..ff0f6b409 Binary files /dev/null and b/_images/vcp-template-01.png differ diff --git a/_images/vcp-template-02.png b/_images/vcp-template-02.png new file mode 100644 index 000000000..32e34429e Binary files /dev/null and b/_images/vcp-template-02.png differ diff --git a/_images/vcp-template-03.png b/_images/vcp-template-03.png new file mode 100644 index 000000000..3587794e5 Binary files /dev/null and b/_images/vcp-template-03.png differ diff --git a/_images/vcp-template-04.png b/_images/vcp-template-04.png new file mode 100644 index 000000000..76592dfb1 Binary files /dev/null and b/_images/vcp-template-04.png differ diff --git a/_images/vcp-template-05.png b/_images/vcp-template-05.png new file mode 100644 index 000000000..2fb97bf08 Binary files /dev/null and b/_images/vcp-template-05.png differ diff --git a/_images/vcp1-01.png b/_images/vcp1-01.png new file mode 100644 index 000000000..d07219d9b Binary files /dev/null and b/_images/vcp1-01.png differ diff --git a/_images/vcp1-02.png b/_images/vcp1-02.png new file mode 100644 index 000000000..5fcea558a Binary files /dev/null and b/_images/vcp1-02.png differ diff --git a/_images/vcp1-03.png b/_images/vcp1-03.png new file mode 100644 index 000000000..e82de99bb Binary files /dev/null and b/_images/vcp1-03.png differ diff --git a/_images/vcp1-04.png b/_images/vcp1-04.png new file mode 100644 index 000000000..7d25776bb Binary files /dev/null and b/_images/vcp1-04.png differ diff --git a/_images/vcp1-05.png b/_images/vcp1-05.png new file mode 100644 index 000000000..cf5b3e631 Binary files /dev/null and b/_images/vcp1-05.png differ diff --git a/_images/vcp1-06.png b/_images/vcp1-06.png new file mode 100644 index 000000000..365a4cef0 Binary files /dev/null and b/_images/vcp1-06.png differ diff --git a/_images/vcp1-config-selector.png b/_images/vcp1-config-selector.png new file mode 100644 index 000000000..5c3908bc3 Binary files /dev/null and b/_images/vcp1-config-selector.png differ diff --git a/_images/vcp1-designer-01.png b/_images/vcp1-designer-01.png new file mode 100644 index 000000000..c16e73c60 Binary files /dev/null and b/_images/vcp1-designer-01.png differ diff --git a/_images/vcp1-designer-02.png b/_images/vcp1-designer-02.png new file mode 100644 index 000000000..ababd839f Binary files /dev/null and b/_images/vcp1-designer-02.png differ diff --git a/_images/vcp1-designer-03.png b/_images/vcp1-designer-03.png new file mode 100644 index 000000000..9bdba3903 Binary files /dev/null and b/_images/vcp1-designer-03.png differ diff --git a/_images/vcp1-designer-04.png b/_images/vcp1-designer-04.png new file mode 100644 index 000000000..781bfbb28 Binary files /dev/null and b/_images/vcp1-designer-04.png differ diff --git a/_images/vcp1-designer-05.png b/_images/vcp1-designer-05.png new file mode 100644 index 000000000..b86ae0221 Binary files /dev/null and b/_images/vcp1-designer-05.png differ diff --git a/_images/vcp1-designer-06.png b/_images/vcp1-designer-06.png new file mode 100644 index 000000000..87aa1f153 Binary files /dev/null and b/_images/vcp1-designer-06.png differ diff --git a/_images/vcp1-designer-07.png b/_images/vcp1-designer-07.png new file mode 100644 index 000000000..6903a55b7 Binary files /dev/null and b/_images/vcp1-designer-07.png differ diff --git a/_images/vcp1-designer-08.png b/_images/vcp1-designer-08.png new file mode 100644 index 000000000..1f7bbdfcf Binary files /dev/null and b/_images/vcp1-designer-08.png differ diff --git a/_images/vcp1-designer-09.png b/_images/vcp1-designer-09.png new file mode 100644 index 000000000..cb9b69003 Binary files /dev/null and b/_images/vcp1-designer-09.png differ diff --git a/_images/vcp1-designer-10.png b/_images/vcp1-designer-10.png new file mode 100644 index 000000000..9dd330b33 Binary files /dev/null and b/_images/vcp1-designer-10.png differ diff --git a/_images/vcp1-designer-11.png b/_images/vcp1-designer-11.png new file mode 100644 index 000000000..3fab51d87 Binary files /dev/null and b/_images/vcp1-designer-11.png differ diff --git a/_images/vcp1-designer-12.png b/_images/vcp1-designer-12.png new file mode 100644 index 000000000..3e14eea51 Binary files /dev/null and b/_images/vcp1-designer-12.png differ diff --git a/_images/vcp1-designer-13.png b/_images/vcp1-designer-13.png new file mode 100644 index 000000000..ad3a7ec22 Binary files /dev/null and b/_images/vcp1-designer-13.png differ diff --git a/_images/vcp1-designer-14.png b/_images/vcp1-designer-14.png new file mode 100644 index 000000000..f79832bbe Binary files /dev/null and b/_images/vcp1-designer-14.png differ diff --git a/_images/vcp1-designer-15.png b/_images/vcp1-designer-15.png new file mode 100644 index 000000000..df3b250aa Binary files /dev/null and b/_images/vcp1-designer-15.png differ diff --git a/_images/vcp1-designer-16.png b/_images/vcp1-designer-16.png new file mode 100644 index 000000000..664ba9895 Binary files /dev/null and b/_images/vcp1-designer-16.png differ diff --git a/_images/vcp1-designer-17.png b/_images/vcp1-designer-17.png new file mode 100644 index 000000000..291477c25 Binary files /dev/null and b/_images/vcp1-designer-17.png differ diff --git a/_images/vcp1-designer-18.png b/_images/vcp1-designer-18.png new file mode 100644 index 000000000..7b4d07a27 Binary files /dev/null and b/_images/vcp1-designer-18.png differ diff --git a/_images/vcp1-designer-19.png b/_images/vcp1-designer-19.png new file mode 100644 index 000000000..ffa0dcf20 Binary files /dev/null and b/_images/vcp1-designer-19.png differ diff --git a/_images/vcp1-designer-20.png b/_images/vcp1-designer-20.png new file mode 100644 index 000000000..09065e6e2 Binary files /dev/null and b/_images/vcp1-designer-20.png differ diff --git a/_images/vcp1-designer-21.png b/_images/vcp1-designer-21.png new file mode 100644 index 000000000..9e18bec2f Binary files /dev/null and b/_images/vcp1-designer-21.png differ diff --git a/_images/vcp1-designer-22.png b/_images/vcp1-designer-22.png new file mode 100644 index 000000000..69342b7bc Binary files /dev/null and b/_images/vcp1-designer-22.png differ diff --git a/_images/vcp1-designer-23.png b/_images/vcp1-designer-23.png new file mode 100644 index 000000000..b430dda5c Binary files /dev/null and b/_images/vcp1-designer-23.png differ diff --git a/_images/vcp1-designer-24.png b/_images/vcp1-designer-24.png new file mode 100644 index 000000000..ab5fe3952 Binary files /dev/null and b/_images/vcp1-designer-24.png differ diff --git a/_images/vcp1-designer-25.png b/_images/vcp1-designer-25.png new file mode 100644 index 000000000..acfde1f23 Binary files /dev/null and b/_images/vcp1-designer-25.png differ diff --git a/_images/vcp1-designer-26.png b/_images/vcp1-designer-26.png new file mode 100644 index 000000000..39d1041c7 Binary files /dev/null and b/_images/vcp1-designer-26.png differ diff --git a/_images/vcp1-designer-27.png b/_images/vcp1-designer-27.png new file mode 100644 index 000000000..b18f7cb41 Binary files /dev/null and b/_images/vcp1-designer-27.png differ diff --git a/_images/vcp1-designer-28.png b/_images/vcp1-designer-28.png new file mode 100644 index 000000000..19e5d2cc8 Binary files /dev/null and b/_images/vcp1-designer-28.png differ diff --git a/_images/vcp1-designer-29.png b/_images/vcp1-designer-29.png new file mode 100644 index 000000000..fced7d35e Binary files /dev/null and b/_images/vcp1-designer-29.png differ diff --git a/_images/vcp1-designer-30.png b/_images/vcp1-designer-30.png new file mode 100644 index 000000000..c9e7eb9c0 Binary files /dev/null and b/_images/vcp1-designer-30.png differ diff --git a/_images/vcp1-designer-31.png b/_images/vcp1-designer-31.png new file mode 100644 index 000000000..f79ef0480 Binary files /dev/null and b/_images/vcp1-designer-31.png differ diff --git a/_images/vcp1-designer-32.png b/_images/vcp1-designer-32.png new file mode 100644 index 000000000..051416455 Binary files /dev/null and b/_images/vcp1-designer-32.png differ diff --git a/_images/vcp1-designer-33.png b/_images/vcp1-designer-33.png new file mode 100644 index 000000000..42c13e925 Binary files /dev/null and b/_images/vcp1-designer-33.png differ diff --git a/_images/vcp1-designer-34.png b/_images/vcp1-designer-34.png new file mode 100644 index 000000000..19173aa3e Binary files /dev/null and b/_images/vcp1-designer-34.png differ diff --git a/_images/vcp1-run-01.png b/_images/vcp1-run-01.png new file mode 100644 index 000000000..97cc88cd1 Binary files /dev/null and b/_images/vcp1-run-01.png differ diff --git a/_images/vcp1-run-02.png b/_images/vcp1-run-02.png new file mode 100644 index 000000000..d90821090 Binary files /dev/null and b/_images/vcp1-run-02.png differ diff --git a/_images/vcp1-run-03.png b/_images/vcp1-run-03.png new file mode 100644 index 000000000..bb96fde2c Binary files /dev/null and b/_images/vcp1-run-03.png differ diff --git a/_images/vcp1-run-04.png b/_images/vcp1-run-04.png new file mode 100644 index 000000000..6d23b1481 Binary files /dev/null and b/_images/vcp1-run-04.png differ diff --git a/_images/vcp1-run-05.png b/_images/vcp1-run-05.png new file mode 100644 index 000000000..0e6d8f282 Binary files /dev/null and b/_images/vcp1-run-05.png differ diff --git a/_images/vcp1-run-06.png b/_images/vcp1-run-06.png new file mode 100644 index 000000000..c5162ee6d Binary files /dev/null and b/_images/vcp1-run-06.png differ diff --git a/_images/vcp1-run-07.png b/_images/vcp1-run-07.png new file mode 100644 index 000000000..33e6e3bdb Binary files /dev/null and b/_images/vcp1-run-07.png differ diff --git a/_images/vcp1-run-08.png b/_images/vcp1-run-08.png new file mode 100644 index 000000000..c8d1d092e Binary files /dev/null and b/_images/vcp1-run-08.png differ diff --git a/_images/vcp1-run-09.png b/_images/vcp1-run-09.png new file mode 100644 index 000000000..9ec6f6dc5 Binary files /dev/null and b/_images/vcp1-run-09.png differ diff --git a/_images/vcp1-run-10.png b/_images/vcp1-run-10.png new file mode 100644 index 000000000..00c61183a Binary files /dev/null and b/_images/vcp1-run-10.png differ diff --git a/_images/vcp1-run-11.png b/_images/vcp1-run-11.png new file mode 100644 index 000000000..1fe9c7973 Binary files /dev/null and b/_images/vcp1-run-11.png differ diff --git a/_images/vcp1-run-12.png b/_images/vcp1-run-12.png new file mode 100644 index 000000000..1b2c7df9a Binary files /dev/null and b/_images/vcp1-run-12.png differ diff --git a/_images/vcp1-run-13.png b/_images/vcp1-run-13.png new file mode 100644 index 000000000..96e4f578a Binary files /dev/null and b/_images/vcp1-run-13.png differ diff --git a/_images/vcp1-run-14.png b/_images/vcp1-run-14.png new file mode 100644 index 000000000..e175a430a Binary files /dev/null and b/_images/vcp1-run-14.png differ diff --git a/_images/vcp1run-01.png b/_images/vcp1run-01.png new file mode 100644 index 000000000..322f186b0 Binary files /dev/null and b/_images/vcp1run-01.png differ diff --git a/_images/vcp1run-02.png b/_images/vcp1run-02.png new file mode 100644 index 000000000..a2aaedca4 Binary files /dev/null and b/_images/vcp1run-02.png differ diff --git a/_images/vcp1run-03.png b/_images/vcp1run-03.png new file mode 100644 index 000000000..1b4486e66 Binary files /dev/null and b/_images/vcp1run-03.png differ diff --git a/_images/vcp1run-04.png b/_images/vcp1run-04.png new file mode 100644 index 000000000..e55ded05e Binary files /dev/null and b/_images/vcp1run-04.png differ diff --git a/_images/vcp1run-05.png b/_images/vcp1run-05.png new file mode 100644 index 000000000..167e531bb Binary files /dev/null and b/_images/vcp1run-05.png differ diff --git a/_images/vcp1run-06.png b/_images/vcp1run-06.png new file mode 100644 index 000000000..1b2cb3fc2 Binary files /dev/null and b/_images/vcp1run-06.png differ diff --git a/_images/vcp1run-07.png b/_images/vcp1run-07.png new file mode 100644 index 000000000..76c27123f Binary files /dev/null and b/_images/vcp1run-07.png differ diff --git a/_images/vcp1run-08.png b/_images/vcp1run-08.png new file mode 100644 index 000000000..f7ffa118e Binary files /dev/null and b/_images/vcp1run-08.png differ diff --git a/_images/vcp1run-09.png b/_images/vcp1run-09.png new file mode 100644 index 000000000..de56c3dca Binary files /dev/null and b/_images/vcp1run-09.png differ diff --git a/_images/vcp1run-10.png b/_images/vcp1run-10.png new file mode 100644 index 000000000..6c4347cf6 Binary files /dev/null and b/_images/vcp1run-10.png differ diff --git a/_images/vcp1run-11.png b/_images/vcp1run-11.png new file mode 100644 index 000000000..84015028a Binary files /dev/null and b/_images/vcp1run-11.png differ diff --git a/_images/vcp1run-12.png b/_images/vcp1run-12.png new file mode 100644 index 000000000..54f6ccc3d Binary files /dev/null and b/_images/vcp1run-12.png differ diff --git a/_images/vcp1run-13.png b/_images/vcp1run-13.png new file mode 100644 index 000000000..4328acc5c Binary files /dev/null and b/_images/vcp1run-13.png differ diff --git a/_images/vcp1run-14.png b/_images/vcp1run-14.png new file mode 100644 index 000000000..fae57ef1e Binary files /dev/null and b/_images/vcp1run-14.png differ diff --git a/_images/vcp1run-15.png b/_images/vcp1run-15.png new file mode 100644 index 000000000..bc68dbf12 Binary files /dev/null and b/_images/vcp1run-15.png differ diff --git a/_images/vcp1run-16.png b/_images/vcp1run-16.png new file mode 100644 index 000000000..6023e7b3e Binary files /dev/null and b/_images/vcp1run-16.png differ diff --git a/_images/vcp1run-17.png b/_images/vcp1run-17.png new file mode 100644 index 000000000..173e102f8 Binary files /dev/null and b/_images/vcp1run-17.png differ diff --git a/_images/vcp1run-18.png b/_images/vcp1run-18.png new file mode 100644 index 000000000..09328ff9d Binary files /dev/null and b/_images/vcp1run-18.png differ diff --git a/_images/vcp1run-19.png b/_images/vcp1run-19.png new file mode 100644 index 000000000..9bba7df07 Binary files /dev/null and b/_images/vcp1run-19.png differ diff --git a/_modules/index.html b/_modules/index.html new file mode 100644 index 000000000..27588fe34 --- /dev/null +++ b/_modules/index.html @@ -0,0 +1,205 @@ + + + + + + Overview: module code — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

All modules for which code is available

+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/actions.html b/_modules/qtpyvcp/actions.html new file mode 100644 index 000000000..ecc7abb49 --- /dev/null +++ b/_modules/qtpyvcp/actions.html @@ -0,0 +1,260 @@ + + + + + + qtpyvcp.actions — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for qtpyvcp.actions

+import os
+import sys
+from qtpy.QtWidgets import QAction, QPushButton, QCheckBox, QSlider, QSpinBox, QComboBox, QDial
+
+from . import machine_actions as machine
+from . import program_actions as program
+from . import spindle_actions as spindle
+from . import coolant_actions as coolant
+from . import tool_actions as tool
+
+# Set up logging
+from qtpyvcp.utilities import logger
+LOG = logger.getLogger(__name__)
+
+IN_DESIGNER = os.getenv('DESIGNER', False)
+
+
[docs]class InvalidAction(Exception): + pass
+ +
[docs]def bindWidget(widget, action): + """Binds a widget to an action. + + Args: + widget (QWidget) : The widget to bind the action too. Typically `widget` + is a QPushButton, QCheckBox, QComboBox, QSlider or QAction instance. + + action (str) : The string identifier of the action to bind the widget + to, in the format ``action_class.action_name:arg1, arg2 ...``. + + Example: + A QPushButton or QCheckBox would typically be bound to an action + that does not take an argument, for example ``machine.power.toggle``:: + + bindWidget(widget, 'machine.power.toggle') + + But it is possible to specify an argument by appending an ':' followed + by the argument value. For example we can bind a QPushButton so that it + homes the X axis when the button is pressed:: + + bindWidget(widget, 'machine.home.axis:x') + + Widgets such as QSliders and QComboBoxs that have a value associated + with them can also be bound to a action, and the value will + automatically be passed to the action. For example we can bind + a QSLider to the ``spindle.0.override`` action:: + + bindWidget(widget, 'spindle.0.override') + """ + action, sep, args = str(action).partition(':') + action = action.replace('-', '_') + + kwargs = {} + + prev_item = '' + method = sys.modules[__name__] + for item in action.split('.'): + if item.isdigit(): + kwargs[prev_item] = int(item) + continue + try: + method = getattr(method, item) + except(AttributeError, KeyError): + if IN_DESIGNER: + return + else: + raise InvalidAction("Could not get action method: %s" % item) + + prev_item = item + + if method is None or not callable(method): + if IN_DESIGNER: + return + else: + raise InvalidAction('Method is not callable: %s' % method) + + if args != '': + # make a list out of comma separated args + args = args.replace(' ', '').split(',') + # convert numbers to int and unicode to str + args = [int(arg) if arg.isdigit() else str(arg) for arg in args] + + if isinstance(widget, QAction): + widget.triggered.connect(lambda: method(*args, **kwargs)) # should be able to do widget.triggered[()] + + # if it is a toggle action make the menu item checkable + if action.endswith('toggle'): + widget.setCheckable(True) + + elif isinstance(widget, QPushButton) or isinstance(widget, QCheckBox): + + if action.startswith('machine.jog.axis'): + widget.pressed.connect(lambda: method(*args, **kwargs)) + widget.released.connect(lambda: method(*args, speed=0, **kwargs)) + + else: + widget.clicked.connect(lambda: method(*args, **kwargs)) + + elif isinstance(widget, QSlider) or isinstance(widget, QSpinBox) or isinstance(widget, QDial): + widget.valueChanged.connect(method) + + elif isinstance(widget, QComboBox): + widget.activated[str].connect(method) + + else: + raise InvalidAction('Can\'t bind action "{}" to unsupported widget type "{}"' + .format(action, widget.__class__.__name__)) + + try: + # Set the initial widget OK state and update on changes + method.ok(*args, widget=widget, **kwargs) + method.bindOk(*args, widget=widget, **kwargs) + except Exception as e: + msg = "%s raised while trying to bind '%s' action to '%s'" % \ + (e, action, widget) + raise InvalidAction(msg, sys.exc_info()[2])
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/actions/base_actions.html b/_modules/qtpyvcp/actions/base_actions.html new file mode 100644 index 000000000..71781e3a5 --- /dev/null +++ b/_modules/qtpyvcp/actions/base_actions.html @@ -0,0 +1,184 @@ + + + + + + qtpyvcp.actions.base_actions — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for qtpyvcp.actions.base_actions

+import linuxcnc
+from qtpyvcp.utilities import logger
+from qtpyvcp.utilities.info import Info
+from qtpyvcp.plugins import getPlugin
+
+STATUS = getPlugin('status')
+STAT = STATUS.stat
+
+INFO = Info()
+CMD = linuxcnc.command()
+
+
+# Set up logging
+LOG = logger.getLogger(__name__)
+
+
[docs]def setTaskMode(new_mode): + """Sets task mode, if possible + + Args: + new_mode (int) : linuxcnc.MODE_MANUAL, linuxcnc.MODE_MDI or linuxcnc.MODE_AUTO + + Returns: + bool : True if successful + """ + if isRunning(): + LOG.error("Can't set mode while machine is running") + return False + else: + CMD.mode(new_mode) + return True
+ +
[docs]def isRunning(): + """Returns TRUE if machine is moving due to MDI, program execution, etc.""" + if STAT.state == linuxcnc.RCS_EXEC: + return True + else: + return STAT.task_mode == linuxcnc.MODE_AUTO \ + and STAT.interp_state != linuxcnc.INTERP_IDLE
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/actions/coolant_actions.html b/_modules/qtpyvcp/actions/coolant_actions.html new file mode 100644 index 000000000..91600ddc3 --- /dev/null +++ b/_modules/qtpyvcp/actions/coolant_actions.html @@ -0,0 +1,282 @@ + + + + + + qtpyvcp.actions.coolant_actions — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for qtpyvcp.actions.coolant_actions

+import linuxcnc
+
+# Set up logging
+from qtpyvcp.utilities import logger
+LOG = logger.getLogger(__name__)
+
+from qtpyvcp.plugins import getPlugin
+
+STATUS = getPlugin('status')
+STAT = STATUS.stat
+
+CMD = linuxcnc.command()
+
+#==============================================================================
+# Coolent actions
+#==============================================================================
+
+
[docs]class flood: + """Flood Actions Group""" +
[docs] @staticmethod + def on(): + """Turns Flood coolant ON + + ActionButton syntax:: + + coolant.flood.on + """ + LOG.debug("Turning Flood coolant green<ON>") + CMD.flood(linuxcnc.FLOOD_ON)
+ +
[docs] @staticmethod + def off(): + """Turns Flood coolant OFF + + ActionButton syntax:: + + coolant.flood.off + """ + LOG.debug("Turning Flood coolant red<OFF>") + CMD.flood(linuxcnc.FLOOD_OFF)
+ +
[docs] @staticmethod + def toggle(): + """Toggles Flood coolant ON/OFF + + ActionButton syntax:: + + coolant.flood.toggle + """ + if STAT.flood == linuxcnc.FLOOD_ON: + flood.off() + else: + flood.on()
+ +
[docs]class mist: + """Mist Actions Group""" +
[docs] @staticmethod + def on(): + """Turns Mist coolant ON + + ActionButton syntax:: + + coolant.mist.on + """ + LOG.debug("Turning Mist coolant green<ON>") + CMD.mist(linuxcnc.MIST_ON)
+ +
[docs] @staticmethod + def off(): + """Turns Mist coolant OFF + + ActionButton syntax:: + + coolant.mist.off + """ + LOG.debug("Turning Mist coolant red<OFF>") + CMD.mist(linuxcnc.MIST_OFF)
+ +
[docs] @staticmethod + def toggle(): + """Toggles Mist coolant ON/OFF + + ActionButton syntax:: + + coolant.mist.toggle + """ + if STAT.mist == linuxcnc.MIST_ON: + mist.off() + else: + mist.on()
+ +def _coolant_ok(widget=None): + """Checks if it is OK to turn coolant ON. + + Args: + widget (QWidget, optional) : Widget to enable/disable according to result. + + Atributes: + msg (string) : The reason the action is not permitted, or empty if permitted. + + Retuns: + bool : True if OK, else False. + """ + if STAT.task_state == linuxcnc.STATE_ON: + ok = True + msg = "" + else: + ok = False + msg = "Can't turn on coolant when machine is not ON" + + _coolant_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _flood_bindOk(widget): + _coolant_ok(widget) + widget.setChecked(STAT.flood == linuxcnc.FLOOD_ON) + STATUS.task_state.onValueChanged(lambda: _coolant_ok(widget)) + STATUS.flood.onValueChanged(lambda s: widget.setChecked(s == linuxcnc.FLOOD_ON)) + +def _mist_bindOk(widget): + _coolant_ok(widget) + widget.setChecked(STAT.mist == linuxcnc.MIST_ON) + STATUS.task_state.onValueChanged(lambda: _coolant_ok(widget)) + STATUS.mist.onValueChanged(lambda s: widget.setChecked(s == linuxcnc.MIST_ON)) + +flood.on.ok = flood.off.ok = flood.toggle.ok = _coolant_ok +flood.on.bindOk = flood.off.bindOk = flood.toggle.bindOk = _flood_bindOk + +mist.on.ok = mist.off.ok = mist.toggle.ok = _coolant_ok +mist.on.bindOk = mist.off.bindOk = mist.toggle.bindOk = _mist_bindOk +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/actions/machine_actions.html b/_modules/qtpyvcp/actions/machine_actions.html new file mode 100644 index 000000000..6dde361ce --- /dev/null +++ b/_modules/qtpyvcp/actions/machine_actions.html @@ -0,0 +1,1342 @@ + + + + + + qtpyvcp.actions.machine_actions — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for qtpyvcp.actions.machine_actions

+import linuxcnc
+from qtpy.QtWidgets import QComboBox
+
+from qtpyvcp.utilities.settings import setting
+
+# Set up logging
+from qtpyvcp.utilities import logger
+from abc import abstractstaticmethod
+LOG = logger.getLogger(__name__)
+
+from qtpyvcp.actions.base_actions import setTaskMode
+from qtpyvcp.plugins import getPlugin
+
+STATUS = getPlugin('status')
+STAT = STATUS.stat
+
+from qtpyvcp.utilities.info import Info
+INFO = Info()
+CMD = linuxcnc.command()
+
+
+# -------------------------------------------------------------------------
+# E-STOP action
+# -------------------------------------------------------------------------
+
[docs]class estop: + """E-Stop action group""" +
[docs] @staticmethod + def activate(): + """Set E-Stop active + + ActionButton syntax:: + + machine.estop.activate + """ + LOG.debug("Setting state red<ESTOP>") + CMD.state(linuxcnc.STATE_ESTOP)
+ +
[docs] @staticmethod + def reset(): + """Resets E-Stop + + ActionButton syntax:: + + machine.estop.reset + """ + LOG.debug("Setting state green<ESTOP_RESET>") + CMD.state(linuxcnc.STATE_ESTOP_RESET)
+ +
[docs] @staticmethod + def toggle(): + """Toggles E-Stop state + + ActionButton syntax:: + + machine.estop.toggle + """ + + if estop.is_activated(): + estop.reset() + else: + estop.activate()
+ +
[docs] @staticmethod + def is_activated(): + """Checks if E_Stop is activated. + + Returns: + bool : True if E-Stop is active, else False. + """ + return bool(STAT.estop)
+ +def _estop_ok(widget=None): + # E-Stop is ALWAYS ok, but provide this method for consistency + _estop_ok.msg = "" + return True + +def _estop_bindOk(widget): + widget.setChecked(STAT.estop != linuxcnc.STATE_ESTOP) + STATUS.estop.onValueChanged(lambda v: widget.setChecked(not v)) + +estop.activate.ok = estop.reset.ok = estop.toggle.ok = _estop_ok +estop.activate.bindOk = estop.reset.bindOk = estop.toggle.bindOk = _estop_bindOk + +# ------------------------------------------------------------------------- +# POWER action +# ------------------------------------------------------------------------- +
[docs]class power: + """Power action group""" +
[docs] @staticmethod + def on(): + """Turns machine power On + + ActionButton syntax:: + + machine.power.on + """ + LOG.debug("Setting state green<ON>") + CMD.state(linuxcnc.STATE_ON)
+ +
[docs] @staticmethod + def off(): + """Turns machine power Off + + ActionButton syntax:: + + machine.power.off + """ + LOG.debug("Setting state red<OFF>") + CMD.state(linuxcnc.STATE_OFF)
+ +
[docs] @staticmethod + def toggle(): + """Toggles machine power On/Off + + ActionButton syntax:: + + machine.power.toggle + """ + if power.is_on(): + power.off() + else: + power.on()
+ +
[docs] @staticmethod + def is_on(): + """Checks if power is on. + + Returns: + bool : True if power is on, else False. + """ + return STAT.task_state == linuxcnc.STATE_ON
+ +def _power_ok(widget=None): + if STAT.task_state == linuxcnc.STATE_ESTOP_RESET: + ok = True + msg = "" + else: + ok = False + msg = "Can't turn machine ON until out of E-Stop" + + _power_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _power_bindOk(widget): + _power_ok(widget) + widget.setChecked(STAT.task_state == linuxcnc.STATE_ON) + STATUS.estop.onValueChanged(lambda: _power_ok(widget)) + STATUS.on.notify(lambda v: widget.setChecked(v)) + +power.on.ok = power.off.ok = power.toggle.ok = _power_ok +power.on.bindOk = power.off.bindOk = power.toggle.bindOk = _power_bindOk + +# ------------------------------------------------------------------------- +# MDI action +# ------------------------------------------------------------------------- + +PREVIOUS_MODE = None + +def _resetMode(interp_state): + global PREVIOUS_MODE + if PREVIOUS_MODE is not None and interp_state == linuxcnc.INTERP_IDLE: + if setTaskMode(PREVIOUS_MODE): + LOG.debug("Successfully reset task_mode after MDI") + PREVIOUS_MODE = None + +STATUS.interp_state.onValueChanged(_resetMode) + +
[docs]def issue_mdi(command, reset=True): + """Issue an MDI command. + + An MDI command can be issued any time the machine is homed (if not + NO_FORCE_HOMING in the INI) and the interpreter is IDLE. The task + mode will automatically be switched to MDI prior to issuing the command + and will be returned to the previous mode when the interpreter becomes IDLE. + + ActionButton syntax to issue G0 X5: + :: + + machine.issue_mdi:G0X5 + + It is simpler to use the MDIButton, examples in the Widgets section. + + Args: + command (str) : A valid RS274 gcode command string. Multiple MDI commands + can be separated with a ``;`` and will be issued sequentially. + reset (bool, optional): Whether to reset the Task Mode to the state + the machine was in prior to issuing the MDI command. + """ + if reset: + # save the previous mode + global PREVIOUS_MODE + PREVIOUS_MODE = STAT.task_mode + # Force `interp_state` update on next status cycle. This is needed because + # some commands might take less than `cycle_time` (50ms) to complete, + # so status would not even notice that the interp_state had changed and the + # reset mode method would not be called. + STATUS.old['interp_state'] = -1 + + if setTaskMode(linuxcnc.MODE_MDI): + # issue multiple MDI commands separated by ';' + for cmd in command.strip().split(';'): + LOG.info("Issuing MDI command: %s", cmd) + CMD.mdi(cmd) + else: + LOG.error("Failed to issue MDI command: {}".format(command))
+ +def _issue_mdi_ok(mdi_cmd='', widget=None): + if STAT.task_state == linuxcnc.STATE_ON \ + and STATUS.allHomed() \ + and STAT.interp_state == linuxcnc.INTERP_IDLE: + + ok = True + msg = "" + + else: + ok = False + msg = "Can't issue MDI unless machine is ON, HOMED and IDLE" + + _issue_mdi_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _issue_mdi_bindOk(mdi_cmd='', widget=None): + _issue_mdi_ok(mdi_cmd=mdi_cmd, widget=widget) + STATUS.task_state.onValueChanged(lambda: _issue_mdi_ok(mdi_cmd=mdi_cmd, widget=widget)) + STATUS.interp_state.onValueChanged(lambda: _issue_mdi_ok(mdi_cmd=mdi_cmd, widget=widget)) + STATUS.homed.onValueChanged(lambda: _issue_mdi_ok(mdi_cmd=mdi_cmd, widget=widget)) + +issue_mdi.ok = _issue_mdi_ok +issue_mdi.bindOk = _issue_mdi_bindOk + +# ------------------------------------------------------------------------- +# WORK COORDINATES action +# ------------------------------------------------------------------------- + +def set_work_coord(coord): + issue_mdi(coord) + +def _set_work_coord_bindOk(coord='', widget=None): + coord = coord.upper() + _issue_mdi_bindOk(coord, widget=widget) + if isinstance(widget, QComboBox): + widget.setCurrentText(coord) + widget.setCurrentText(STATUS.g5x_index.getString()) + STATUS.g5x_index.notify(lambda g5x: widget.setCurrentText(g5x), 'string') + else: + widget.setCheckable(True) + widget.setChecked(STATUS.g5x_index.getString() == coord) + STATUS.g5x_index.notify(lambda g5x: widget.setChecked(g5x == coord), 'string') + +set_work_coord.ok = _issue_mdi_ok +set_work_coord.bindOk = _set_work_coord_bindOk + +# ------------------------------------------------------------------------- +# FEED HOLD action +# ------------------------------------------------------------------------- +
[docs]class feedhold: + """Feed Hold action Group""" + + # FIXME: Not sure what feedhold does, or how to turn it ON/OFF, if it even can be. + +
[docs] @staticmethod + def enable(): + """Enables Feed Hold""" + LOG.info("Setting feedhold ENABLED") + CMD.set_feed_hold(1)
+ +
[docs] @staticmethod + def disable(): + """Disables Feed Hold""" + LOG.info("Setting feedhold DISABLED") + CMD.set_feed_hold(0)
+ +
[docs] @staticmethod + def toggle(): + """Toggles Feed Hold state""" + if STAT.feed_hold_enabled: + feedhold.disable() + else: + feedhold.enable()
+ +def _feed_hold_ok(widget=None): + return STAT.task_state == linuxcnc.STATE_ON and STAT.interp_state == linuxcnc.INTERP_IDLE + +def _feed_hold_bindOk(widget): + widget.setEnabled(STAT.task_state == linuxcnc.STATE_ON) + widget.setChecked(STAT.feed_hold_enabled) + STATUS.task_state.notify(lambda s: widget.setEnabled(s == linuxcnc.STATE_ON)) + STATUS.feed_hold_enabled.notify(widget.setChecked) + +feedhold.enable.ok = feedhold.disable.ok = feedhold.toggle.ok = _feed_hold_ok +feedhold.enable.bindOk = feedhold.disable.bindOk = feedhold.toggle.bindOk = _feed_hold_bindOk + +# ------------------------------------------------------------------------- +# FEED OVERRIDE actions +# ------------------------------------------------------------------------- + +
[docs]class feed_override: + """Feed Override Group""" +
[docs] @staticmethod + def enable(): + """Feed Override Enable""" + CMD.set_feed_override(True)
+ +
[docs] @staticmethod + def disable(): + """Feed Override Disable""" + CMD.set_feed_override(False)
+ +
[docs] @staticmethod + def toggle_enable(): + """Feed Override Enable Toggle""" + if STAT.feed_override_enabled: + feed_override.disable() + else: + feed_override.enable()
+ +
[docs] @staticmethod + def set(value): + """Feed Override Set Value""" + CMD.feedrate(float(value) / 100)
+ +
[docs] @staticmethod + def reset(): + """Feed Override Reset""" + CMD.feedrate(1.0)
+ +def _feed_override_enable_ok(widget=None): + if STAT.task_state == linuxcnc.STATE_ON \ + and STAT.interp_state == linuxcnc.INTERP_IDLE: + ok = True + msg = "" + else: + ok = False + msg = "Machine must be ON and IDLE to enable/disable feed override" + + _feed_override_enable_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _feed_override_enable_bindOk(widget): + STATUS.task_state.onValueChanged(lambda: _feed_override_enable_ok(widget)) + STATUS.interp_state.onValueChanged(lambda: _feed_override_enable_ok(widget)) + STATUS.feed_override_enabled.onValueChanged(widget.setChecked) + +def _feed_override_ok(value=100, widget=None): + if STAT.task_state == linuxcnc.STATE_ON and STAT.feed_override_enabled == 1: + ok = True + msg = "" + elif STAT.task_state != linuxcnc.STATE_ON: + ok = False + msg = "Machine must be ON to set feed override" + elif STAT.feed_override_enabled == 0: + ok = False + msg = "Feed override is not enabled" + else: + ok = False + msg = "Feed override can not be changed" + + _feed_override_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _feed_override_bindOk(value=100, widget=None): + + # This will work for any widget + STATUS.task_state.onValueChanged(lambda: _feed_override_ok(widget=widget)) + STATUS.feed_override_enabled.onValueChanged(lambda: _feed_override_ok(widget=widget)) + + try: + # these will only work for QSlider or QSpinBox + widget.setMinimum(0) + widget.setMaximum(int(INFO.maxFeedOverride() * 100)) + + try: + widget.setSliderPosition(100) + STATUS.feedrate.onValueChanged( + lambda v: widget.setSliderPosition(int(v * 100))) + + except AttributeError: + widget.setValue(100) + STATUS.feedrate.onValueChanged( + lambda v: widget.setValue(int(v * 100))) + + feed_override.set(100) + + except AttributeError: + pass + except: + LOG.exception('Error in feed_override bindOk') + +feed_override.set.ok = feed_override.reset.ok = _feed_override_ok +feed_override.set.bindOk = feed_override.reset.bindOk = _feed_override_bindOk +feed_override.enable.ok = feed_override.disable.ok = feed_override.toggle_enable.ok = _feed_override_enable_ok +feed_override.enable.bindOk = feed_override.disable.bindOk = feed_override.toggle_enable.bindOk = _feed_override_enable_bindOk + +# ------------------------------------------------------------------------- +# RAPID OVERRIDE actions +# ------------------------------------------------------------------------- + +
[docs]class rapid_override: + """Rapid Override Group""" + @staticmethod + def set(value): + CMD.rapidrate(float(value) / 100) + + @staticmethod + def reset(): + CMD.rapidrate(1.0)
+ +def _rapid_override_ok(value=100, widget=None): + if STAT.task_state == linuxcnc.STATE_ON: + ok = True + msg = "" + else: + ok = False + msg = "Machine must be ON to set rapid override" + + _rapid_override_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _rapid_override_bindOk(value=100, widget=None): + + # This will work for any widget + STATUS.task_state.onValueChanged(lambda: _rapid_override_ok(widget=widget)) + + try: + # these will only work for QSlider or QSpinBox + widget.setMinimum(0) + widget.setMaximum(100) + + try: + widget.setSliderPosition(100) + STATUS.rapidrate.onValueChanged( + lambda v: widget.setSliderPosition(int(v * 100))) + + except AttributeError: + STATUS.rapidrate.onValueChanged( + lambda v: widget.setValue(v * 100)) + widget.setValue(100) + + rapid_override.set(100) + + except AttributeError: + pass + except: + LOG.exception('Error in rapid_override bindOk') + +rapid_override.set.ok = rapid_override.reset.ok = _rapid_override_ok +rapid_override.set.bindOk = rapid_override.reset.bindOk = _rapid_override_bindOk + +# ------------------------------------------------------------------------- +# MAX VEL OVERRIDE actions +# ------------------------------------------------------------------------- + +
[docs]class max_velocity: + """Max Velocity Group""" +
[docs] @staticmethod + def set(value): + """Max Velocity Override Set Value""" + CMD.maxvel(float(value) / 60)
+ +
[docs] @staticmethod + def reset(): + """Max Velocity Override Reset Value""" + CMD.maxvel(INFO.maxVelocity() / 60)
+ +def _max_velocity_ok(value=100, widget=None): + if STAT.task_state == linuxcnc.STATE_ON: + ok = True + msg = "" + else: + ok = False + msg = "Machine must be ON to set max velocity" + + _max_velocity_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _max_velocity_bindOk(value=100, widget=None): + + # This will work for any widget + STATUS.task_state.onValueChanged(lambda: _max_velocity_ok(widget=widget)) + + try: + # these will only work for QSlider or QSpinBox + widget.setMinimum(0) + widget.setMaximum(int(INFO.maxVelocity())) + + try: + widget.setSliderPosition(int(INFO.maxVelocity())) + STATUS.max_velocity.onValueChanged( + lambda v: widget.setSliderPosition(int(v * 60))) + + except AttributeError: + widget.setValue(INFO.maxVelocity()) + STATUS.max_velocity.onValueChanged( + lambda v: widget.setValue(v * 60)) + + except AttributeError: + pass + except: + LOG.exception('Error in max_velocity bindOk') + +max_velocity.set.ok = max_velocity.reset.ok = _max_velocity_ok +max_velocity.set.bindOk = max_velocity.reset.bindOk = _max_velocity_bindOk + + +# ------------------------------------------------------------------------- +# set MODE actions +# ------------------------------------------------------------------------- +
[docs]class mode: + """Mode action group""" +
[docs] @staticmethod + def manual(): + """Change mode to Manual + + ActionButton syntax: + :: + + machine.mode.manual + + """ + setTaskMode(linuxcnc.MODE_MANUAL)
+ +
[docs] @staticmethod + def auto(): + """Change mode to Auto + + ActionButton syntax: + :: + + machine.mode.auto + + """ + setTaskMode(linuxcnc.MODE_AUTO)
+ +
[docs] @staticmethod + def mdi(): + """Change mode to MDI + + ActionButton syntax: + :: + + machine.mode.mdi + + """ + setTaskMode(linuxcnc.MODE_MDI)
+ +def _mode_ok(widget=None): + if STAT.task_state == linuxcnc.STATE_ON and STAT.interp_state == linuxcnc.INTERP_IDLE: + ok = True + msg = "" + + else: + ok = False + msg = "Can't set mode when not ON and IDLE" + + _mode_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _manual_bindOk(widget): + widget.setChecked(STAT.task_mode == linuxcnc.MODE_MANUAL) + STATUS.task_state.onValueChanged(lambda: _mode_ok(widget)) + STATUS.interp_state.onValueChanged(lambda: _mode_ok(widget)) + STATUS.task_mode.onValueChanged(lambda m: widget.setChecked(m == linuxcnc.MODE_MANUAL)) + +mode.manual.ok = _mode_ok +mode.manual.bindOk = _manual_bindOk + +def _auto_bindOk(widget): + widget.setChecked(STAT.task_mode == linuxcnc.MODE_AUTO) + STATUS.task_state.onValueChanged(lambda: _mode_ok(widget)) + STATUS.interp_state.onValueChanged(lambda: _mode_ok(widget)) + STATUS.task_mode.onValueChanged(lambda m: widget.setChecked(m == linuxcnc.MODE_AUTO)) + +mode.auto.ok = _mode_ok +mode.auto.bindOk = _auto_bindOk + +def _mdi_bindOk(widget): + widget.setChecked(STAT.task_mode == linuxcnc.MODE_MDI) + STATUS.task_state.onValueChanged(lambda: _mode_ok(widget)) + STATUS.interp_state.onValueChanged(lambda: _mode_ok(widget)) + STATUS.task_mode.onValueChanged(lambda m: widget.setChecked(m == linuxcnc.MODE_MDI)) + +mode.mdi.ok = _mode_ok +mode.mdi.bindOk = _mdi_bindOk + +# ------------------------------------------------------------------------- +# HOME actions +# ------------------------------------------------------------------------- +
[docs]class home: + """Homing actions group""" +
[docs] @staticmethod + def all(): + """Homes all axes + + ActionButton syntax:: + + machine.home.all + """ + LOG.info("Homing all axes") + _home_joint(-1)
+ +
[docs] @staticmethod + def axis(axis): + """Home a specific axis + + Args: + axis (int | str) : one of (xyzabcuvw or 012345678) + + ActionButton syntax to home the X axis:: + + machine.home.axis:x + """ + axis = getAxisLetter(axis) + if axis.lower() == 'all': + home.all() + return + jnum = INFO.COORDINATES.index(axis) + LOG.info('Homing Axis: {}'.format(axis.upper())) + _home_joint(jnum)
+ +
[docs] @staticmethod + def joint(jnum): + """Home a specific joint + + Args: + jnum (int) : one of (012345678) + + ActionButton syntax to home joint 0:: + + machine.home.joint:0 + """ + LOG.info("Homing joint: {}".format(jnum)) + _home_joint(jnum)
+ +def _home_ok(jnum=-1, widget=None): + # TODO: Check if homing a specific joint is OK + if power.is_on(): # and not STAT.homed[jnum]: + ok = True + msg = "" + else: + ok = False + msg = "Machine must be on to home" + + _home_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _home_all_bindOk(widget): + STATUS.on.notify(lambda: _home_ok(widget=widget)) + STATUS.homed.notify(lambda: _home_ok(widget=widget)) + +home.all.ok = _home_ok +home.all.bindOk = _home_all_bindOk + +def _home_joint_bindOk(jnum, widget): + STATUS.on.notify(lambda: _home_ok(jnum, widget=widget)) + STATUS.homed.notify(lambda: _home_ok(jnum, widget=widget)) + +home.joint.ok = _home_ok +home.joint.bindOk = _home_joint_bindOk + +def _home_axis_bindOk(axis, widget): + aletter = getAxisLetter(axis) + if aletter not in INFO.AXIS_LETTER_LIST: + msg = 'Machine has no {} axis'.format(aletter.upper()) + widget.setEnabled(False) + widget.setToolTip(msg) + widget.setStatusTip(msg) + return + + jnum = INFO.AXIS_LETTER_LIST.index(axis) + STATUS.on.notify(lambda: _home_ok(jnum, widget=widget)) + +home.axis.ok = _home_ok +home.axis.bindOk = _home_axis_bindOk + + +
[docs]class unhome: + """Unhoming actions group""" +
[docs] @staticmethod + def all(): + """Unhome all the axes + + ActionButton syntax:: + + machine.unhome.all + """ + LOG.info("Unhoming all Axes") + _unhome_joint(-1)
+ +
[docs] @staticmethod + def axis(axis): + """Unhome a specific axis + + Args: + axis (int | str) : one of (xyzabcuvw or 012345678) + + ActionButton syntax to unhome the X axis:: + + machine.unhome.axis:x + """ + axis = getAxisLetter(axis) + if axis.lower() == 'all': # not sure what this is copied from home + unhome.all() + return + #jnum = INFO.COORDINATES.index(axis) + for ax in INFO.ALETTER_JNUM_DICT: + #LOG.info('Unhoming Axis: {}'.format(axis.upper())) + if axis == ax[0]: + LOG.info('Unhoming Axis: {}'.format(ax.upper())) + _unhome_joint(INFO.ALETTER_JNUM_DICT[ax])
+ +
[docs] @staticmethod + def joint(jnum): + """Unhome a specific joint + + Args: + jnum (int) : The number of the joint to home. + + ActionButton syntax to unhome the joint 0:: + + machine.unhome.joint:0 + """ + LOG.info("Unhoming joint: {}".format(jnum)) + _unhome_joint(jnum)
+ +def _home_joint(jnum): + setTaskMode(linuxcnc.MODE_MANUAL) + CMD.teleop_enable(False) + CMD.home(jnum) + +def _unhome_joint(jnum): + setTaskMode(linuxcnc.MODE_MANUAL) + CMD.teleop_enable(False) + CMD.unhome(jnum) + +# Homing helper functions + +
[docs]def getAxisLetter(axis): + """Takes an axis letter or number and returns the axis letter. + + Args: + axis (int | str) : Either a axis letter or an axis number. + + Returns: + str : The axis letter, `all` for an input of -1. + """ + if isinstance(axis, int): + return ['x', 'y', 'z', 'a', 'b', 'c', 'u', 'v', 'w', 'all'][axis] + return axis.lower()
+ +
[docs]def getAxisNumber(axis): + """Takes an axis letter or number and returns the axis number. + + Args: + axis (int | str) : Either a axis letter or an axis number. + + Returns: + int : The axis number, -1 for an input of `all`. + """ + if isinstance(axis, str): + return ['x', 'y', 'z', 'a', 'b', 'c', 'u', 'v', 'w', 'all'].index(axis.lower()) + return axis
+ + +# ------------------------------------------------------------------------- +# OVERRIDE LIMITS action +# ------------------------------------------------------------------------- +def override_limits(): + LOG.info("Setting override limits") + CMD.override_limits() + +def _override_limits_ok(widget=None): + ok = False + mgs = None + for anum in INFO.AXIS_NUMBER_LIST: + if STAT.limit[anum] != 0: + aaxis = getAxisLetter(anum) + aletter = INFO.COORDINATES.index(aaxis) + ok = True + msg = "Axis {} on limit".format(aletter) + + if not ok: + msg = "Axis must be on limit to override" + + _override_limits_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _override_limits_bindOk(widget): + STATUS.limit.onValueChanged(lambda: _override_limits_ok(widget)) + +override_limits.ok = _override_limits_ok +override_limits.bindOk = _override_limits_bindOk + +# ------------------------------------------------------------------------- +# JOG actions +# ------------------------------------------------------------------------- + +DEFAULT_JOG_SPEED = INFO.getJogVelocity() +MAX_JOG_SPEED = INFO.getMaxJogVelocity() +DEFAULT_JOG_ANGULAR_SPEED = INFO.getJogAngularVelocity() +MAX_JOG_ANGULAR_SPEED = INFO.getMaxJogAngularVelocity() + +@setting('machine.jog.linear-speed', + default_value=DEFAULT_JOG_SPEED, + min_value=0, + max_value=MAX_JOG_SPEED, + persistent=True) +def jog_linear_speed(obj): + return obj.value + +@jog_linear_speed.setter +def jog_linear_speed(obj, value): + + obj.value = obj.clampValue(value) + jog_linear_speed.signal.emit(obj.value) + LOG.debug("Setting jog linear speed: %4.1f", obj.value) + + percentage = int(obj.value * 100 / MAX_JOG_SPEED) + jog_linear_speed_percentage.value = percentage + jog_linear_speed_percentage.signal.emit(percentage) + + +@setting('machine.jog.linear-speed-percentage', + default_value=int(DEFAULT_JOG_SPEED * 100 / MAX_JOG_SPEED), + min_value=0, + max_value=100, + persistent=False) +def jog_linear_speed_percentage(obj): + return obj.value + +@jog_linear_speed_percentage.setter +def jog_linear_speed_percentage(obj, percentage): + LOG.debug("Setting jog linear speed percentage: %d", percentage) + jog_linear_speed.setValue(float(MAX_JOG_SPEED * percentage / 100)) + + +@setting('machine.jog.angular-speed', + default_value=DEFAULT_JOG_ANGULAR_SPEED, + min_value=0, + max_value=MAX_JOG_ANGULAR_SPEED, + persistent=True) +def jog_angular_speed(obj): + return obj.value + +@jog_angular_speed.setter +def jog_angular_speed(obj, value): + obj.value = obj.clampValue(value) + jog_angular_speed.signal.emit(obj.value) + LOG.debug("Setting Jog Angular Speed: %d", value) + + percentage = int(obj.value * 100 / MAX_JOG_ANGULAR_SPEED) + jog_angular_speed_percentage.value = percentage + jog_angular_speed_percentage.signal.emit(percentage) + +@setting('machine.jog.angular-speed-percentage', + default_value=int(DEFAULT_JOG_ANGULAR_SPEED * 100 / MAX_JOG_ANGULAR_SPEED), + min_value=0, + max_value=100, + persistent=False) +def jog_angular_speed_percentage(obj): + return obj.value + +@jog_angular_speed_percentage.setter +def jog_angular_speed_percentage(obj, percentage): + LOG.debug("Setting jog angular speed percentage: %d", percentage) + jog_angular_speed.setValue(float(MAX_JOG_ANGULAR_SPEED * percentage / 100)) + + +@setting('machine.jog.mode-incremental', default_value=True) +def jog_mode_incremental(obj): + return obj.value + +@jog_mode_incremental.setter +def jog_mode_incremental(obj, value): + LOG.debug("Setting jog mode incremental: %s", value) + obj.value = value + + +def fromInternalLinearUnits(v, unit=None): + if unit is None: + unit = STAT.linear_units + lu = (unit or 1) * 25.4 + return v * lu + +def parseJogIncrement(jogincr): + scale = 1 + if isinstance(jogincr, str): + jogincr = jogincr.lower() + if jogincr.endswith("mm"): + scale = fromInternalLinearUnits(1 / 25.4) + elif jogincr.endswith("cm"): + scale = fromInternalLinearUnits(10 / 25.4) + elif jogincr.endswith("um"): + scale = fromInternalLinearUnits(.001 / 25.4) + elif jogincr.endswith("in") or jogincr.endswith("inch") or jogincr.endswith('"'): + scale = fromInternalLinearUnits(1.) + elif jogincr.endswith("mil"): + scale = fromInternalLinearUnits(.001) + else: + scale = 1 + jogincr = jogincr.rstrip(" inchmuil") + try: + if "/" in jogincr: + p, q = jogincr.split("/") + jogincr = float(p) / float(q) + else: + jogincr = float(jogincr) + except ValueError: + jogincr = 0 + return jogincr * scale + +@setting('machine.jog.increment', parseJogIncrement(INFO.getIncrements()[0])) +def jog_increment(obj): + """Linear jog increment. + + Args: + jogincr (str, int, float) : The desired jog increment. Can be passed + as a string including an optional units specifier. Valid unit + specifiers include ``mm``, ``cm``, ``um``, ``in``, ``inch``, ``"``, + and ``mil``. + """ + return obj.value + +@jog_increment.setter +def jog_increment(obj, jogincr): + value = parseJogIncrement(jogincr) + LOG.debug('Setting jog increment: %s (%2.4f)', jogincr, value) + obj.value = value + obj.signal.emit(value) + + +
[docs]class jog: + """Jog Actions Group""" + + max_linear_speed = INFO.getMaxJogVelocity() + max_angular_speed = INFO.getMaxJogAngularVelocity() + angular_speed = INFO.getJogAngularVelocity() + continuous = False + increment = INFO.getIncrements()[0] + +
[docs] @staticmethod + def axis(axis, direction=0, speed=None, distance=None): + """Jog an axis. + + Action Button Syntax to jog the X axis in the positive direction:: + + machine.jog.axis:x,pos + + Args: + axis (str | int) : Either the letter or number of the axis to jog. + direction (str | int) : pos or +1 for positive, neg or -1 for negative. + speed (float, optional) : Desired jog vel in machine_units/s. + distance (float, optional) : Desired jog distance, continuous if 0.00. + """ + + # check if it even makes sense to try to jog + if STAT.task_state != linuxcnc.STATE_ON or STAT.task_mode != linuxcnc.MODE_MANUAL: + return + + if isinstance(direction, str): + direction = {'neg': -1, 'pos': 1}.get(direction.lower(), 0) + + axis = getAxisNumber(axis) + + # must be in teleoperating mode to jog. + if STAT.motion_mode != linuxcnc.TRAJ_MODE_TELEOP: + CMD.teleop_enable(1) + + if speed == 0 or direction == 0: + CMD.jog(linuxcnc.JOG_STOP, 0, axis) + + else: + + if speed is None: + if axis in (3, 4, 5): + speed = jog_angular_speed.value / 60.0 + else: + speed = jog_linear_speed.value / 60.0 + + if distance is None: + if jog_mode_incremental.value: + distance = jog_increment.value + else: + distance = 0 + + velocity = float(speed) * direction + + if distance == 0: + CMD.jog(linuxcnc.JOG_CONTINUOUS, 0, axis, velocity) + else: + CMD.jog(linuxcnc.JOG_INCREMENT, 0, axis, velocity, distance)
+ +
[docs] @staticmethod + def set_jog_continuous(continuous): + """Set Jog Continuous""" + if continuous: + LOG.debug("Setting jog mode to continuous") + else: + LOG.debug("Setting jog mode to incremental") + jog.jog_mode_incremental = continuous
+ +
[docs] @staticmethod + def set_increment(raw_increment): + """Set Jog Increment""" + jog_increment.setValue(raw_increment)
+ +
[docs] @staticmethod + def set_linear_speed(speed): + """Set Jog Linear Speed + + ActionSlider syntax:: + + machine.jog.set-linear-speed + """ + jog_linear_speed.setValue(float(speed))
+ +
[docs] @staticmethod + def set_angular_speed(speed): + """Set Jog Angular Speed""" + jog_angular_speed.setValue(float(speed))
+ +
[docs] @staticmethod + def set_linear_speed_percentage(percentage): + """Set Jog Linear Speed Percentage""" + jog_linear_speed_percentage.setValue(percentage)
+ +
[docs] @staticmethod + def set_angular_speed_percentage(percentage): + """Set Jog Angular Speed Percentage""" + jog_angular_speed_percentage.setValue(percentage)
+ +def _jog_speed_slider_bindOk(widget): + + try: + # these will only work for QSlider or QSpinBox + widget.setMinimum(0) + widget.setMaximum(100) + widget.setValue((jog.linear_speed.getValue() / jog.max_linear_speed) * 100) + + # jog.linear_speed.connect(lambda v: widget.setValue(v * 100)) + except AttributeError: + pass + +def _jog_angular_speed_slider_bindOk(widget): + + try: + # these will only work for QSlider or QSpinBox + widget.setMinimum(0) + widget.setMaximum(100) + widget.setValue((jog.angular_speed.getValue() / jog.max_angular_speed) * 100) + + # jog.linear_speed.connect(lambda v: widget.setValue(v * 100)) + except AttributeError: + pass + + +jog.set_linear_speed.ok = jog.set_angular_speed.ok = lambda *a, **k: True +jog.set_linear_speed.bindOk = jog.set_angular_speed.bindOk = lambda *a, **k: True + +jog.set_linear_speed_percentage.ok = lambda *a, **k: True +jog.set_linear_speed_percentage.bindOk = _jog_speed_slider_bindOk + +jog.set_angular_speed_percentage.ok = lambda *a, **k: True +jog.set_angular_speed_percentage.bindOk = _jog_angular_speed_slider_bindOk + + +def _jog_axis_ok(axis, direction=0, widget=None): + axisnum = getAxisNumber(axis) + jnum = INFO.COORDINATES.index(axis) + if STAT.task_state == linuxcnc.STATE_ON \ + and STAT.interp_state == linuxcnc.INTERP_IDLE \ + and (STAT.limit[axisnum] == 0 or STAT.joint[jnum]['override_limits']): + # and STAT.homed[axisnum] == 1 \ + ok = True + msg = "" + else: + ok = False + msg = "Machine must be ON and in IDLE to jog" + + _jog_axis_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + + +def _jog_axis_bindOk(axis, direction, widget): + aletter = getAxisLetter(axis) + if aletter not in INFO.AXIS_LETTER_LIST: + msg = 'Machine has no {} axis'.format(aletter.upper()) + widget.setEnabled(False) + widget.setToolTip(msg) + widget.setStatusTip(msg) + return + + STATUS.limit.onValueChanged(lambda: _jog_axis_ok(aletter, direction, widget)) + STATUS.homed.onValueChanged(lambda: _jog_axis_ok(aletter, direction, widget)) + STATUS.task_state.onValueChanged(lambda: _jog_axis_ok(aletter, direction, widget)) + STATUS.interp_state.onValueChanged(lambda: _jog_axis_ok(aletter, direction, widget)) + +jog.axis.ok = _jog_axis_ok +jog.axis.bindOk = _jog_axis_bindOk + + +
[docs]class jog_mode: + """Jog Mode Group""" + +
[docs] @staticmethod + def continuous(): + """Set Jog Continuous + + ActionButton syntax:: + + machine.jog-mode.continuous + """ + jog_mode_incremental.setValue(False)
+ + +
[docs] @staticmethod + def incremental(): + """Set Jog Incremental + + ActionButton syntax:: + + machine.jog-mode.incremental + """ + jog_mode_incremental.setValue(True)
+ +
[docs] @staticmethod + def toggle(): + """Jog Mode Toggle + + ActionButton syntax:: + + machine.jog-mode.toggle + """ + jog_mode_incremental.setValue(not jog_mode_incremental.value)
+ +jog_mode.incremental.ok = jog_mode.continuous.ok = lambda *a, **kw: True +jog_mode.incremental.bindOk = jog_mode.continuous.bindOk = lambda *a, **kw: True +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/actions/program_actions.html b/_modules/qtpyvcp/actions/program_actions.html new file mode 100644 index 000000000..a0a7e023e --- /dev/null +++ b/_modules/qtpyvcp/actions/program_actions.html @@ -0,0 +1,741 @@ + + + + + + qtpyvcp.actions.program_actions — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for qtpyvcp.actions.program_actions

+import sys
+import linuxcnc
+import tempfile
+
+from qtpy.QtCore import Qt, QTimer
+# Set up logging
+from qtpyvcp.utilities import logger
+LOG = logger.getLogger(__name__)
+
+from qtpyvcp.utilities.info import Info
+from qtpyvcp.plugins import getPlugin
+
+STATUS = getPlugin('status')
+STAT = STATUS.stat
+INFO = Info()
+CMD = linuxcnc.command()
+
+from qtpyvcp.actions.base_actions import setTaskMode
+
+
+#==============================================================================
+# Program actions
+#==============================================================================
+
+def load(fname, add_to_recents=True, isreload=False):
+    if not fname:
+        # load a blank file. Maybe should load [DISPLAY] OPEN_FILE
+        clear()
+
+    #setTaskMode(linuxcnc.MODE_AUTO)
+    if not isreload:
+        STATUS.addLock()
+    
+    filter_prog = INFO.getFilterProgram(fname)
+    if not filter_prog:
+        LOG.debug('Loading NC program: %s', fname)
+        CMD.program_open(fname.encode('utf-8'))
+        CMD.wait_complete()
+    else:
+        LOG.debug('Loading file with filter program: %s', fname)
+        openFilterProgram(fname, filter_prog)
+
+    if add_to_recents:
+        addToRecents(fname)
+    
+    QTimer.singleShot(2000, STATUS.removeLock)
+
+load.ok = lambda *args, **kwargs: True
+load.bindOk = lambda *args, **kwargs: True
+
+
[docs]def reload(): + """Reload the currently loaded NC program + + ActionButton syntax:: + + program.reload + """ + stat = linuxcnc.stat() + stat.poll() + fname = stat.file + if os.path.exists(fname): + load(stat.file, add_to_recents=False, isreload=True)
+ +reload.ok = lambda *args, **kwargs: True +reload.bindOk = lambda *args, **kwargs: True + +
[docs]def clear(): + """Clear the loaded NC program + + ActionButton syntax:: + + program.clear + """ + + _, blankfile = tempfile.mkstemp(prefix="new_program_", suffix=".ngc") + with open(blankfile, 'w') as fp: + fp.write("(New Program)\n\n\nM30") + load(blankfile, add_to_recents=False)
+ +clear.ok = lambda *args, **kwargs: True +clear.bindOk = lambda *args, **kwargs: True + +def addToRecents(fname): + files = STATUS.recent_files.getValue() + if fname in files: + files.remove(fname) + files.insert(0, fname) + STATUS.recent_files.setValue(files[:STATUS.max_recent_files]) + +# ------------------------------------------------------------------------- +# program RUN action +# ------------------------------------------------------------------------- +
[docs]def run(start_line=0): + """Runs the loaded program, optionally starting from a specific line. + + ActionButton syntax:: + + program.run + program.run:line + + Args: + start_line (int, optional) : The line to start program from. Defaults to 0. + """ + if STAT.state == linuxcnc.RCS_EXEC and STAT.paused: + CMD.auto(linuxcnc.AUTO_RESUME) + elif setTaskMode(linuxcnc.MODE_AUTO): + CMD.auto(linuxcnc.AUTO_RUN, start_line)
+ +def _run_ok(widget=None): + """Checks if it is OK to run a program. + + Args: + widget (QWidget, optional) : If a widget is supplied it will be + enabled/disabled according to the result, and will have it's + statusTip property set to the reason the action is disabled. + + Returns: + bool : True if Ok, else False. + """ + if STAT.estop: + ok = False + msg = "Can't run program when in E-Stop" + elif not STAT.enabled: + ok = False + msg = "Can't run program when not enabled" + elif not STATUS.allHomed(): + ok = False + msg = "Can't run program when not homed" + elif not STAT.paused and not STAT.interp_state == linuxcnc.INTERP_IDLE: + ok = False + msg = "Can't run program when already running" + elif STAT.file == "": + ok = False + msg = "Can't run program when no file loaded" + else: + ok = True + msg = "Run program" + + _run_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _run_bindOk(widget): + STATUS.estop.onValueChanged(lambda: _run_ok(widget)) + STATUS.enabled.onValueChanged(lambda: _run_ok(widget)) + STATUS.all_axes_homed.onValueChanged(lambda: _run_ok(widget)) + STATUS.interp_state.onValueChanged(lambda: _run_ok(widget)) + STATUS.file.onValueChanged(lambda: _run_ok(widget)) + +run.ok = _run_ok +run.bindOk = _run_bindOk + +# ------------------------------------------------------------------------- +# program RUN from LINE action +# ------------------------------------------------------------------------- +def run_from_line(line=None): + # TODO: This might should show a popup to select start line, + # or it could get the start line from the gcode view or + # even from the backplot. + LOG.error('Run from line not implemented yet.') + + +run_from_line.ok = _run_ok +run_from_line.bindOk = _run_bindOk + +# ------------------------------------------------------------------------- +# program STEP action +# ------------------------------------------------------------------------- +
[docs]def step(): + """Steps program line by line + + ActionButton syntax:: + + program.step + + """ + if STAT.state == linuxcnc.RCS_EXEC and STAT.paused: + CMD.auto(linuxcnc.AUTO_STEP) + elif setTaskMode(linuxcnc.MODE_AUTO): + CMD.auto(linuxcnc.AUTO_STEP)
+ +step.ok = _run_ok +step.bindOk = _run_bindOk + +# ------------------------------------------------------------------------- +# program PAUSE action +# ------------------------------------------------------------------------- +
[docs]def pause(): + """Pause executing program + + + ActionButton syntax:: + + program.pause + + """ + LOG.debug("Pausing program execution") + CMD.auto(linuxcnc.AUTO_PAUSE)
+ +def _pause_ok(widget=None): + """Checks if it is OK to pause the program. + + Args: + widget (QWidget, optional) : If a widget is supplied it will be + enabled/disabled according to the result, and will have it's + statusTip property set to the reason the action is disabled. + + Returns: + bool : True if Ok, else False. + """ + if STAT.state == linuxcnc.RCS_EXEC and not STAT.paused: + msg = "Pause program execution" + ok = True + elif STAT.paused: + msg = "Program is already paused" + ok = False + else: + msg = "No program running to pause" + ok = False + + _pause_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _pause_bindOk(widget): + STATUS.state.onValueChanged(lambda: _pause_ok(widget)) + STATUS.paused.onValueChanged(lambda: _pause_ok(widget)) + +pause.ok = _pause_ok +pause.bindOk = _pause_bindOk + +# ------------------------------------------------------------------------- +# program RESUME action +# ------------------------------------------------------------------------- +
[docs]def resume(): + """Resume a previously paused program + + ActionButton syntax:: + + program.resume + + """ + LOG.debug("Resuming program execution") + CMD.auto(linuxcnc.AUTO_RESUME)
+ +def _resume_ok(widget): + """Checks if it is OK to resume a paused program. + + Args: + widget (QWidget, optional) : If a widget is supplied it will be + enabled/disabled according to the result, and will have it's + statusTip property set to the reason the action is disabled. + + Returns: + bool : True if Ok, else False. + """ + if STAT.state == linuxcnc.RCS_EXEC and STAT.paused: + ok = True + msg = "Resume program execution" + else: + ok = False + msg = "No paused program to resume" + + _resume_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _resume_bindOk(widget): + STATUS.paused.onValueChanged(lambda: _resume_ok(widget)) + STATUS.state.onValueChanged(lambda: _resume_ok(widget)) + +resume.ok = _resume_ok +resume.bindOk = _resume_bindOk + +# ------------------------------------------------------------------------- +# program ABORT action +# ------------------------------------------------------------------------- +
[docs]def abort(): + """Aborts any currently executing program, MDI command or homing operation. + + ActionButton syntax:: + + program.abort + + """ + + LOG.debug("Aborting program") + CMD.abort()
+ +def _abort_ok(widget=None): + """Checks if it is OK to abort current operation. + + Args: + widget (QWidget, optional) : If a widget is supplied it will be + enabled/disabled according to the result, and will have it's + statusTip property set to the reason the action is disabled. + + Returns: + bool : True if Ok, else False. + """ + if STAT.state == linuxcnc.RCS_EXEC or STAT.state == linuxcnc.RCS_ERROR: + ok = True + msg = "" + else: + ok = False + msg = "Nothing to abort" + + _abort_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _abort_bindOk(widget): + STATUS.state.onValueChanged(lambda: _abort_ok(widget)) + +abort.ok = _abort_ok +abort.bindOk = _abort_bindOk + +# ------------------------------------------------------------------------- +# BLOCK DELETE actions +# ------------------------------------------------------------------------- +
[docs]class block_delete: + """Block Delete Group""" +
[docs] @staticmethod + def on(): + """Start ignoring lines beginning with '/'. + + ActionButton syntax:: + + program.block-delete.on + + """ + + LOG.debug("Setting block delete green<ON>") + CMD.set_block_delete(True)
+ +
[docs] @staticmethod + def off(): + """Stop ignoring lines beginning with '/'. + + ActionButton syntax:: + + program.block-delete.off + + """ + + LOG.debug("Setting block delete red<OFF>") + CMD.set_block_delete(False)
+ +
[docs] @staticmethod + def toggle(): + """Toggle ignoring lines beginning with '/'. + + ActionButton syntax:: + + program.block-delete.toggle + + """ + + if STAT.block_delete == True: + block_delete.off() + else: + block_delete.on()
+ +def _block_delete_ok(widget=None): + """Checks if it is OK to set block_delete. + + Args: + widget (QWidget, optional) : If a widget is supplied it will be + enabled/disabled according to the result, and will have it's + statusTip property set to the reason the action is disabled. + + Returns: + bool : True if Ok, else False. + """ + if STAT.task_state == linuxcnc.STATE_ON: + ok = True + msg = "" + else: + ok = False + msg = "Machine must be ON to set Block Del" + + _block_delete_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _block_delete_bindOk(widget): + widget.setChecked(STAT.block_delete) + STATUS.task_state.onValueChanged(lambda: _block_delete_ok(widget)) + STATUS.block_delete.onValueChanged(lambda s: widget.setChecked(s)) + +block_delete.on.ok = block_delete.off.ok = block_delete.toggle.ok = _block_delete_ok +block_delete.on.bindOk = block_delete.off.bindOk = block_delete.toggle.bindOk = _block_delete_bindOk + +# ------------------------------------------------------------------------- +# OPTIONAL STOP actions +# ------------------------------------------------------------------------- +
[docs]class optional_stop: + """Optional Stop Group""" +
[docs] @staticmethod + def on(): + """Pause when a line beginning with M1 is encountered + + ActionButton syntax:: + + program.optional-stop.on + + """ + + LOG.debug("Setting optional stop green<ON>") + CMD.set_optional_stop(True)
+ +
[docs] @staticmethod + def off(): + """Don't pause when a line beginning with M1 is encountered + + ActionButton syntax:: + + program.option-stop.off + + """ + + LOG.debug("Setting optional stop red<OFF>") + CMD.set_optional_stop(False)
+ +
[docs] @staticmethod + def toggle(): + """Toggle pause when a line beginning with M1 is encountered + + ActionButton syntax:: + + program.optional-stop.toggle + + """ + + if STAT.optional_stop == True: + optional_stop.off() + else: + optional_stop.on()
+ +def _optional_stop_ok(widget=None): + """Checks if it is OK to set optional_stop. + + Args: + widget (QWidget, optional) : If a widget is supplied it will be + enabled/disabled according to the result, and will have it's + statusTip property set to the reason the action is disabled. + + Returns: + bool : True if Ok, else False. + """ + if STAT.task_state == linuxcnc.STATE_ON: + ok = True + msg = "" + else: + ok = False + msg = "Machine must be ON to set Opt Stop" + + _optional_stop_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _optional_stop_bindOk(widget): + widget.setChecked(STAT.block_delete) + STATUS.task_state.onValueChanged(lambda: _optional_stop_ok(widget)) + STATUS.optional_stop.onValueChanged(lambda s: widget.setChecked(s)) + +optional_stop.on.ok = optional_stop.off.ok = optional_stop.toggle.ok = _optional_stop_ok +optional_stop.on.bindOk = optional_stop.off.bindOk = optional_stop.toggle.bindOk = _optional_stop_bindOk + +optional_skip = block_delete + +#============================================================================== +# Program preprocessing handlers +#============================================================================== +import os, sys, time, select, re +import tempfile, atexit, shutil + +FILTER_TEMP = None + +def openFilterProgram(infile, prog_name): + temp_dir = _mktemp() + outfile = os.path.join(temp_dir, os.path.basename(infile)) + #FilterProgram(prog_name, infile, outfile, lambda r: r or _loadFilterResult(outfile)) + FilterProgram(prog_name, infile, outfile, None) + CMD.program_open(outfile) + LOG.debug('Linuxcnc Command - program_open') + +def _loadFilterResult(fname): + if fname: + CMD.program_open(fname) + +def _mktemp(): + global FILTER_TEMP + if FILTER_TEMP is not None: + return FILTER_TEMP + FILTER_TEMP = tempfile.mkdtemp(prefix='emcflt-', suffix='.d') + atexit.register(lambda: shutil.rmtree(FILTER_TEMP)) + return FILTER_TEMP + +# slightly reworked code from gladevcp +# loads a filter program and collects the result +progress_re = re.compile("^FILTER_PROGRESS=(\\d*)$") +class FilterProgram: + def __init__(self, prog_name, infile, outfile, callback=None): + import subprocess + outfile = open(outfile, "w") + infile = infile.replace("'", "'\\''") + + env = dict(os.environ) + env['AXIS_PROGRESS_BAR'] = '1' + self.p = subprocess.run(["sh", "-c", "%s '%s'" % (prog_name, infile)], + stdin=subprocess.PIPE, + stdout=outfile, + stderr=subprocess.PIPE, + env=env, + text=True) + + self.stderr_text = [] + self.program_filter = prog_name + self.callback = callback + #self.gid = STATUS.onValueChanged('periodic', self.update) + #progress = Progress(1, 100) + #progress.set_text(_("Filtering...")) + # force file load until know what to do about update/finish + + def update(self, w): + if self.p.poll() is not None: + self.finish() + STATUS.disconnect(self.gid) + return False + + r, w, x = select.select([self.p.stderr], [], [], 0) + if not r: + return True + stderr_line = self.p.stderr.readline() + m = progress_re.match(stderr_line) + if m: + pass #progress.update(int(m.group(1)), 1) + else: + self.stderr_text.append(stderr_line) + sys.stderr.write(stderr_line) + return True + + def finish(self): + # .. might be something left on stderr + for line in self.p.stderr: + m = progress_re.match(line) + if not m: + self.stderr_text.append(line) + sys.stderr.write(line) + r = self.p.returncode + if r: + self.error(r, "".join(self.stderr_text)) + if self.callback: + self.callback(r) + + def error(self, exitcode, stderr): + LOG.error("Error loading filter program!") + # dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, + # _("The program %(program)r exited with code %(code)d. " + # "Any error messages it produced are shown below:") + # % {'program': self.program_filter, 'code': exitcode}) + # diaLOG.format_secondary_text(stderr) + # diaLOG.run() + # diaLOG.destroy() +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/actions/spindle_actions.html b/_modules/qtpyvcp/actions/spindle_actions.html new file mode 100644 index 000000000..64091dbaa --- /dev/null +++ b/_modules/qtpyvcp/actions/spindle_actions.html @@ -0,0 +1,636 @@ + + + + + + qtpyvcp.actions.spindle_actions — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for qtpyvcp.actions.spindle_actions

+import linuxcnc
+import json
+# Set up logging
+from qtpyvcp.utilities import logger
+LOG = logger.getLogger(__name__)
+
+from qtpyvcp.utilities.info import Info
+from qtpyvcp.plugins import getPlugin
+
+STATUS = getPlugin('status')
+STAT = STATUS.stat
+INFO = Info()
+
+SPINDLES = list(range(INFO.spindles()))
+DEFAULT_SPEED = INFO.defaultSpindleSpeed()
+
+CMD = linuxcnc.command()
+
+from qtpyvcp.actions.base_actions import setTaskMode
+
+
+def _spindle_exists(spindle):
+    if spindle in SPINDLES:
+        return True
+    return False
+
+def _spindle_ok(speed=None, spindle=0, widget=None):
+    msg = None
+    if spindle not in SPINDLES:
+        ok = False
+        msg = "No spindle No. {}".format(spindle)
+    elif STAT.task_state != linuxcnc.STATE_ON:
+        ok = False
+        msg = "Power must be ON"
+    elif STAT.task_mode == linuxcnc.MODE_AUTO:
+        ok = False
+        msg = "Mode must be MAN or MDI"
+    elif len(widget.rules) > 2:  # check if widget has enable rules
+        # print(widget)
+        rule_list = json.loads(widget.rules)
+        for rule in rule_list:
+            # print(rule)
+            if rule.get("property") == "Enable":
+
+                channels = rule.get("channels")
+                expression = channels[0].get("url")
+
+                plugin_name, data_channel_name = expression.split(':')
+
+                channel_name = None
+
+                if '?' in data_channel_name:
+                    data_channel, channel_name = data_channel_name.split('?')
+                else:
+                    data_channel = data_channel_name
+
+                plugin = getPlugin(plugin_name)
+
+                if hasattr(plugin, data_channel):
+                    function = getattr(plugin, data_channel)
+
+                    ch = list()
+
+                    if channel_name is not None:
+                        ch.append(function(channel_name))
+                    else:
+                        ch.append(function())
+
+                    ok = eval(rule.get("expression"))
+                else:
+                    ok = True
+                    msh = ""
+
+            else:
+                ok = True
+                msh = ""
+    else:
+        ok = True
+        msg = ""
+
+    _spindle_ok.msg = msg
+
+    if widget is not None:
+        widget.setEnabled(ok)
+        widget.setStatusTip(msg)
+        widget.setToolTip(msg)
+
+    return ok
+
+def _spindle_bindOk(speed=None, spindle=0, widget=None):
+    if not _spindle_exists(spindle):
+        return
+    STATUS.on.notify(lambda: _spindle_ok(spindle=spindle, widget=widget))
+    STATUS.task_mode.onValueChanged(lambda: _spindle_ok(spindle=spindle, widget=widget))
+
+
+
[docs]def forward(speed=None, spindle=0): + """Turn a spindle ON in the *FORWARD* direction. + + ActionButton syntax to start spindle 0 CW (spindle 0 is default) + :: + + spindle.forward + + ActionButton syntax to start spindle 1 CW + :: + + spindle.1.forward + + + ActionButton syntax to start spindle 0 CW at 1800 RPM + :: + + spindle.forward:1800 + + Args: + speed (int, optional) : The requested speed to spin the spindle at. + If ``speed`` is not specified the current interpreter speed setting + (as set by the last S word) is used, taking into account the + value of the spindle override if it is enabled. + spindle (int, optional) : The number of the spindle to turn ON. If + ``spindle`` is not specified spindle 0 is assumed. + """ + + if speed is None: + speed = getSpeed() + CMD.spindle(linuxcnc.SPINDLE_FORWARD, speed, spindle)
+ +def _spindle_forward_bindOk(speed=None, spindle=0, widget=None): + if not _spindle_exists(spindle): + return + widget.setCheckable(True) + STATUS.on.notify(lambda: _spindle_ok(spindle=spindle, widget=widget)) + STATUS.task_mode.onValueChanged(lambda: _spindle_ok(spindle=spindle, widget=widget)) + STATUS.spindle[spindle].direction.onValueChanged(lambda d: widget.setChecked(d == 1)) + +forward.ok = _spindle_ok +forward.bindOk = _spindle_forward_bindOk + + +
[docs]def reverse(speed=None, spindle=0): + """Turn a spindle ON in the *REVERSE* direction. + + ActionButton syntax to start spindle 0 CCW (spindle 0 is default) + :: + + spindle.reverse + + ActionButton syntax to start spindle 1 CCW + :: + + spindle.1.reverse + + + ActionButton syntax to start spindle 0 CCW at 1800 RPM + :: + + spindle.reverse:1800 + + Args: + speed (float, optional) : The requested speed to spin the spindle at. + If ``speed`` is not specified the current interpreter speed setting + (as set by the last S word) is used, taking into account the + value of the spindle override if it is enabled. + spindle (int, optional) : The number of the spindle to turn ON. If + ``spindle`` is not specified spindle 0 is assumed. + """ + if speed is None: + speed = getSpeed() + CMD.spindle(linuxcnc.SPINDLE_REVERSE, speed, spindle)
+ +def _spindle_reverse_bindOk(speed=None, spindle=0, widget=None): + if not _spindle_exists(spindle): + return + widget.setCheckable(True) + STATUS.on.notify(lambda: _spindle_ok(spindle=spindle, widget=widget)) + STATUS.task_mode.onValueChanged(lambda: _spindle_ok(spindle=spindle, widget=widget)) + STATUS.spindle[spindle].direction.onValueChanged(lambda d: widget.setChecked(d == -1)) + +reverse.ok = _spindle_ok +reverse.bindOk = _spindle_reverse_bindOk + + +
[docs]def off(spindle=0): + """Turn a spindle OFF. + + ActionButton syntax to stop spindle 0 (spindle 0 is default) + :: + + spindle.off + + ActionButton syntax to stop spindle 1 + :: + + spindle.1.off + + Args: + spindle (int, optional) : The number of the spindle to turn OFF. If + ``spindle`` is not specified spindle 0 is assumed. + """ + CMD.spindle(linuxcnc.SPINDLE_OFF, spindle)
+ +off.ok = _spindle_ok +off.bindOk = _spindle_bindOk + + +
[docs]def faster(spindle=0): + """Increase spindle speed by 100rpm. + + ActionButton syntax to increase spindle 0 speed (spindle 0 is default) + :: + + spindle.faster + + ActionButton syntax to increase spindle 1 + :: + + spindle.1.faster + + Args: + spindle (int, optional) : The number of the spindle to increase the + speed of. If ``spindle`` is not specified spindle 0 is assumed. + """ + CMD.spindle(linuxcnc.SPINDLE_INCREASE, spindle)
+ +faster.ok = _spindle_ok +faster.bindOk = _spindle_bindOk + + +
[docs]def slower(spindle=0): + """Decrease spindle speed by 100rpm. + + ActionButton syntax to decrease spindle 0 speed (spindle 0 is default) + :: + + spindle.slower + + ActionButton syntax to decrease spindle 1 speed + :: + + spindle.1.slower + + + Args: + spindle (int, optional) : The number of the spindle to decrease the + speed of. If ``spindle`` is not specified spindle 0 is assumed. + """ + CMD.spindle(linuxcnc.SPINDLE_DECREASE, spindle)
+ +slower.ok = _spindle_ok +slower.bindOk = _spindle_bindOk + + +# Spindle CONSTANT +
[docs]def constant(spindle=0): + """Unclear""" + CMD.spindle(linuxcnc.SPINDLE_CONSTANT)
+ +constant.ok = _spindle_ok +constant.bindOk = _spindle_bindOk + + +
[docs]def getSpeed(spindle=0): + """Gets the interpreter's speed setting for the specified spindle. + + Args: + spindle (int, optional) : The number of the spindle to get the speed + of. If ``spindle`` is not specified spindle 0 is assumed. + + Returns: + float: The interpreter speed setting, with any override applied if + override enabled. + """ + raw_speed = STAT.settings[2] + if raw_speed == 0: + raw_speed = abs(DEFAULT_SPEED) + + return raw_speed
+ +#============================================================================== +# Spindle Override +#============================================================================== + +
[docs]def override(override, spindle=0): + """Set spindle override percentage. + + Args: + override (float) : The desired spindle override in percent. + spindle (float, optional) : The number of the spindle to apply the + override to. If ``spindle`` is not specified spindle 0 is assumed. + """ + CMD.spindleoverride(float(override) / 100, spindle)
+ +def _or_reset(spindle=0): + CMD.spindleoverride(1.0, spindle) + +def _or_enable(spindle=0): + CMD.set_spindle_override(1, spindle) + +def _or_disable(spindle=0): + CMD.set_spindle_override(0, spindle) + +def _or_toggle_enable(spindle=0): + if STAT.spindle[spindle]['override_enabled']: + override.disable() + else: + override.enable() + +override.reset = _or_reset +override.enable = _or_enable +override.disable = _or_disable +override.toggle_enable = _or_toggle_enable + +def _or_ok(value=100, spindle=0, widget=None): + if spindle not in SPINDLES: + ok = False + msg = "No spindle No. {}".format(spindle) + elif STAT.task_state != linuxcnc.STATE_ON: + ok = False + msg = "Machine must be ON" + elif STAT.spindle[0]['override_enabled'] != 1: + ok = False + msg = "Override not enabled" + else: + ok = True + msg = "" + + _or_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _or_bindOk(value=100, spindle=0, widget=None): + if not _spindle_exists(spindle): + return + + # This will work for any widget + STATUS.task_state.onValueChanged(lambda: _or_ok(widget=widget)) + STATUS.spindle[spindle].override_enabled.onValueChanged(lambda: _or_ok(widget=widget)) + + try: + # these will only work for QSlider or QSpinBox + widget.setMinimum(int(INFO.minSpindleOverride() * 100)) + widget.setMaximum(int(INFO.maxSpindleOverride() * 100)) + + try: + widget.setSliderPosition(100) + STATUS.spindle[spindle].override.onValueChanged( + lambda v: widget.setSliderPosition(int(v * 100))) + + except AttributeError: + widget.setValue(100) + STATUS.spindle[spindle].override.onValueChanged( + lambda v: widget.setValue(int(v * 100))) + + override(100) + + except AttributeError: + pass + except: + LOG.exception('Error in spindle.override bindOk') + +override.ok = override.reset.ok = _or_ok +override.bindOk = override.reset.bindOk = _or_bindOk + +def _or_enable_ok(spindle=0, widget=None): + if spindle not in SPINDLES: + ok = False + msg = "No spindle No. {}".format(spindle) + elif STAT.task_state != linuxcnc.STATE_ON: + ok = False + msg = "Machine must be ON" + elif STAT.interp_state != linuxcnc.INTERP_IDLE: + ok = False + msg = "Machine must be IDLE" + else: + ok = True + msg = "" + + _or_enable_ok.msg = msg + + if widget is not None: + widget.setEnabled(ok) + widget.setStatusTip(msg) + widget.setToolTip(msg) + + return ok + +def _or_enable_bindOk(spindle=0, widget=None): + if not _spindle_exists(spindle): + return + + STATUS.task_state.onValueChanged(lambda: _or_enable_ok(spindle, widget)) + STATUS.interp_state.onValueChanged(lambda: _or_enable_ok(spindle, widget)) + STATUS.spindle[spindle].override_enabled.onValueChanged(widget.setChecked) + +override.enable.ok = override.disable.ok = override.toggle_enable.ok = _or_enable_ok +override.enable.bindOk = override.disable.bindOk = override.toggle_enable.bindOk = _or_enable_bindOk + +#============================================================================== +# Spindle Brake +#============================================================================== + +
[docs]class brake: + """Spindle brake Group""" +
[docs] @staticmethod + def on(spindle=0): + """Set spindle brake ON. + + ActionButton syntax to engage spindle 0 brake (spindle 0 is default) + :: + + spindle.brake.on + + ActionButton syntax to engage spindle 1 brake + :: + + spindle.1.brake.on + + Args: + spindle (int, optional) : The number of the spindle to apply the + override to. If ``spindle`` is not specified spindle 0 is assumed. + """ + CMD.brake(linuxcnc.BRAKE_ENGAGE, spindle)
+ +
[docs] @staticmethod + def off(spindle=0): + """Set spindle brake OFF. + + ActionButton syntax to disengage spindle 0 brake (spindle 0 is default) + :: + + spindle.brake.off + + ActionButton syntax to disengage spindle 1 brake + :: + + spindle.1.brake.off + + + Args: + spindle (int, optional) : The number of the spindle to apply the + override to. If ``spindle`` is not specified spindle 0 is assumed. + """ + CMD.brake(linuxcnc.BRAKE_RELEASE, spindle)
+ +
[docs] @staticmethod + def toggle(spindle=0): + """Toggle spindle brake ON/OFF. + + ActionButton syntax to toggle spindle 0 brake (spindle 0 is default) + :: + + spindle.brake.toggle + + ActionButton syntax to toggle spindle 1 brake + :: + + spindle.1.brake.toggle + + Args: + spindle (int, optional) : The number of the spindle to apply the + override to. If ``spindle`` is not specified spindle 0 is assumed. + """ + if brake.is_on(): + brake.off() + else: + brake.on()
+ + @staticmethod + def is_on(spindle=0): + return STAT.spindle[spindle]['brake'] == linuxcnc.BRAKE_ENGAGE
+ +def _brake_is_on(spindle=0): + return STAT.spindle[spindle]['brake'] == linuxcnc.BRAKE_ENGAGE + +def _brake_bind_ok(spindle=0, widget=None): + if not _spindle_exists(spindle): + return + + STATUS.on.notify(lambda: _spindle_ok(spindle=spindle, widget=widget)) + STATUS.task_mode.onValueChanged(lambda: _spindle_ok(spindle=spindle, widget=widget)) + STATUS.spindle[spindle].brake.onValueChanged(widget.setChecked) + +brake.on.ok = brake.off.ok = brake.toggle.ok = _spindle_ok +brake.on.bindOk = brake.off.bindOk = brake.toggle.bindOk = _brake_bind_ok +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/actions/tool_actions.html b/_modules/qtpyvcp/actions/tool_actions.html new file mode 100644 index 000000000..67192ae9f --- /dev/null +++ b/_modules/qtpyvcp/actions/tool_actions.html @@ -0,0 +1,242 @@ + + + + + + qtpyvcp.actions.tool_actions — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for qtpyvcp.actions.tool_actions

+import os
+import hal
+
+INI_FILE = os.getenv('INI_FILE_NAME')
+TCLPATH = os.getenv('LINUXCNC_TCL_DIR', '/usr/lib/tcltk/linuxcnc')
+
+"""Tool Actions launch LinuxCNC tools"""
+
+
[docs]def halshow(): + """Launch HALShow utility to view HAL and a Watch Window + + * Components + * Pins + * Parameters + * Signals + * Functions + * Threads + + ActionButton syntax:: + + tool_actions.halshow + + """ + p = os.popen("tclsh {0}/bin/halshow.tcl &".format(TCLPATH))
+ +
[docs]def calibration(): + """Launch the HAL PID calibration utility. + + Test PID, Scale, Acceleration and Velocity settings if they are in the INI + file. + + ActionButton syntax:: + + tool_actions.calibration + + """ + p = os.popen("tclsh {0}/bin/emccalib.tcl -ini {1} > /dev/null &" + .format(TCLPATH, INI_FILE), "w")
+ +
[docs]def halmeter(): + """Launch the HALMeter utility to display the current value of a single pin. + + ActionButton syntax:: + + tool_actions.halmeter + + """ + p = os.popen("halmeter &")
+ +
[docs]def status(): + """Launch the LinuxCNC status monitor utility. + + ActionButton syntax:: + + tool_actions.status + + """ + p = os.popen("linuxcnctop > /dev/null &", "w")
+ +
[docs]def halscope(): + """Launch the HALScope utility. + + Halscope is an oscilloscope for the HAL + + ActionButton syntax:: + + tool_actions.halscope + + """ + p = os.popen("halscope > /dev/null &", "w")
+ +
[docs]def classicladder(): + """Launch the ClassicLadder editor. + + Todo: + classicladder.bindOk should check for classicladder comp + """ + if hal.component_exists("classicladder_rt"): + p = os.popen("classicladder &", "w") + else: + text = "Classicladder real-time component not detected" + print(text)
+ # self.error_dialog.run(text) + +
[docs]def simulate_probe(): + """Launch Simulate Probe + + ActionButton syntax:: + + tool_actions.simulate_probe + + """ + p = os.popen("simulate_probe > /dev/null &", "w")
+ +halshow.ok = calibration.ok = halmeter.ok = status.ok = halscope.ok = classicladder.ok = simulate_probe.ok = lambda widget: True +halshow.bindOk = calibration.bindOk = halmeter.bindOk = status.bindOk = halscope.bindOk = classicladder.bindOk = simulate_probe.bindOk = lambda widget: None +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/app/application.html b/_modules/qtpyvcp/app/application.html new file mode 100644 index 000000000..7f80715ef --- /dev/null +++ b/_modules/qtpyvcp/app/application.html @@ -0,0 +1,405 @@ + + + + + + qtpyvcp.app.application — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for qtpyvcp.app.application

+"""Main QtPyVCP Application Module
+
+Contains the VCPApplication class with core function and VCP loading logic.
+"""
+import os
+import sys
+import imp
+import inspect
+from pkg_resources import iter_entry_points
+
+from qtpy import API
+from qtpy.QtGui import QFontDatabase
+from qtpy.QtCore import QTimer, Slot, Qt
+from qtpy.QtWidgets import QApplication, QStyleFactory
+
+import qtpyvcp
+
+from qtpyvcp.utilities.logger import initBaseLogger
+from qtpyvcp.plugins import initialisePlugins, terminatePlugins, getPlugin
+from qtpyvcp.widgets.base_widgets.base_widget import VCPPrimitiveWidget
+from qtpyvcp.widgets.form_widgets.main_window import VCPMainWindow
+
+# initialize logging. If a base logger was already initialized in a startup
+# script (e.g. vcp_launcher.py), then that logger will be returned, otherwise
+# this will initialise a base logger with default log level of DEBUG
+LOG = initBaseLogger('qtpyvcp')
+
+# Needed to silence this PySide2 warning:
+#    Qt WebEngine seems to be initialized from a plugin. Please set
+#    Qt::AA_ShareOpenGLContexts using QCoreApplication::setAttribute
+#    before constructing QGuiApplication.
+if API == 'pyside2':
+    from qtpy.QtCore import Qt
+    QApplication.setAttribute(Qt.AA_ShareOpenGLContexts)
+
+
+
[docs]class VCPApplication(QApplication): + + def __init__(self, theme=None, stylesheet=None, custom_fonts=[]): + app_args = (qtpyvcp.OPTIONS.command_line_args or "").split() + super(VCPApplication, self).__init__(app_args) + + opts = qtpyvcp.OPTIONS + + self.status = getPlugin('status') + + # initialize plugins + initialisePlugins() + + theme = opts.theme or theme + if theme is not None: + self.setStyle(QStyleFactory.create(theme)) + + stylesheet = opts.stylesheet or stylesheet + if stylesheet is not None: + self.loadStylesheet(stylesheet, opts.develop) + + if custom_fonts: + if isinstance(custom_fonts, str): # single font or location + self.loadCustomFont(custom_fonts) + else: # list of fonts or locations + for font in custom_fonts: + self.loadCustomFont(font) + + # self.window = self.loadVCPMainWindow(opts, vcp_file) + # if self.window is not None: + # self.window.show() + + if opts.hide_cursor: + from qtpy.QtGui import QCursor + self.setOverrideCursor(QCursor(Qt.BlankCursor)) + + # Performance monitoring + if opts.perfmon: + import psutil + self.perf = psutil.Process() + self.perf_timer = QTimer() + self.perf_timer.setInterval(2000) + self.perf_timer.timeout.connect(self.logPerformance) + self.perf_timer.start() + + self.aboutToQuit.connect(self.terminate) + +
[docs] def loadVCPMainWindow(self, opts, vcp_file=None): + """ + Loads a VCPMainWindow instance defined by a Qt .ui file, a Python .py + file, or from a VCP python package. + + Parameters + ---------- + vcp_file : str + The path or name of the VCP to load. + opts : OptDict + A OptDict of options to pass to the VCPMainWindow subclass. + + Returns + ------- + VCPMainWindow instance + """ + vcp = opts.vcp or vcp_file + if vcp is None: + return + + if os.path.exists(vcp): + + vcp_path = os.path.realpath(vcp) + if os.path.isfile(vcp_path): + directory, filename = os.path.split(vcp_path) + name, ext = os.path.splitext(filename) + if ext == '.ui': + LOG.info("Loading VCP from UI file: yellow<{}>".format(vcp)) + return VCPMainWindow(opts=opts, ui_file=vcp_path) + elif ext == '.py': + LOG.info("Loading VCP from PY file: yellow<{}>".format(vcp)) + return self.loadPyFile(vcp_path, opts) + elif os.path.isdir(vcp_path): + LOG.info("VCP is a directory") + # TODO: Load from a directory if it has a __main__.py entry point + else: + try: + entry_points = {} + for entry_point in iter_entry_points(group='qtpyvcp.example_vcp'): + entry_points[entry_point.name] = entry_point + for entry_point in iter_entry_points(group='qtpyvcp.vcp'): + entry_points[entry_point.name] = entry_point + window = entry_points[vcp.lower()].load() + return window(opts=opts) + except: + LOG.exception("Failed to load entry point") + + LOG.critical("VCP could not be loaded: yellow<{}>".format(vcp)) + sys.exit()
+ +
[docs] def loadPyFile(self, pyfile, opts): + """ + Load a .py file, performs some sanity checks to try and determine + if the file actually contains a valid VCPMainWindow subclass, and if + the checks pass, create and return an instance. + + This is an internal method, users will usually want to use `loadVCP` instead. + + Parameters + ---------- + pyfile : str + The path to a .py file to load. + opts : OptDict + A OptDict of options to pass to the VCPMainWindow subclass. + + Returns + ------- + VCPMainWindow instance + """ + # Add the pyfile module directory to the python path, so that submodules can be loaded + module_dir = os.path.dirname(os.path.abspath(pyfile)) + sys.path.append(module_dir) + + # Load the module. It's attributes can be accessed via `python_vcp.attr` + module = imp.load_source('python_vcp', pyfile) + + classes = [obj for name, obj in inspect.getmembers(module) + if inspect.isclass(obj) + and issubclass(obj, VCPMainWindow) + and obj != VCPMainWindow] + if len(classes) == 0: + raise ValueError("Invalid File Format." + " {} has no class inheriting from VCPMainWindow.".format(pyfile)) + if len(classes) > 1: + LOG.warn("More than one VCPMainWindow class in file yellow<{}>." + " The first occurrence (in alphabetical order) will be used: {}" + .format(pyfile, classes[0].__name__)) + cls = classes[0] + + # initialize and return the VCPMainWindow subclass + return cls(opts=opts)
+ +
[docs] def loadStylesheet(self, stylesheet, watch=False): + """Loads a QSS stylesheet file containing styles to be applied + to specific Qt and/or QtPyVCP widget classes. + + Args: + stylesheet (str) : Path to the .qss stylesheet to load. + watch (bool) : Whether to watch and re-load on .qss file changes. + """ + + def load(path): + LOG.info("Loading global stylesheet: yellow<{}>".format(stylesheet)) + self.setStyleSheet("file:///" + path) + + if watch: + from qtpy.QtCore import QFileSystemWatcher + self.qss_file_watcher = QFileSystemWatcher() + self.qss_file_watcher.addPath(stylesheet) + self.qss_file_watcher.fileChanged.connect(load) + + load(stylesheet)
+ +
[docs] def loadCustomFont(self, font): + """Loads custom front from a file or directory.""" + + if os.path.isfile(font) and os.path.splitext(font)[1] in ['.ttf', '.otf', '.woff', '.woff2']: + self.addApplicationFont(font) + elif os.path.isdir(font): + for ffile in os.listdir(font): + fpath = os.path.join(font, ffile) + self.loadCustomFont(fpath)
+ +
[docs] def addApplicationFont(self, font_path): + """Loads a font file into the font database. The path can specify the + location of a font file or a qresource.""" + LOG.debug("Loading custom font: %s" % font_path) + res = QFontDatabase.addApplicationFont(font_path) + # per QT docs -1 is error and 0+ is index to font loaded for later use + if res < 0: + LOG.error("Failed to load font: %s", font_path)
+ +
[docs] def getWidget(self, name): + """Searches for a widget by name in the application windows. + + Args: + name (str) : ObjectName of the widget. + + Returns: QWidget + """ + for win_name, obj in list(qtpyvcp.WINDOWS.items()): + if hasattr(obj, name): + return getattr(obj, name) + + raise AttributeError("Could not find widget with name: %s" % name)
+ +
[docs] @Slot() + def logPerformance(self): + """Logs total CPU usage (in percent), as well as per-thread usage. + """ + with self.perf.oneshot(): + total_percent = self.perf.cpu_percent(interval=None) + total_time = sum(self.perf.cpu_times()) + usage = ["{:.3f}".format(total_percent * ((t.system_time + t.user_time) / total_time)) for t in self.perf.threads()] + + LOG.info("Performance:\n" + " Total CPU usage (%): {}\n" + " Per Thread: {}\n" + .format(total_percent, ' '.join(usage)))
+ + def terminate(self): + self.terminateWidgets() + terminatePlugins() + + def initialiseWidgets(self): + for w in self.allWidgets(): + if isinstance(w, VCPPrimitiveWidget): + w.initialize() + + def terminateWidgets(self): + LOG.debug("Terminating widgets") + for w in self.allWidgets(): + if isinstance(w, VCPPrimitiveWidget): + try: + w.terminate() + except Exception: + LOG.exception('Error terminating %s widget', w)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/hal.html b/_modules/qtpyvcp/hal.html new file mode 100644 index 000000000..fff56273b --- /dev/null +++ b/_modules/qtpyvcp/hal.html @@ -0,0 +1,236 @@ + + + + + + qtpyvcp.hal — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for qtpyvcp.hal

+"""
+HAL Interface
+-------------
+
+This module allows the creation of userspace HAL components in Python.
+This includes pins and parameters of the various HAL types.
+
+Example:
+
+.. code-block:: python
+
+    from qtpyvcp import hal
+
+    # create a new component and add some pins
+    comp = hal.getComponent("loop-back")
+    comp.addPin("in", "float", "in")
+    comp.addPin("out", "float", "out")
+
+    # mark the component as 'ready'
+    comp.ready()
+
+    # define a function to call when the input pin changes
+    def onInChanged(new_value):
+        # loop the out pin to the in pin value
+        comp.getPin('out').value = new_value
+
+    # connect the listener to the input pin
+    comp.addListener('in', onInChanged)
+
+"""
+
+from qtpyvcp.utilities.logger import getLogger
+
+from .hal_qlib import QComponent, QPin
+
+COMPONENTS = {}
+LOG = getLogger(__name__)
+
+
+
[docs]def component(name): + """Initializes a new HAL component and registers it.""" + comp = QComponent(name) + COMPONENTS[name] = comp + return comp
+ + +
[docs]def getComponent(name='qtpyvcp'): + """Get HAL component. + + Args: + name (str) : The name of the component to get. Defaults to `qtpyvcp`. + + Returns: + QComponent : An existing or new HAL component. + """ + + try: + comp = COMPONENTS[name] + LOG.debug("Using existing HAL component: %s", name) + except KeyError: + LOG.info("Creating new HAL component: %s", name) + comp = component(name) + + return comp
+ + +if __name__ == "__main__": + + from qtpy.QtWidgets import QApplication + from qtpyvcp import hal + + app = QApplication([]) + + # create a new component and add some pins + comp = hal.getComponent("loop-back") + comp.addPin("in", "float", "in") + comp.addPin("out", "float", "out") + + # mark the component as 'ready' + comp.ready() + + # define a function to call when the input pin changes + def onInChanged(new_value): + print(("loop-back.in pin changed:", new_value)) + # loop the out pin to the in pin value + comp.getPin('out').value = new_value + + # connect the listener to the input pin + comp.addListener('in', onInChanged) + + app.exec_() +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/plugins.html b/_modules/qtpyvcp/plugins.html new file mode 100644 index 000000000..e78a8f16e --- /dev/null +++ b/_modules/qtpyvcp/plugins.html @@ -0,0 +1,293 @@ + + + + + + qtpyvcp.plugins — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for qtpyvcp.plugins

+"""
+QtPyVCP Plugins
+---------------
+
+These package level functions provide methods for registering and initializing
+plugins, as well as retrieving them for use and terminating them in the proper
+order.
+"""
+import importlib
+
+from collections import OrderedDict
+
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.plugins.base_plugins import Plugin, DataPlugin, DataChannel
+
+LOG = getLogger(__name__)
+
+_PLUGINS = OrderedDict()  # Ordered dict so we can initialize/terminate in order
+
+
+
[docs]def registerPlugin(plugin_id, plugin_inst): + """Register a Plugin instance. + + Args: + plugin_id (str) : The unique name to register the plugin under. + plugin_inst(plugin_inst) : The plugin instance to register. + """ + + if plugin_id in _PLUGINS: + LOG.warning("Replacing {} with {} for use with '{}' plugin" + .format(_PLUGINS[plugin_id].__class__, plugin_inst.__class__, plugin_id)) + + _PLUGINS[plugin_id] = plugin_inst
+ + +
[docs]def registerPluginFromClass(plugin_id, plugin_cls, args=[], kwargs={}): + """Register a plugin from a class. + + This is primarily used for registering plugins defined in the YAML config. + + .. code-block:: yaml + + data_plugins: + my_plugin: + provider: my_package.my_module:MyPluginClass + args: + - 10 + - False + kwargs: + my_number: 75 + my_string: A string argument + + Args: + plugin_id (str) : A unique name to register the plugin under. + plugin_cls (class, str) : A :py:class:`.Plugin` subclass, or a fully + qualified class spec of format ``package.module:Class`` specifying + the location of an importable :py:class:`.Plugin` subclass. + args (list) : Arguments to pass to the plugin's __init__ method. + kwargs (dict) : Keyword argument to pass to the plugin's __init__ method. + + Returns: + The plugin instance + """ + + if isinstance(plugin_cls, str): + LOG.debug("Loading plugin '{}' from '{}'".format(plugin_id, plugin_cls)) + + modname, sep, clsname = plugin_cls.partition(':') + + try: + plugin_cls = getattr(importlib.import_module(modname), clsname) + except Exception: + LOG.critical("Failed to import data plugin.") + raise + + assert issubclass(plugin_cls, Plugin), "Not a valid plugin, must be a qtpyvcp.plugins.Plugin subclass." + + try: + inst = plugin_cls(*args, **kwargs) + registerPlugin(plugin_id, inst) + return inst + except TypeError: + LOG.critical("Error initializing plugin: {}(*{}, **{})".format(plugin_cls, args, kwargs)) + raise
+ + +
[docs]def getPlugin(plugin_id): + """Get plugin instance from ID. + + Args: + plugin_id (str) : The ID of the plugin to retrieve. + + Returns: + A plugin instance, or None. + """ + try: + return _PLUGINS[plugin_id] + except KeyError: + LOG.error("Failed to find plugin with ID '%s'", plugin_id) + return None
+ + +def iterPlugins(): + """Returns an iterator for the plugins dict.""" + return iter(_PLUGINS.items()) + + +
[docs]def initialisePlugins(): + """Initializes all registered plugins. + + Plugins are initialized in the order they were registered in. + Plugins defined in the YAML file are registered in the order they + were defined. + """ + for plugin_id, plugin_inst in list(_PLUGINS.items()): + LOG.debug("Initializing '%s' plugin", plugin_id) + plugin_inst.initialise()
+ + +
[docs]def postGuiInitialisePlugins(main_window): + """Initializes all registered plugins after main window is shown. + + Plugins are initialized in the order they were registered in. + Plugins defined in the YAML file are registered in the order they + were defined. + """ + for plugin_id, plugin_inst in list(_PLUGINS.items()): + LOG.debug("Post GUI Initializing '%s' plugin", plugin_id) + plugin_inst.postGuiInitialise(main_window)
+ + +
[docs]def terminatePlugins(): + """Terminates all registered plugins. + + Plugins are terminated in the reverse order they were registered in. + If an error is encountered while terminating a plugin it will be ignored + and the remaining plugins will still be terminated. + """ + # terminate in reverse order, this is to prevent problems + # when terminating plugins that make use of other plugins. + for plugin_id, plugin_inst in reversed(list(_PLUGINS.items())): + LOG.debug("Terminating '%s' plugin", plugin_id) + try: + # try so that other plugins are terminated properly + # even if one of them fails. + plugin_inst.terminate() + except Exception: + LOG.exception("Error terminating '%s' plugin", plugin_id)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/plugins/base_plugins.html b/_modules/qtpyvcp/plugins/base_plugins.html new file mode 100644 index 000000000..3a1daeb49 --- /dev/null +++ b/_modules/qtpyvcp/plugins/base_plugins.html @@ -0,0 +1,353 @@ + + + + + + qtpyvcp.plugins.base_plugins — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for qtpyvcp.plugins.base_plugins

+import inspect
+
+from qtpy.QtCore import QObject, Signal
+from qtpyvcp.utilities.logger import getLogger, logLevelFromName
+
+LOG = getLogger(__name__)
+
+
+
[docs]class Plugin(QObject): + """QtPyVCP Plugin base class.""" + def __init__(self): + super(Plugin, self).__init__() + + self._log = None + self._initialized = False + self._postGuiInitialized = False + + @property + def log(self): + if self._log is None: + self._log = getLogger(self.__module__) + return self._log + return self._log + +
[docs] def initialise(self): + """Initialize the plugin. + + This method is called after the main event loop has started. Any timers + or threads used by the plugin should be started here. + + This method should set ``self._initialized`` to true if successfully. + """ + self._initialized = True
+ +
[docs] def postGuiInitialise(self, main_window): + """Initialize the plugin after mainwindow is shown. + + This method is called after the main window is shown. + Args: + main_window (VCPMainWindow) : the VCPMainWindow object. + + This method should set ``self._postGuiInitialized`` to true if successfully. + """ + self._postGuiInitialized = True
+ +
[docs] def terminate(self): + """Terminate the plugin. + + This is called right before the main event loop exits. Any cleanup + of the plugin should be done here, such as saving persistent data. + """ + pass
+ + +def isDataChan(obj): + return isinstance(obj, DataChannel) + + +
[docs]class DataPlugin(Plugin): + """DataPlugin.""" + + def __init__(self): + super(DataPlugin, self).__init__() + + self.channels = {name: obj for name, obj in + inspect.getmembers(self, isDataChan)} + +
[docs] def getChannel(self, url): + """Get data channel from URL. + + Args: + url (str) : The URL of the channel to get. + + Returns: + tuple : (chan_obj, chan_exp) + """ + + chan, sep, query = url.partition('?') + raw_args = query.split('&') + + # print(url, chan, raw_args) + + args = [] + kwargs = {} + for arg in [a for a in raw_args if a != '']: + if '=' in arg: + key, val = arg.split('=') + kwargs[key] = val + else: + args.append(arg) + + # print(chan, args, kwargs) + + try: + chan_obj = self.channels[chan] + if len(args) > 0 and args[0] in ('string', 'text', 'str'): + chan_exp = lambda: chan_obj.getString(*args[1:], **kwargs) + else: + chan_exp = lambda: chan_obj.getValue(*args, **kwargs) + + except (KeyError, SyntaxError): + return None, None + + return chan_obj, chan_exp
+ +
[docs] def setLogLevel(self, level): + """Set plugin log level. + + Args: + level (str, int) : Log level (DEBUG | INFO | ERROR | CRITICAL) + """ + if level: + if isinstance(level, str): + level = logLevelFromName(level) + self.log.setLevel(level)
+ + +
[docs]class DataChannel(QObject): + + signal = Signal(object) + + def __init__(self, fget=None, fset=None, fstr=None, data=None, settable=False, + doc = None): + super(DataChannel, self).__init__() + + self.fget = fget + self.fset = fset + self.fstr = fstr + + self.value = data + + self.settable = settable + self.instance = None + + if doc is None and fget is not None: + doc = fget.__doc__ + self.__doc__ = doc + +
[docs] def getValue(self, *args, **kwargs): + """Channel data getter method.""" + if self.fget is None: + return self.value + return self.fget(self.instance, self, *args, **kwargs)
+ +
[docs] def getString(self, *args, **kwargs): + """Channel data getter method.""" + if self.fstr is None: + return str(self.value) + return self.fstr(self.instance, self, *args, **kwargs)
+ +
[docs] def setValue(self, value): + """Channel data setter method.""" + if self.fset is None: + self.value = value + self.signal.emit(value) + else: + self.fset(self.instance, self, value)
+ + def getter(self, fget): + def inner(*args, **kwargs): + fget(*args, **kwargs) + + self.fget = inner + return self + + def setter(self, fset): + def inner(*args, **kwargs): + fset(*args, **kwargs) + + self.fset = inner + return self + + def tostring(self, fstr): + def inner(*args, **kwargs): + return fstr(*args, **kwargs) + + self.fstr = inner + return self + + def notify(self, slot, *args, **kwargs): + # print('Connecting %s to slot %s' % (self._signal, slot)) + if len(args) == 0 and len(kwargs) == 0: + self.signal.connect(slot) + else: + if args[0] in ['string', 'str']: + self.signal.connect(lambda: slot(self.getString(*args[1:], **kwargs))) + else: + self.signal.connect(lambda: slot(self.getValue(*args, **kwargs))) + + # fixme + onValueChanged = notify + + def __get__(self, instance, owner): + self.instance = instance + return self + + def __call__(self, *args, **kwargs): + return self.getValue(*args, **kwargs) + + def __set__(self, instance, value): + return self.setValue(value) + + def __getitem__(self, item): + return self.value[item] + + def __str__(self): + return self.getString()
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/plugins/clock.html b/_modules/qtpyvcp/plugins/clock.html new file mode 100644 index 000000000..c94680d78 --- /dev/null +++ b/_modules/qtpyvcp/plugins/clock.html @@ -0,0 +1,254 @@ + + + + + + qtpyvcp.plugins.clock — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for qtpyvcp.plugins.clock

+"""
+DateTime
+--------
+
+This plugin provides the Date and Time
+
+This plugin is not loaded by default, so to use it you will first
+need to add it to your VCPs YAML config file.
+
+YAML configuration:
+
+.. code-block:: yaml
+
+    data_plugins:
+      clock:
+        provider: qtpyvcp.plugins.clock:Clock
+
+"""
+
+from datetime import datetime
+
+from qtpy.QtCore import QTimer
+from qtpyvcp.plugins import DataPlugin, DataChannel
+
+
+
[docs]class Clock(DataPlugin): + """Clock Plugin""" + def __init__(self): + super(Clock, self).__init__() + + # set initial values + self.time.setValue(datetime.now()) + self.date.setValue(datetime.now()) + + # make the clock tick + self.timer = QTimer() + self.timer.timeout.connect(self.tick) + + @DataChannel + def time(self, chan): + """The current time, updated every second. + + Args: + format (str) : Format spec. Defaults to ``%I:%M:%S %p``. + See http://strftime.org for supported formats. + + Returns: + The current time as a formatted string. Default HH:MM:SS AM + + Channel syntax:: + + clock:time + clock:time?string + clock:time?string&format=%S + + """ + return chan.value + + @time.tostring + def time(self, chan, format="%I:%M:%S %p"): + return chan.value.strftime(format) + + @DataChannel + def date(self, chan): + """The current date, updated every second. + + Args: + format (str) : Format spec. Defaults to ``%m/%d/%Y``. + See http://strftime.org for supported formats. + + Returns: + The current date as a formatted string. Default MM/DD/YYYY + + Channel syntax:: + + clock:date + clock:date?string + clock:date?string&format=%Y + + """ + return chan.value + + @date.tostring + def date(self, chan, format="%m/%d/%Y"): + return chan.value.strftime(format) + +
[docs] def initialise(self): + self.timer.start(1000)
+ + def tick(self): + self.time.setValue(datetime.now()) + self.date.setValue(datetime.now())
+ + +if __name__ == "__main__": + from qtpy.QtWidgets import QApplication + app = QApplication([]) + + c = Clock() + c.initialise() + + def onTimeChanged(val): + print((c.time)) + print((c.date)) + + c.time.notify(onTimeChanged) + + app.exec_() +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/plugins/notifications.html b/_modules/qtpyvcp/plugins/notifications.html new file mode 100644 index 000000000..35e5dd235 --- /dev/null +++ b/_modules/qtpyvcp/plugins/notifications.html @@ -0,0 +1,347 @@ + + + + + + qtpyvcp.plugins.notifications — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for qtpyvcp.plugins.notifications

+"""
+Notifications Plugin
+--------------------
+
+Plugin to handle error and status notifications.
+
+Supports evaluating arbitrary python expressions placed in gcode DEBUG statements.
+
+
+    ::
+
+        example.ngc
+        #1 = 12.345
+
+        ; this will set my_label's text to the value of variable #1
+        (DEBUG, EVAL[vcp.getWidget{"my_label"}.setText{'%4.2f' % #1}])
+
+        ; this will change the text color of my_label to red
+        (DEBUG, EVAL[vcp.getWidget{"my_label"}.setStyleSheet{"color: red"}])
+"""
+
+import time
+import linuxcnc
+
+from qtpy.QtWidgets import QApplication
+
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.plugins import DataPlugin, DataChannel, getPlugin
+from qtpyvcp.lib.native_notification import NativeNotification
+from qtpyvcp.lib.dbus_notification import DBusNotification
+
+LOG = getLogger(__name__)
+STATUS = getPlugin('status')
+
+
+
[docs]class Notifications(DataPlugin): + """ + Notification data plugin + + Args: + enabled (bool, optional): Enable or disable notification popups (Default = True) + mode (str, optional): native or dbus (Default = 'native') + max_messages (int, optional) Max number of notification popups to show. + persistent (bool, optional): Save notifications on shutdown (Default = True) + """ + def __init__(self, enabled=True, mode="native", max_messages=5, + persistent=True, **kwargs): + super(Notifications, self).__init__() + + self.enabled = enabled + self.mode = mode + self.max_messages = max_messages + + self.error_channel = linuxcnc.error_channel() + + self.messages = [] + self.notification_dispatcher = None + + self.persistent = persistent + + self.data_manager = getPlugin('persistent_data_manager') + + @DataChannel + def debug_message(self, chan): + """Debug messages from LinuxCNC. + """ + return chan.value or '' + + @DataChannel + def info_message(self, chan): + """Gives messages from GCode. + Syntax: (MSG, ...) + (DEBUG, ...) + See http://linuxcnc.org/docs/html/gcode/overview.html#gcode:messages + for more information. + """ + return chan.value or '' + + @DataChannel + def warn_message(self, chan): + """Warning messages from LinuxCNC. + """ + return chan.value or '' + + @DataChannel + def error_message(self, chan): + """Error messages from LinuxCNC. + """ + return chan.value or '' + + def captureMessage(self, m_type, msg): + + if self.enabled: + self.notification_dispatcher.setNotify(m_type, msg) + + self.messages.append({'timestamp': time.time(), + 'message_type': m_type, + 'message_text': msg, + 'operator_id': '', + 'loaded_file': STATUS.file.getValue(), + 'task_mode': STATUS.task_mode.getString(), + 'task_state': STATUS.task_state.getString(), + 'interp_mode': STATUS.interp_state.getString(), + } + ) + +
[docs] def timerEvent(self, event): + """Called every 200ms to poll error channel""" + error = self.error_channel.poll() + + if not error: + return + + kind, msg_text = error + msg_text = msg_text.strip() + + message_words = msg_text.split(' ') + + index = 1 + max_words = 5 + tmp_message = list() + + for word in message_words: + tmp_message.append(word) + if index == max_words: + tmp_message.append('\n') + index = 1 + else: + index += 1 + + msg = ' '.join(tmp_message) + + if msg == "" or msg is None: + msg = "No message text set." + + if kind in [linuxcnc.NML_ERROR, linuxcnc.OPERATOR_ERROR]: + self.error_message.setValue(msg) + self.captureMessage('error', msg) + LOG.error(msg) + + elif kind in [linuxcnc.NML_TEXT, linuxcnc.OPERATOR_TEXT]: + self.debug_message.setValue(msg) + self.captureMessage('debug', msg) + LOG.debug(msg) + + elif kind in [linuxcnc.NML_DISPLAY, linuxcnc.OPERATOR_DISPLAY]: + + if msg_text.lower().startswith('eval['): + exp = msg_text[5:].strip(']') + exp = exp.replace('{', '(').replace('}', ')') + + LOG.debug("Evaluating gcode DEBUG expression: '%s'", exp) + + try: + app = QApplication.instance() + eval(exp, {"vcp": app}) + except Exception: + LOG.exception("Error evaluating DEBUG expression: '%s'", exp) + + else: + self.info_message.setValue(msg) + self.captureMessage('info', msg) + LOG.info(msg) + + else: + self.info_message.setValue(msg) + self.captureMessage('info', msg) + LOG.error(msg)
+ +
[docs] def initialise(self): + + if self.persistent: + self.messages = self.data_manager.getData('messages', []) + + # Enable notifications before there is a main window, captureMessage wins postGuiInitialise. + # Initalice later with a main window set as parent in postGuiInitilise ( FIXME ) + + if self.enabled: + if self.mode == "native": + self.notification_dispatcher = NativeNotification() + self.notification_dispatcher.maxMessages = self.max_messages + elif self.mode == "dbus": + self.notification_dispatcher = DBusNotification("qtpyvcp") + else: + raise Exception("error notification mode {}".format(self.mode)) + + self.startTimer(200)
+ +
[docs] def postGuiInitialise(self, main_window): + if self.enabled: + if self.mode == "native": + self.notification_dispatcher = NativeNotification(parent=main_window) + self.notification_dispatcher.maxMessages = self.max_messages + elif self.mode == "dbus": + self.notification_dispatcher = DBusNotification("qtpyvcp") + else: + raise Exception("error notification mode {}".format(self.mode))
+ +
[docs] def terminate(self): + if self.persistent: + self.data_manager.setData('messages', self.messages)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/plugins/positions.html b/_modules/qtpyvcp/plugins/positions.html new file mode 100644 index 000000000..3ae29b60e --- /dev/null +++ b/_modules/qtpyvcp/plugins/positions.html @@ -0,0 +1,426 @@ + + + + + + qtpyvcp.plugins.positions — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for qtpyvcp.plugins.positions

+"""Smart Axis and Join positions plugin.
+
+Calculates Absolute, Relative, and Distance-To-Go values for each axis.
+Configurable to report actual or commanded positions and values in either
+machine or program units.
+
+Usage:
+    Example syntax to use in Widget Rules Editor::
+
+        position:abs?string&axis=x        # returns X axis absolute position
+        position:rel?string&axis=x        # returns X axis relative position
+        position:dtg?string&axis=x        # returns X axis DTG value
+
+
+YAML configuration:
+
+This goes in your config.yml file in the `data_plugins` section.
+
+.. code-block:: yaml
+
+    data_plugins:
+      position:
+        kwargs:
+          # whether to report actual or commanded pos
+          report_actual_pos: False
+          # whether to report program or machine units
+          use_program_units: True
+          # format used for metric units
+          metric_format: "%9.3f"
+          # format used for imperial units
+          imperial_format: "%8.4f"
+
+ToDO:
+    Add joint positions.
+"""
+
+import math
+
+from qtpyvcp.utilities.info import Info
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.plugins import DataPlugin, DataChannel, getPlugin
+
+STATUS = getPlugin('status')
+STAT = STATUS.stat
+INFO = Info()
+
+# Set up logging
+LOG = getLogger(__name__)
+
+MACHINE_COORDS = INFO.getCoordinates()
+MACHINE_UNITS = 2 if INFO.getIsMachineMetric() else 1
+
+# Set the conversions used for changing the DRO units
+# Only convert linear axes (XYZUVW), use factor of unity for ABC
+if MACHINE_UNITS == 2:
+    # List of factors for converting from mm to inches
+    CONVERSION_FACTORS = [1.0 / 25.4] * 3 + [1] * 3 + [1.0 / 25.4] * 3
+else:
+    # List of factors for converting from inches to mm
+    CONVERSION_FACTORS = [25.4] * 3 + [1] * 3 + [25.4] * 3
+
+
+
[docs]class Position(DataPlugin): + """Positions Plugin""" + def __init__(self, report_actual_pos=False, use_program_units=True, + metric_format='%9.3f', imperial_format='%8.4f'): + super(Position, self).__init__() + + self._report_actual_pos = False + self._use_program_units = use_program_units + self._metric_format = metric_format + self._imperial_format = imperial_format + + self._current_format = self._imperial_format + + self._update() + + # all these should cause the positions to update + STATUS.position.signal.connect(self._update) + STATUS.g5x_offset.signal.connect(self._update) + STATUS.g92_offset.signal.connect(self._update) + STATUS.tool_offset.signal.connect(self._update) + STATUS.program_units.signal.connect(self.updateUnits) + + self.report_actual_pos = report_actual_pos + +
[docs] def getChannel(self, url): + """Get data channel from URL. + + Args: + url (str) : The URL of the channel to get. + + Returns: + tuple : (chan_obj, chan_exp) + """ + + chan, sep, query = url.partition('?') + raw_args = query.split('&') + + args = [] + kwargs = {} + for arg in [a for a in raw_args if a != '']: + if '=' in arg: + key, val = arg.split('=') + kwargs[key] = val + else: + args.append(arg) + + try: + chan_obj = self.channels[chan] + + if 'axis' in kwargs: + axis = kwargs.pop('axis') + try: + kwargs['anum'] = int(axis) + except ValueError: + kwargs['anum'] = 'xyzabcuvw'.index(str(axis).lower()) + + if len(args) > 0 and args[0] in ('string', 'text', 'str'): + chan_exp = lambda: chan_obj.getString(*args[1:], **kwargs) + else: + chan_exp = lambda: chan_obj.getValue(*args, **kwargs) + + except (KeyError, SyntaxError): + LOG.exception('Error getting channel') + return None, None + + return chan_obj, chan_exp
+ + def updateUnits(self, canon_units): + print(('updating units', canon_units)) + if canon_units == 2: + self._current_format = self._metric_format + else: + self._current_format = self._imperial_format + + self._update() + + @DataChannel + def rel(self, chan, anum=-1): + """The current relative axis positions including all offsets + + To get a single axis pass string and the axis letter:: + + position:rel?string&axis=x + + To get a tuple of all the axes pass only string:: + + position:rel? + + :returns: current relative axis positions including all offsets + :rtype: tuple, str + """ + + if anum == -1: + return chan.value + return chan.value[anum] + + @rel.tostring + def rel(self, chan, anum): + return self._current_format % chan.value[anum] + + @DataChannel + def abs(self, chan, anum=-1): + """The current absolute axis positions + + To get a single axis pass string and the axis letter:: + + position:abs?string&axis=x + + To get a tuple of all the axes pass only string:: + + position:abs? + + :returns: current absolute axis positions + :rtype: tuple, str + """ + + if anum == -1: + return chan.value + return chan.value[anum] + + @abs.tostring + def abs(self, chan, anum): + return self._current_format % chan.value[anum] + + + @DataChannel + def dtg(self, chan, anum=-1): + """The remaining distance-to-go for the current move + + To get a single axis pass string and the axis letter:: + + position:dtg?string&axis=x + + To get a tuple of all the axes pass only string:: + + position:dtg? + + :returns: remaining distance-to-go for the current move + :rtype: tuple, str + """ + + if anum == -1: + return chan.value + return chan.value[anum] + + @dtg.tostring + def dtg(self, chan, anum): + return self._current_format % chan.value[anum] + + # aliases + Relative = rel + Absolute = abs + DistanceToGo = dtg + + @property + def report_actual_pos(self): + """Whether to report the actual position. Default True. + + See the YAML configuration. + + """ + return self._report_actual_pos + + @report_actual_pos.setter + def report_actual_pos(self, report_actual_pos): + if report_actual_pos == self._report_actual_pos: + return + self._report_actual_pos = report_actual_pos + + if self._report_actual_pos: + # disconnect commanded pos update signals + STATUS.position.signal.disconnect(self._update) + # STATUS.joint_position.signal.disconnect(self._update) + # connect actual pos update signals + STATUS.actual_position.signal.connect(self._update) + # STATUS.joint_actual_position.signal.connect(self.joint._update) + else: + # disconnect actual pos update signals + STATUS.actual_position.signal.disconnect(self._update) + # STATUS.joint_actual_position.signal.disconnect(self._update) + # connect commanded pos update signals + STATUS.position.signal.connect(self._update) + # STATUS.joint_position.signal.connect(self._update) + + def _update(self): + + if self._report_actual_pos: + pos = STAT.actual_position + else: + pos = STAT.position + + dtg = STAT.dtg + g5x_offset = STAT.g5x_offset + g92_offset = STAT.g92_offset + tool_offset = STAT.tool_offset + + rel = [0] * 9 + for axis in INFO.AXIS_NUMBER_LIST: + rel[axis] = pos[axis] - g5x_offset[axis] - tool_offset[axis] + + if STAT.rotation_xy != 0: + t = math.radians(-STAT.rotation_xy) + xr = rel[0] * math.cos(t) - rel[1] * math.sin(t) + yr = rel[0] * math.sin(t) + rel[1] * math.cos(t) + rel[0] = xr + rel[1] = yr + + for axis in INFO.AXIS_NUMBER_LIST: + rel[axis] -= g92_offset[axis] + + if STAT.program_units != MACHINE_UNITS and self._use_program_units: + pos = [pos[anum] * CONVERSION_FACTORS[anum] for anum in range(9)] + rel = [rel[anum] * CONVERSION_FACTORS[anum] for anum in range(9)] + dtg = [dtg[anum] * CONVERSION_FACTORS[anum] for anum in range(9)] + + self.rel.setValue(tuple(rel)) + self.abs.setValue(tuple(pos)) + self.dtg.setValue(tuple(dtg))
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/plugins/status.html b/_modules/qtpyvcp/plugins/status.html new file mode 100644 index 000000000..f4da91bde --- /dev/null +++ b/_modules/qtpyvcp/plugins/status.html @@ -0,0 +1,978 @@ + + + + + + qtpyvcp.plugins.status — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for qtpyvcp.plugins.status

+import os
+import linuxcnc
+
+from qtpy.QtCore import QTimer, QFileSystemWatcher, Qt
+from qtpy.QtWidgets import QApplication
+
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.app.runtime_config import RuntimeConfig
+from qtpyvcp.plugins import DataPlugin, DataChannel
+
+from qtpyvcp.utilities.info import Info
+
+INFO = Info()
+LOG = getLogger(__name__)
+STAT = linuxcnc.stat()
+CMD = linuxcnc.command()
+
+
+IN_DESIGNER = os.getenv('DESIGNER', False)
+
+
[docs]class Status(DataPlugin): + + stat = STAT + + def __init__(self, cycle_time=100): + super(Status, self).__init__() + + + self.no_force_homing = INFO.noForceHoming() + + self.file_watcher = None + + # recent files + self.max_recent_files = 10 + with RuntimeConfig('~/.axis_preferences') as rc: + files = rc.get('DEFAULT', 'recentfiles', default=[]) + files = [file for file in files if os.path.exists(file)] + self.recent_files.setValue(files) + + # MDI history + self._max_mdi_history_length = 100 + self._mdi_history_file = INFO.getMDIHistoryFile() + self.loadMdiHistory(self._mdi_history_file) + + self.jog_increment = 0 # jog + self.step_jog_increment = INFO.getIncrements()[0] + self.jog_mode = True + self.linear_jog_velocity = INFO.getJogVelocity() + self.angular_jog_velocity = INFO.getJogVelocity() + + if not IN_DESIGNER: + try: + STAT.poll() + except Exception as e: + LOG.Error("Unable to poll status channel") + LOG.Debug(e) + + excluded_items = ['axis', 'joint', 'spindle', 'poll'] + + self.old = {} + # initialize data channels + for item in dir(STAT): + if item in self.channels: + self.old[item] = getattr(STAT, item) + self.channels[item].setValue(getattr(STAT, item)) + elif item not in excluded_items and not item.startswith('_'): + self.old[item] = getattr(STAT, item) + chan = DataChannel(doc=item) + chan.setValue(getattr(STAT, item)) + self.channels[item] = chan + setattr(self, item, chan) + + # add joint status channels + self.joint = tuple(JointStatus(jnum) for jnum in range(9)) + for joint in self.joint: + for chan, obj in list(joint.channels.items()): + self.channels['joint.{}.{}'.format(joint.jnum, chan)] = obj + + # add spindle status channels + self.spindle = tuple(SpindleStatus(snum) for snum in range(8)) + for spindle in self.spindle: + for chan, obj in list(spindle.channels.items()): + self.channels['spindle.{}.{}'.format(spindle.snum, chan)] = obj + + self.all_axes_homed.value = False + self.homed.notify(self.all_axes_homed.setValue) + self.enabled.notify(self.all_axes_homed.setValue) + + # Set up the periodic update timer + self.timer = QTimer() + self._cycle_time = cycle_time + self.timer.timeout.connect(self._periodic) + + self.on.settable = True + self.task_state.notify(lambda ts: + self.on.setValue(ts == linuxcnc.STATE_ON)) + + # Set default UI locking counter + self.locking_count = 0 + + recent_files = DataChannel(doc='List of recently loaded files', settable=True, data=[]) + + def isLocked(self): + return True if self.locking_count > 0 else False + + def addLock(self): + if self.locking_count == 0: + QApplication.setOverrideCursor(Qt.BusyCursor) + self.locking_count += 1 + LOG.debug(f"Add lock. Total = {self.locking_count}") + + def removeLock(self): + self.locking_count -= 1 + if self.locking_count <= 0: + self.locking_count = 0 + QApplication.restoreOverrideCursor() + LOG.debug(f"---- Remove lock: restoreOverrideCursor") + LOG.debug(f"Remove lock. Total = {self.locking_count}") + +
[docs] def loadMdiHistory(self, fname): + """Load MDI history from file.""" + mdi_history = [] + if os.path.isfile(fname): + with open(fname, 'r') as fh: + for line in fh.readlines(): + line = line.strip() + mdi_history.append(line) + + mdi_history.reverse() + self.mdi_history.setValue(mdi_history)
+ +
[docs] def saveMdiHistory(self, fname): + """Write MDI history to file.""" + with open(fname, 'w') as fh: + cmds = self.mdi_history.value + cmds.reverse() + for cmd in cmds: + fh.write(cmd + '\n')
+ + @DataChannel + def axis_mask(self, chan, format='int'): + """Axes as configured in the [TRAJ]COORDINATES INI option. + + To return the string in a status label:: + + status:axis_mask + status:axis_mask?string + status:axis_mask?list + + :returns: the configured axes + :rtype: int, list, str + """ + + if format == 'list': + + mask = '{0:09b}'.format(self.stat.axis_mask or 7) + + axis_list = [] + for anum, enabled in enumerate(mask[::-1]): + if enabled == '1': + axis_list.append(anum) + + return axis_list + + return self.stat.axis_mask + + @axis_mask.tostring + def axis_mask(self, chan): + axes = '' + for anum in self.axis_mask.getValue(format='list'): + axes += 'XYZABCUVW'[anum] + + return axes + + @DataChannel + def mdi_history(self, chan): + """List of recently issued MDI commands. + Commands are stored in reverse chronological order, with the + newest command at the front of the list, and oldest at the end. + When the list exceeds the length given by MAX_MDI_COMMANDS the + oldest entries will be dropped. + + Duplicate commands will not be removed, so that MDI History + can be replayed via the queue meachanisim from a point in + the history forward. The most recently issued + command will always be at the front of the list. + """ + return chan.value + + @mdi_history.setter + def mdi_history(self, chan, new_value): + LOG.debug("---------set mdi_history: {}, {}".format(chan, new_value)) + if isinstance(new_value, list): + chan.value = new_value[:self._max_mdi_history_length] + else: + cmd = str(new_value.strip()) + cmds = chan.value + LOG.debug("---------cmd: {}".format(cmd)) + LOG.debug("---------cmds: {}".format(cmds)) + if cmd in cmds: + cmds.remove(cmd) + + cmds.insert(0, cmd) + chan.value = cmds[:self._max_mdi_history_length] + LOG.debug("---------chan.value: {}".format(chan.value)) + + chan.signal.emit(chan.value) + +
[docs] def mdi_remove_entry(self, mdi_index): + """Remove the indicated cmd by index reference""" + # TODO: This has some potential code redundancy. Follow above pattern + chan = self.mdi_history + cmds = chan.value + # only attempt to delete if index is in range. + if mdi_index > -1 and mdi_index < len(cmds): + del cmds[mdi_index] + chan.signal.emit(cmds) + else: + LOG.debug("---------mdi history delete attempt index out of range")
+ +
[docs] def mdi_swap_entries(self, index1, index2): + """Switch two entries about.""" + chan = self.mdi_history + cmds = chan.value + cmds[index2], cmds[index1] = cmds[index1], cmds[index2] + chan.signal.emit(cmds)
+ +
[docs] def mdi_remove_all(self): + """Remove all entries in mdi history""" + chan = self.mdi_history + cmds = chan.value + cmds.clear() + chan.signal.emit(cmds)
+ + @DataChannel + def on(self, chan): + """True if machine power is ON.""" + return STAT.task_state == linuxcnc.STATE_ON + + @DataChannel + def file(self, chan): + """Currently loaded file including path""" + return chan.value or 'No file loaded' + + @file.setter + def file(self, chan, fname): + if STAT.interp_state == linuxcnc.INTERP_IDLE \ + and STAT.call_level == 0: + + if self.file_watcher is not None: + if self.file_watcher.files(): + self.file_watcher.removePath(chan.value) + if os.path.isfile(fname): + self.file_watcher.addPath(fname) + + chan.value = fname + chan.signal.emit(fname) + + def updateFile(self, path): + if STAT.interp_state == linuxcnc.INTERP_IDLE: + LOG.debug("Reloading edited G-Code file: %s", path) + if os.path.isfile(path): + self.file.signal.emit(path) + CMD.program_open(path) + else: + LOG.debug("G-Code file changed, won't reload: %s", path) + + @DataChannel + def state(self, chan): + """Current command execution status + + 1) Done + 2) Exec + 3) Error + + To return the string in a status label:: + + status:state?string + + :returns: current command execution state + :rtype: int, str + """ + return STAT.state + + @state.tostring + def state(self, chan): + states = {0: "N/A", + linuxcnc.RCS_DONE: "Done", + linuxcnc.RCS_EXEC: "Exec", + linuxcnc.RCS_ERROR: "Error"} + + return states[STAT.state] + + @DataChannel + def exec_state(self, chan): + """Current task execution state + + 1) Error + 2) Done + 3) Waiting for Motion + 4) Waiting for Motion Queue + 5) Waiting for Pause + 6) -- + 7) Waiting for Motion and IO + 8) Waiting for Delay + 9) Waiting for system CMD + 10) Waiting for spindle orient + + To return the string in a status label:: + + status:exec_state?string + + :returns: current task execution error + :rtype: int, str + """ + return STAT.exec_state + + @exec_state.tostring + def exec_state(self, chan): + exec_states = {0: "N/A", + linuxcnc.EXEC_ERROR: "Error", + linuxcnc.EXEC_DONE: "Done", + linuxcnc.EXEC_WAITING_FOR_MOTION: "Waiting for Motion", + linuxcnc.EXEC_WAITING_FOR_MOTION_QUEUE: "Waiting for Motion Queue", + linuxcnc.EXEC_WAITING_FOR_IO: "Waiting for Pause", + linuxcnc.EXEC_WAITING_FOR_MOTION_AND_IO: "Waiting for Motion and IO", + linuxcnc.EXEC_WAITING_FOR_DELAY: "Waiting for Delay", + linuxcnc.EXEC_WAITING_FOR_SYSTEM_CMD: "Waiting for system CMD", + linuxcnc.EXEC_WAITING_FOR_SPINDLE_ORIENTED: "Waiting for spindle orient"} + + return exec_states[STAT.exec_state] + + @DataChannel + def interp_state(self, chan): + """Current state of RS274NGC interpreter + + 1) Idle + 2) Reading + 3) Paused + 4) Waiting + + To return the string in a status label:: + + status:interp_state?string + + :returns: RS274 interpreter state + :rtype: int, str + """ + return STAT.interp_state + + @interp_state.tostring + def interp_state(self, chan): + interp_states = {0: "N/A", + linuxcnc.INTERP_IDLE: "Idle", + linuxcnc.INTERP_READING: "Reading", + linuxcnc.INTERP_PAUSED: "Paused", + linuxcnc.INTERP_WAITING: "Waiting"} + + return interp_states[STAT.interp_state] + + + @DataChannel + def interpreter_errcode(self, chan): + """Current RS274NGC interpreter return code + + 0) Ok + 1) Exit + 2) Finished + 3) Endfile + 4) File not open + 5) Error + + To return the string in a status label:: + + status:interpreter_errcode?string + + :returns: interp error code + :rtype: int, str + """ + return STAT.interpreter_errcode + + @interpreter_errcode.tostring + def interpreter_errcode(self, chan): + interpreter_errcodes = {0: "Ok", + 1: "Exit", + 2: "Finished", + 3: "Endfile", + 4: "File not open", + 5: "Error"} + + return interpreter_errcodes[STAT.interpreter_errcode] + + @DataChannel + def task_state(self, chan, query=None): + """Current status of task + + 1) E-Stop + 2) Reset + 3) Off + 4) On + + To return the string in a status label:: + + status:task_state?string + + :returns: current task state + :rtype: int, str + """ + return STAT.task_state + + @task_state.tostring + def task_state(self, chan): + task_states = {0: "N/A", + linuxcnc.STATE_ESTOP: "E-Stop", + linuxcnc.STATE_ESTOP_RESET: "Reset", + linuxcnc.STATE_ON: "On", + linuxcnc.STATE_OFF: "Off"} + + return task_states[STAT.task_state] + + @DataChannel + def task_mode(self, chan): + """Current task mode + + 1) Manual + 2) Auto + 3) MDI + + To return the string in a status label:: + + status:task_mode?string + + :returns: current task mode + :rtype: int, str + """ + return STAT.task_mode + + @task_mode.tostring + def task_mode(self, chan): + task_modes = {0: "N/A", + linuxcnc.MODE_MANUAL: "Manual", + linuxcnc.MODE_AUTO: "Auto", + linuxcnc.MODE_MDI: "MDI"} + + return task_modes[STAT.task_mode] + + @DataChannel + def motion_mode(self, chan): + """Current motion controller mode + + 1) Free + 2) Coord + 3) Teleop + + To return the string in a status label:: + + status:motion_mode?string + + :returns: current motion mode + :rtype: int, str + """ + return STAT.motion_mode + + @motion_mode.tostring + def motion_mode(self, chan): + modes = {0: "N/A", + linuxcnc.TRAJ_MODE_COORD: "Coord", + linuxcnc.TRAJ_MODE_FREE: "Free", + linuxcnc.TRAJ_MODE_TELEOP: "Teleop"} + + return modes[STAT.motion_mode] + + @DataChannel + def motion_type(self, chan, query=None): + """Motion type + + 0) None + 1) Traverse + 2) Linear Feed + 3) Arc Feed + 4) Tool Change + 5) Probing + 6) Rotary Index + + To return the string in a status label:: + + status:motion_type?string + + :returns: current motion type + :rtype: int, str + """ + return STAT.motion_type + + @motion_type.tostring + def motion_type(self, chan): + motion_types = {0: "None", + linuxcnc.MOTION_TYPE_TRAVERSE: "Traverse", + linuxcnc.MOTION_TYPE_FEED: "Linear Feed", + linuxcnc.MOTION_TYPE_ARC: "Arc Feed", + linuxcnc.MOTION_TYPE_TOOLCHANGE: "Tool Change", + linuxcnc.MOTION_TYPE_PROBING: "Probing", + linuxcnc.MOTION_TYPE_INDEXROTARY: "Rotary Index"} + + return motion_types[STAT.motion_type] + + @DataChannel + def program_units(self, chan): + """Program units + + Available as an integer, or in short or long string formats. + + 1) in, Inches + 2) mm, Millimeters + 3) cm, Centimeters + + To return the string in a status label:: + + status:program_units + status:program_units?string + status:program_units?string&format=long + + :returns: current program units + :rtype: int, str + """ + return STAT.program_units + + @program_units.tostring + def program_units(self, chan, format='short'): + if format == 'short': + return ["N/A", "in", "mm", "cm"][STAT.program_units] + else: + return ["N/A", "Inches", "Millimeters", "Centimeters"][STAT.program_units] + + @DataChannel + def linear_units(self, chan): + """Machine linear units + + Available as float (units/mm), or in short or long string formats. + + To return the string in a status label:: + + status:linear_units + status:linear_units?string + status:linear_units?string&format=long + + :returns: machine linear units + :rtype: float, str + """ + return STAT.linear_units + + @linear_units.tostring + def linear_units(self, chan, format='short'): + if format == 'short': + return {0.0: "N/A", 1.0: "mm", 1 / 25.4: "in"}[STAT.linear_units] + else: + return {0.0: "N/A", 1.0: "Millimeters", 1 / 25.4: "Inches"}[STAT.linear_units] + + @DataChannel + def gcodes(self, chan, fmt=None): + """G-codes + + active G-codes for each modal group + + | syntax ``status:gcodes`` returns tuple of strings + | syntax ``status:gcodes?raw`` returns tuple of integers + | syntax ``status:gcodes?string`` returns str + """ + if fmt == 'raw': + return STAT.gcodes + return chan.value + + @gcodes.tostring + def gcodes(self, chan): + return " ".join(chan.value) + + @gcodes.setter + def gcodes(self, chan, gcodes): + chan.value = tuple(["G%g" % (c/10.) for c in sorted(gcodes[1:]) if c != -1]) + chan.signal.emit(self.gcodes.value) + + @DataChannel + def mcodes(self, chan, fmt=None): + """M-codes + + active M-codes for each modal group + + | syntax ``status:mcodes`` returns tuple of strings + | syntax ``status:mcodes?raw`` returns tuple of integers + | syntax ``status:mcodes?string`` returns str + """ + if fmt == 'raw': + return STAT.mcodes + return chan.value + + @mcodes.tostring + def mcodes(self, chan): + return " ".join(chan.value) + + @mcodes.setter + def mcodes(self, chan, gcodes): + chan.value = tuple(["M%g" % gcode for gcode in sorted(gcodes[1:]) if gcode != -1]) + chan.signal.emit(chan.value) + + @DataChannel + def g5x_index(self, chan): + """Current G5x work coord system + + | syntax ``status:g5x_index`` returns int + | syntax ``status:g5x_index?string`` returns str + """ + return STAT.g5x_index + + @DataChannel + def rotation_xy(self, chan): + """Current G5x Rotation + + | syntax ``status:rotation_xy`` returns float + """ + return STAT.rotation_xy + + @g5x_index.tostring + def g5x_index(self, chan): + return ["G53", "G54", "G55", "G56", "G57", "G58", + "G59", "G59.1", "G59.2", "G59.3"][STAT.g5x_index] + + @DataChannel + def settings(self, chan, item=None): + """Interpreter Settings + + Available Items: + 0) sequence_number + 1) feed + 2) speed + + :return: interpreter settings + :rtype: tuple, int, float + """ + if item is None: + return STAT.settings + return STAT.settings[{'sequence_number': 0, 'feed': 1, 'speed': 2}[item]] + + @DataChannel + def homed(self, chan, anum=None): + """Axis homed status + + If no axis number is specified returns a tuple of integers. + If ``anum`` is specified returns True if the axis is homed, else False. + + Rules syntax:: + + status:homed + status:homed?anum=0 + + Args: + anum (int, optional) : the axis number to return the homed state of. + + :returns: axis homed states + :rtype: tuple, bool + + """ + if anum is None: + return STAT.homed + if int(anum) > len(INFO.AXIS_LETTER_LIST)-1: + # looking for a ui element axis that machine does not have + LOG.warning(f'Homed axis anum={anum} will be outside INFO.AXIS_LETTER_LIST index range: 0 to {len(INFO.AXIS_LETTER_LIST)-1}') + return False + axis_ltr = INFO.AXIS_LETTER_LIST[int(anum)] + is_homed = [] + for ax in INFO.ALETTER_JNUM_DICT: + if axis_ltr == ax[0]: + axis_num = INFO.ALETTER_JNUM_DICT[ax] + is_homed.append(STAT.homed[axis_num]) + if 0 in is_homed: + return False + else: + return True + #return bool(STAT.homed[int(anum)]) + + @DataChannel + def all_axes_homed(self, chan): + """All axes homed status + + True if all axes are homed or if [TRAJ]NO_FORCE_HOMING set in INI. + + If [TRAJ]NO_FORCE_HOMING is set in the INI the value will be come + true as soon as the machine is turned on and the signal will be emitted, + otherwise the signal will be emitted once all the axes defined in the + INI have been homed. + + :returns: all homed + :rtype: bool + """ + return chan.value + + @all_axes_homed.setter + def all_axes_homed(self, chan, homed): + if self.no_force_homing: + all_homed = True + else: + for anum in INFO.AXIS_NUMBER_LIST: + if STAT.homed[anum] != 1: + all_homed = False + break + else: + all_homed = True + + if all_homed != chan.value: + chan.value = all_homed + chan.signal.emit(chan.value) + + + # this is used by File "qtpyvcp/qtpyvcp/actions/program_actions.py", + # line 83, in _run_ok elif not STATUS.allHomed(): + + def allHomed(self): + if self.no_force_homing: + return True + for jnum in range(STAT.joints): + if not STAT.joint[jnum]['homed']: + return False + return True + +
[docs] def forceUpdateStaticChannelMembers(self): + """Static items need a force update to operate properly with the + gui rules. This needs to be done with consideration to the + data structure so as to not "break" things. + """ + # TODO: add to this list as needed. Possible to externalise via yaml? + pass
+ # axes not in stat chanel + # self.old['axes'] = None + +
[docs] def initialise(self): + """Start the periodic update timer.""" + + # watch the gcode file for changes and reload as needed + self.file_watcher = QFileSystemWatcher() + if self.file.value: + self.file_watcher.addPath(self.file.value) + self.file_watcher.fileChanged.connect(self.updateFile) + + LOG.debug("Starting periodic updates with %ims cycle time", + self._cycle_time) + self.timer.start(self._cycle_time) + + self.forceUpdateStaticChannelMembers()
+ +
[docs] def terminate(self): + """Save persistent data on terminate.""" + + # save recent files + with RuntimeConfig('~/.axis_preferences') as rc: + rc.set('DEFAULT', 'recentfiles', self.recent_files.value) + + # save MDI history + self.saveMdiHistory(self._mdi_history_file)
+ + def _periodic(self): + + # s = time.time() + + try: + STAT.poll() + except Exception: + LOG.warning("Status polling failed, is LinuxCNC running?", exc_info=True) + self.timer.stop() + return + + # status updates + for item, old_val in self.old.items(): + new_val = getattr(STAT, item) + if new_val != old_val: + self.old[item] = new_val + self.channels[item].setValue(new_val) + + # joint status updates + for joint in self.joint: + joint._update() + + # spindle status updates + for spindle in self.spindle: + spindle._update()
+ + # print(time.time() - s) + + +
[docs]class JointStatus(DataPlugin): + def __init__(self, jnum): + super(JointStatus, self).__init__() + + self.jnum = jnum + self.jstat = STAT.joint[jnum] + + for key, value in list(self.jstat.items()): + chan = DataChannel(doc=key, data=value) + self.channels[key] = chan + setattr(self, key, chan) + + def _update(self): + """Periodic joint item updates.""" + + jstat = list(STAT.joint[self.jnum].items()) + changed_items = tuple(set(jstat) - set(self.jstat.items())) + for item in changed_items: + LOG.debug('JOINT_{0} {1}: {2}'.format(self.jnum, item[0], item[1])) + self.channels[item[0]].setValue(item[1]) + + self.jstat.update(jstat)
+ + +
[docs]class SpindleStatus(DataPlugin): + def __init__(self, snum): + super(SpindleStatus, self).__init__() + + self.snum = snum + self.sstat = STAT.spindle[snum] + + for key, value in list(self.sstat.items()): + chan = DataChannel(doc=key, data=value) + self.channels[key] = chan + setattr(self, key, chan) + + def _update(self): + """Periodic spindle item updates.""" + + sstat = list(STAT.spindle[self.snum].items()) + changed_items = tuple(set(sstat) - set(self.sstat.items())) + for item in changed_items: + LOG.debug('Spindle_{0} {1}: {2}'.format(self.snum, item[0], item[1])) + self.channels[item[0]].setValue(item[1]) + + self.sstat.update(sstat)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/plugins/tool_table.html b/_modules/qtpyvcp/plugins/tool_table.html new file mode 100644 index 000000000..7bef1ed50 --- /dev/null +++ b/_modules/qtpyvcp/plugins/tool_table.html @@ -0,0 +1,607 @@ + + + + + + qtpyvcp.plugins.tool_table — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for qtpyvcp.plugins.tool_table

+"""Tool Table data plugin.
+
+Exposes all the info available in the tool table. Watches the
+tool table file for changes and re-loads as needed.
+
+Tool Table YAML configuration:
+
+.. code-block:: yaml
+
+    data_plugins:
+      tooltable:
+        kwargs:
+          # specify the columns that should be read and writen to the
+          # tooltable file. To use all columns set to: TPXYZABCUVWDIJQR
+          columns: PTDZR
+          # specify text to be added before the tool table data
+          file_header_template: |
+            LinuxCNC Tool Table
+            -------------------
+
+            QtPyVCP will preserve comments before the opening semicolon.
+"""
+
+import os
+import re
+import io
+from itertools import takewhile
+from datetime import datetime
+
+import linuxcnc
+
+from qtpy.QtCore import QFileSystemWatcher, QTimer, Signal, Slot
+
+import qtpyvcp
+from qtpyvcp.utilities.info import Info
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.actions.machine_actions import issue_mdi
+from qtpyvcp.plugins import DataPlugin, DataChannel, getPlugin
+
+CMD = linuxcnc.command()
+LOG = getLogger(__name__)
+STATUS = getPlugin('status')
+STAT = STATUS.stat
+INFO = Info()
+
+IN_DESIGNER = os.getenv('DESIGNER', False)
+
+
+
[docs]def merge(a, b): + """Shallow merge two dictionaries""" + r = a.copy() + r.update(b) + return r
+ + +DEFAULT_TOOL = { + 'A': 0.0, + 'B': 0.0, + 'C': 0.0, + 'D': 0.0, + 'I': 0.0, + 'J': 0.0, + 'P': 0, + 'Q': 1, + 'T': -1, + 'U': 0.0, + 'V': 0.0, + 'W': 0.0, + 'X': 0.0, + 'Y': 0.0, + 'Z': 0.0, + 'R': '', +} + +NO_TOOL = merge(DEFAULT_TOOL, {'T': 0, 'R': 'No Tool Loaded'}) + +# FILE_HEADER = """ +# LinuxCNC Tool Table +# ------------------- +# +# (QtPyVCP will preserve any comments before this separator.) +# --- +# Generated by: QtPyVCP ToolTable plugin ({version}) +# Generated on: {datetime:%x %I:%M:%S %p} +# +# """ + +COLUMN_LABELS = { + 'A': 'A Offset', + 'B': 'B Offset', + 'C': 'C Offset', + 'D': 'Diameter', + 'I': 'Fnt Ang', + 'J': 'Bak Ang', + 'P': 'Pocket', + 'Q': 'Orient', + 'R': 'Remark', + 'T': 'Tool', + 'U': 'U Offset', + 'V': 'V Offset', + 'W': 'W Offset', + 'X': 'X Offset', + 'Y': 'Y Offset', + 'Z': 'Z Offset', +} + +# Column formats when writing tool table +INT_COLUMN_WIDTH = 6 +FLOAT_COLUMN_WIDTH = 12 +FLOAT_DECIMAL_PLACES = 6 + + +def makeLorumIpsumToolTable(): + return {i: merge(DEFAULT_TOOL, + {'T': i, 'P': i, 'R': 'Lorum Ipsum ' + str(i)}) + for i in range(10)} + + +
[docs]class ToolTable(DataPlugin): + + TOOL_TABLE = {0: NO_TOOL} + DEFAULT_TOOL = DEFAULT_TOOL + COLUMN_LABELS = COLUMN_LABELS + + tool_table_changed = Signal(dict) + + def __init__(self, columns='TPXYZABCUVWDIJQR', file_header_template=None, + remember_tool_in_spindle=True): + super(ToolTable, self).__init__() + + self.db_prog = INFO.ini.find('EMCIO','DB_PROGRAM') + self.fs_watcher = None + self.orig_header_lines = [] + self.file_header_template = file_header_template or '' + self.remember_tool_in_spindle = remember_tool_in_spindle + self.columns = self.validateColumns(columns) or [c for c in 'TPXYZABCUVWDIJQR'] + + self.data_manager = getPlugin('persistent_data_manager') + + self.setCurrentToolNumber(0) + + self.tool_table_file = INFO.getToolTableFile() + if not os.path.exists(self.tool_table_file) and self.db_prog is None: + return + + self.loadToolTable() + + self.current_tool.setValue(self.TOOL_TABLE[STATUS.tool_in_spindle.getValue()]) + + # update signals + STATUS.tool_in_spindle.notify(self.setCurrentToolNumber) + STATUS.tool_table.notify(lambda *args: self.loadToolTable()) + + STATUS.all_axes_homed.notify(self.reload_tool) + + def reload_tool(self): + if self.remember_tool_in_spindle and STATUS.all_axes_homed.value and STATUS.enabled.value: + tnum = self.data_manager.getData('tool-in-spindle', 0) + LOG.debug("reload_tool: tool in spindle: %i new tool: %i" % (STAT.tool_in_spindle, tnum)) + if STAT.tool_in_spindle == 0 and tnum != STAT.tool_in_spindle: + LOG.info("Reloading tool in spindle: %i", tnum) + cmd = "M61 Q{0} G43".format(tnum) + # give LinuxCNC time to switch modes + QTimer.singleShot(200, lambda: issue_mdi(cmd)) + + @DataChannel + def current_tool(self, chan, item=None): + """Current Tool Info + + Available items: + + * T -- tool number + * P -- pocket number + * X -- x offset + * Y -- y offset + * Z -- z offset + * A -- a offset + * B -- b offset + * C -- c offset + * U -- u offset + * V -- v offset + * W -- w offset + * I -- front angle + * J -- back angle + * Q -- orientation + * R -- remark + + Rules channel syntax:: + + tooltable:current_tool + tooltable:current_tool?X + tooltable:current_tool?x_offset + + :param item: the name of the tool data item to get + :return: dict, int, float, str + """ + if item is None: + return self.TOOL_TABLE[STAT.tool_in_spindle] + return self.TOOL_TABLE[STAT.tool_in_spindle].get(item[0].upper()) + +
[docs] def initialise(self): + if self.db_prog is None: + self.fs_watcher = QFileSystemWatcher() + self.fs_watcher.addPath(self.tool_table_file) + self.fs_watcher.fileChanged.connect(self.onToolTableFileChanged) + else: + self.fs_watcher = None
+ +
[docs] def terminate(self): + self.data_manager.setData('tool-in-spindle', STAT.tool_in_spindle)
+ +
[docs] @staticmethod + def validateColumns(columns): + """Validate display column specification. + + The user can specify columns in multiple ways, method is used to make + sure that that data is validated and converted to a consistent format. + + Args: + columns (str | list) : A string or list of the column IDs + that should be shown in the tooltable. + + Returns: + None if not valid, else a list of uppercase column IDs. + """ + if not isinstance(columns, (str, list, tuple)): + return + + return [col for col in [col.strip().upper() for col in columns] + if col in 'TPXYZABCUVWDIJQR' and not col == '']
+ +
[docs] def newTool(self, tnum=None): + """Get a dict of default tool values for a new tool.""" + if tnum is None: + tnum = len(self.TOOL_TABLE) + new_tool = DEFAULT_TOOL.copy() + new_tool.update({'T': tnum, 'R': 'New Tool'}) + return new_tool
+ + def onToolTableFileChanged(self, path): + LOG.debug('Tool Table file changed: {}'.format(path)) + # ToolEdit deletes the file and then rewrites it, so wait + # a bit to ensure the new data has been writen out. + QTimer.singleShot(50, self.reloadToolTable) + + def setCurrentToolNumber(self, tool_num): + self.current_tool.setValue(self.TOOL_TABLE[tool_num]) + + def reloadToolTable(self): + # rewatch the file if it stop being watched because it was deleted + if self.tool_table_file not in self.fs_watcher.files() and self.db_prog is None: + self.fs_watcher.addPath(self.tool_table_file) + + # reload with the new data + tool_table = self.loadToolTable() + self.tool_table_changed.emit(tool_table) + + def iterTools(self, tool_table=None, columns=None): + tool_table = tool_table or self.TOOL_TABLE + columns = self.validateColumns(columns) or self.columns + for tool in sorted(tool_table.keys()): + tool_data = tool_table[tool] + yield [tool_data[key] for key in columns] + + def loadToolTable(self, tool_file=None): + + if tool_file is None: + tool_file = self.tool_table_file + + if not os.path.exists(tool_file) and self.db_prog is None: + if IN_DESIGNER: + lorum_tooltable = makeLorumIpsumToolTable() + self.current_tool.setValue(lorum_tooltable) + return lorum_tooltable + LOG.critical("Tool table file does not exist: {}".format(tool_file)) + return {} + + if self.db_prog is None: + with io.open(tool_file, 'r') as fh: + lines = [line.strip() for line in fh.readlines()] + + # find opening colon, and get header data so it can be restored + for rlnum, line in enumerate(reversed(lines)): + if line.startswith(';'): + lnum = len(lines) - rlnum + raw_header = lines[:lnum] + lines = lines[lnum:] + + self.orig_header_lines = list(takewhile(lambda l: + not l.strip() == '---' and + not l.startswith(';Tool'), raw_header)) + break + + table = {0: NO_TOOL,} + for line in lines: + + data, sep, comment = line.partition(';') + items = re.findall(r"([A-Z]+[0-9.+-]+)", data.replace(' ', '')) + + tool = DEFAULT_TOOL.copy() + for item in items: + descriptor = item[0] + if descriptor in 'TPXYZABCUVWDIJQR': + value = item[1:] + if descriptor in ('T', 'P', 'Q'): + + try: + tool[descriptor] = int(value) + except: + LOG.error('Error converting value to int: {}'.format(value)) + break + else: + try: + tool[descriptor] = float(value) + except: + LOG.error('Error converting value to float: {}'.format(value)) + break + + tool['R'] = comment.strip() + + tnum = tool['T'] + if tnum == -1: + continue + + # add the tool to the table + table[tnum] = tool + else: + # build tool table from linxcnc status object + table = {0: NO_TOOL,} + lcnc_tools = STAT.tool_table + for tool in lcnc_tools: + if int(tool.id) != -1: + newtool = DEFAULT_TOOL.copy() + # build up new tool + newtool['T'] = int(tool.id) + newtool['P'] = int(lcnc_tools.index(tool)) + newtool['X'] = float(tool.xoffset) + newtool['Y'] = float(tool.yoffset) + newtool['Z'] = float(tool.zoffset) + newtool['A'] = float(tool.aoffset) + newtool['B'] = float(tool.boffset) + newtool['C'] = float(tool.coffset) + newtool['U'] = float(tool.uoffset) + newtool['V'] = float(tool.voffset) + newtool['W'] = float(tool.woffset) + newtool['D'] = float(tool.diameter) + newtool['I'] = float(tool.frontangle) + newtool['J'] = float(tool.backangle) + newtool['Q'] = int(tool.orientation) + newtool['R'] = 'Database tool' + table[int(tool.id)] = newtool + + # update tooltablec + self.__class__.TOOL_TABLE = table + + self.current_tool.setValue(self.TOOL_TABLE[STATUS.tool_in_spindle.getValue()]) + + # import json + # print(json.dumps(table, sort_keys=True, indent=4)) + + self.tool_table_changed.emit(table) + return table.copy() + + def getToolTable(self): + return self.TOOL_TABLE.copy() + +
[docs] def saveToolTable(self, tool_table, columns=None, tool_file=None): + """Write tooltable data to file. + + Args: + tool_table (dict) : Dictionary of dictionaries containing + the tool data to write to the file. + columns (str | list) : A list of data columns to write. + If `None` will use the value of ``self.columns``. + tool_file (str) : Path to write the tooltable too. + Defaults to ``self.tool_table_file``. + """ + + if self.db_prog is not None: + LOG.warn("Tool Table Plugin trying to write to DB Data Storage - ignoring request.") + return + + columns = self.validateColumns(columns) or self.columns + + if tool_file is None: + tool_file = self.tool_table_file + + lines = [] + header_lines = [] + + # restore file header + if self.file_header_template: + try: + header_lines = self.file_header_template.format( + version=qtpyvcp.__version__, + datetime=datetime.now()).lstrip().splitlines() + header_lines.append('') # extra new line before table header + except: + pass + + if self.orig_header_lines: + try: + self.orig_header_lines.extend(header_lines[header_lines.index('---'):]) + header_lines = self.orig_header_lines + except ValueError: + header_lines = self.orig_header_lines + + lines.extend(header_lines) + + # create the table header + items = [] + if 'P' not in columns: + columns.insert(1, 'P') + + for col in columns: + if col == 'R': + continue + w = (INT_COLUMN_WIDTH if col in 'TPQ' else FLOAT_COLUMN_WIDTH) - \ + (1 if col == self.columns[0] else 0) + items.append('{:<{w}}'.format(COLUMN_LABELS[col], w=w)) + + items.append('Remark') + lines.append(';' + ' '.join(items)) + + # add the tools + for tool_num in sorted(tool_table.keys())[1:]: + items = [] + tool_data = tool_table[tool_num] + for col in columns: + if col == 'R': + continue + if col in 'TPQ': + items.append('{col}{val:<{w}}' + .format(col=col, + val=tool_data[col], + w=INT_COLUMN_WIDTH)) + else: + items.append('{col}{val:<+{w}.{d}f}' + .format(col=col, + val=tool_data[col], + w=FLOAT_COLUMN_WIDTH, + d=FLOAT_DECIMAL_PLACES)) + + comment = tool_data.get('R', '') + if comment != '': + items.append('; ' + comment) + + + lines.append(''.join(items)) + + # for line in lines: + # print(line) + + # write to file + with io.open(tool_file, 'w') as fh: + fh.write('\n'.join(lines)) + fh.write('\n') # new line at end of file + fh.flush() + os.fsync(fh.fileno()) + + CMD.load_tool_table()
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/base_widgets/base_widget.html b/_modules/qtpyvcp/widgets/base_widgets/base_widget.html new file mode 100644 index 000000000..f15df06d9 --- /dev/null +++ b/_modules/qtpyvcp/widgets/base_widgets/base_widget.html @@ -0,0 +1,434 @@ + + + + + + qtpyvcp.widgets.base_widgets.base_widget — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.base_widgets.base_widget

+"""
+Base Widgets
+------------
+
+This file contains the definitions of the fundamental widgets upon which
+all other QtPyVCP widgets are based.
+"""
+
+import os
+import json
+
+from qtpy.QtCore import Property, Slot
+from qtpy.QtWidgets import QPushButton
+
+from qtpyvcp.plugins import getPlugin
+from qtpyvcp.utilities.logger import getLogger
+
+LOG = getLogger(__name__)
+
+
[docs]class ChanList(list): + """Channel value list. + + This list is intended to hold lambda functions for retrieving the current + data channel values. When the list is indexed the function is called and + the resulting value is returned. + """ + def __getitem__(self, index): + return super(ChanList, self).__getitem__(index)()
+ + +
[docs]class VCPPrimitiveWidget(object): + """VCPPrimitiveWidget. + + Class on which all QtPyVCP widgets should be based. + """ + def __init__(self, parent=None): + super(VCPPrimitiveWidget, self).__init__() + +
[docs] def initialize(self): + """This method is called right before the main application starts.""" + pass
+ +
[docs] def terminate(self): + """This method is called right before the main application ends.""" + pass
+ + +
[docs]class VCPBaseWidget(VCPPrimitiveWidget): + """QtPyVCP Base Widget. + + This class handles the rules and other things that + apply to QtPyVCP widgets regardless of use. + """ + IN_DESIGNER = os.getenv('DESIGNER') != None + + DEFAULT_RULE_PROPERTY = 'None' + RULE_PROPERTIES = { + 'None': ['None', None], + 'Enable': ['setEnabled', bool], + 'Visible': ['setVisible', bool], + 'Style Class': ['setStyleClass', str], + 'Style Sheet': ['setStyleSheet', str], + } + + def __init__(self, parent=None): + super(VCPBaseWidget, self).__init__() + self._rules = '[]' + self._style = '' + self._data_channels = [] + self._security_level = 0 + + # + # Security implementation + # + + @Property(int) + def security(self): + """ Security level + + An integer representing the security level for a widget. + The higher the integer value the higher the operator access level + needs to be to be able to interact with the widget. + + Example: + + security = 0 requires an operator to have an assigned security value + of 0 or more. This is essentially the lowest security rating. + Negative numbers having no effect. + + security = 5 requires an operator to have an assigned security level + of 5 or more to interact with the widget. So an operator with a + rating of 2 will not be able to interact with the widget. The widget + will be represented as "disabled" to them. + + returns: + int + """ + return self._security_level + + @security.setter + def security(self, security): + self._security_level = security + + # + # Style Rules implementation + # +
[docs] def setStyleClass(self, style_class): + """Set the QSS style class for the widget""" + self.setProperty('style', style_class)
+ + @Property(str, designable=False) + def style(self): + """QSS style class selector property. + + This property can be changed dynamically to update the QSS style + applied to the widget. + + Example: + + The ``style`` property can be used as a selector in QSS to + apply different styles depending on the value. + + :: + + /* This will be applied when the `style` is set to "error" */ + WidgetClass[style="error"] { + color: red; + } + + /* This will be applied when the `style` is not set */ + WidgetClass { + color: black; + } + + Returns: + str + """ + return self._style + + @style.setter + def style(self, style): + self._style = style + self.style().unpolish(self) + self.style().polish(self) + + @Property(str, designable=False) + def rules(self): + """JSON formatted list of dictionaries, defining the widget rules. + + Returns: + str + """ + return self._rules + + @rules.setter + def rules(self, rules): + self._rules = rules or '[]' + self.registerRules() + + def registerRules(self): + rules = json.loads(self._rules) + for rule in rules: + # print(rule) + ch = ChanList() + triggers = [] + for chan in rule['channels']: + + try: + url = chan['url'].strip() + protocol, sep, item = url.partition(':') + chan_obj, chan_exp = getPlugin(protocol).getChannel(item) + + ch.append(chan_exp) + + if chan.get('trigger', False): + triggers.append(chan_obj.notify) + + except Exception: + LOG.exception("Error evaluating rule: {}" + .format(chan.get('url', ''))) + return + + prop = self.RULE_PROPERTIES[rule['property']] + + if prop[1] is None: + # donothing + self._data_channels = ch + continue + + eval_env = {'ch': ch, 'widget': self} + eval_exp = 'lambda: widget.{}({})'.format( + prop[0], rule['expression']).encode('utf-8') + exp = eval(eval_exp, eval_env) + + # initial call to update + try: + exp() + except: + LOG.exception('Error calling rules expression:') + continue + + for trigger in triggers: + trigger(exp)
+ + +
[docs]class VCPWidget(VCPBaseWidget): + """VCP Widget + + This is a general purpose widget for displaying data + and other uses that do not involve user interaction. + """ + def __init__(self, parent=None): + super(VCPWidget, self).__init__()
+ +
[docs]class CMDWidget(VCPBaseWidget): + """Command Widget + + This widget should be used as the base class for all widgets + that control the machine. Eventually additional functionality + will be added to this class. + """ + def __init__(self, parent=None): + super(CMDWidget, self).__init__()
+ +
[docs]class HALWidget(VCPBaseWidget): + """HAL Widget + + This widget should be used as the base class for HAL widgets. + ToDo: Implement HAL functionality. + """ + def __init__(self, parent=None): + super(HALWidget, self).__init__() + + self._hal_object_name = None + + @Property(str) + def pinBaseName(self): + """The base name to use for the generated HAL pins. + + If not specified the widgets objectName will be used. + + Returns: + str + """ + if self._hal_object_name is None: + return str(self.objectName()).replace('_', '-') + return self._hal_object_name + + @pinBaseName.setter + def pinBaseName(self, name): + # ToDO: Validate HAL pin name + self._hal_object_name = name + + @Slot() + def getPinBaseName(self): + return self.pinBaseName
+ +
[docs]class VCPButton(QPushButton, CMDWidget): + """VCP Button Widget + + This is a general purpose button widget for displaying data + and other uses that do not involve user interaction. + """ + + DEFAULT_RULE_PROPERTY = 'Enable' + RULE_PROPERTIES = CMDWidget.RULE_PROPERTIES.copy() + RULE_PROPERTIES.update({ + 'Text': ['setText', str], + 'Checked': ['setChecked', bool] + }) + + def __init__(self, parent=None): + super(VCPButton, self).__init__(parent) + self.status = getPlugin('status') + +
[docs] def mousePressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if self.status.isLocked(): + LOG.debug('Accept mouse Press Event') + event.accept() + return + super().mousePressEvent(event)
+ +
[docs] def mouseReleaseEvent(self, event): + if self.status.isLocked(): + LOG.debug('Accept mouse Release Event') + event.accept() + return + super().mouseReleaseEvent(event)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/button_widgets/action_button.html b/_modules/qtpyvcp/widgets/button_widgets/action_button.html new file mode 100644 index 000000000..11030e58f --- /dev/null +++ b/_modules/qtpyvcp/widgets/button_widgets/action_button.html @@ -0,0 +1,181 @@ + + + + + + qtpyvcp.widgets.button_widgets.action_button — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.button_widgets.action_button

+from qtpy.QtCore import Property
+
+from qtpyvcp.widgets import VCPButton
+from qtpyvcp.actions import bindWidget, InvalidAction
+
+
[docs]class ActionButton(VCPButton): + """General purpose button for triggering QtPyVCP actions. + + Args: + parent (QWidget, optional) : The parent widget of the button, or None. + action (str, optional) : The name of the action the button should trigger. + """ + + def __init__(self, parent=None, action=None): + super(ActionButton, self).__init__(parent) + + self._action_name = '' + if action is not None: + self.actionName = action + + @Property(str) + def actionName(self): + """Property for the name of the action the button triggers (str). + + When this property is set it calls :meth:`QtPyVCP.actions.bindWidget` + to bind the widget to the action. + """ + return self._action_name + + @actionName.setter + def actionName(self, action_name): + self._action_name = action_name + try: + bindWidget(self, action_name) + except InvalidAction: + pass
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/button_widgets/action_checkbox.html b/_modules/qtpyvcp/widgets/button_widgets/action_checkbox.html new file mode 100644 index 000000000..6b6d3f4d4 --- /dev/null +++ b/_modules/qtpyvcp/widgets/button_widgets/action_checkbox.html @@ -0,0 +1,178 @@ + + + + + + qtpyvcp.widgets.button_widgets.action_checkbox — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.button_widgets.action_checkbox

+from qtpy.QtWidgets import QCheckBox
+from qtpy.QtCore import Property
+
+from qtpyvcp.widgets import CMDWidget
+from qtpyvcp.actions import bindWidget
+
+
[docs]class ActionCheckBox(QCheckBox, CMDWidget): + """General purpose checkbox for triggering QtPyVCP actions. + + Args: + parent (QWidget, optional) : The parent widget of the checkbox, or None. + action (str, optional) : The name of the action the checkbox should trigger. + """ + def __init__(self, parent=None, action=None): + super(ActionCheckBox, self).__init__(parent) + + self._action_name = '' + if action is not None: + self.actionName = action + + @Property(str) + def actionName(self): + """Property for the name of the action the checkbox triggers (str). + + When this property is set it calls :meth:`QtPyVCP.actions.bindWidget` + to bind the widget to the action. + """ + return self._action_name + + @actionName.setter + def actionName(self, action_name): + self._action_name = action_name + bindWidget(self, action_name)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/button_widgets/action_spinbox.html b/_modules/qtpyvcp/widgets/button_widgets/action_spinbox.html new file mode 100644 index 000000000..aca876499 --- /dev/null +++ b/_modules/qtpyvcp/widgets/button_widgets/action_spinbox.html @@ -0,0 +1,181 @@ + + + + + + qtpyvcp.widgets.button_widgets.action_spinbox — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.button_widgets.action_spinbox

+from qtpy.QtWidgets import QSpinBox
+from qtpy.QtCore import Property
+
+from qtpyvcp.widgets import CMDWidget
+from qtpyvcp.actions import bindWidget
+
+
[docs]class ActionSpinBox(QSpinBox, CMDWidget): + """Action spinbox for triggering QtPyVCP actions that take a numeric argument. + + On spinbox valueChange the action will be triggered with the spinbox + value passed as the action argument. + + Args: + parent (QWidget, optional) : The parent widget of the spindbox, or None. + action (str, optional) : The name of the action the spindbox should trigger. + """ + def __init__(self, parent=None, action=None): + super(ActionSpinBox, self).__init__(parent) + + self._action_name = '' + if action is not None: + self.actionName = action + + @Property(str) + def actionName(self): + """Property for the name of the action the spindbox triggers (str). + + When this property is set it calls :meth:`QtPyVCP.actions.bindWidget` + to bind the widget to the action. + """ + return self._action_name + + @actionName.setter + def actionName(self, action_name): + self._action_name = action_name + bindWidget(self, action_name)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/button_widgets/dialog_button.html b/_modules/qtpyvcp/widgets/button_widgets/dialog_button.html new file mode 100644 index 000000000..2e6e0cdca --- /dev/null +++ b/_modules/qtpyvcp/widgets/button_widgets/dialog_button.html @@ -0,0 +1,184 @@ + + + + + + qtpyvcp.widgets.button_widgets.dialog_button — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.button_widgets.dialog_button

+from qtpy.QtCore import Property, Slot
+
+from qtpyvcp.widgets import VCPButton
+from qtpyvcp.widgets.dialogs import showDialog
+
+
+
[docs]class DialogButton(VCPButton): + """Dialog Button. + + Button for launching dialogs. + + Args: + parent (QObject) : The dialog's parent or None. + dialog_name (str) : The name of the dialog to show then the button is clicked. + """ + + def __init__(self, parent=None, dialog_name=''): + super(DialogButton, self).__init__(parent) + + self._dialog_name = dialog_name + + self.clicked.connect(self.showDialog) + + @Slot() + def showDialog(self): + showDialog(self._dialog_name) + + @Property(str) + def dialogName(self): + """Property for the name of the dialog the button triggers (str). + + When this property is set it calls :meth:`QtPyVCP.actions.bindWidget` + to bind the widget to the action. + """ + return self._dialog_name + + @dialogName.setter + def dialogName(self, dialog_name): + self._dialog_name = dialog_name
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/button_widgets/mdi_button.html b/_modules/qtpyvcp/widgets/button_widgets/mdi_button.html new file mode 100644 index 000000000..93e65056f --- /dev/null +++ b/_modules/qtpyvcp/widgets/button_widgets/mdi_button.html @@ -0,0 +1,246 @@ + + + + + + qtpyvcp.widgets.button_widgets.mdi_button — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.button_widgets.mdi_button

+
+import re
+
+from qtpy.QtCore import Property
+from qtpy.QtWidgets import QApplication
+
+from qtpyvcp.widgets import VCPButton
+from qtpyvcp.actions.machine_actions import issue_mdi
+
+from qtpyvcp.utilities import logger
+LOG = logger.getLogger(__name__)
+
+# input format: T#<object_name> M6 G43
+# result: [("T", "object_name")]
+# if a group is not present it will be an empty string
+PARSE_VARS = re.compile(r'([A-Z0-9_-]*)#<([^>]+)>', re.I)
+
+
+
[docs]class MDIButton(VCPButton): + """ + MDI Button + + This widget is intended for calling individual MDI commands. Useful for + `Go To Home`, `Tool Change` and similar actions. + + The MDI command can include variables to be expanded from widgets present + in the active window. For example, to make a `Change Tool` button you could + add an MDIButton and a QLineEdit named ``tool_number_entry``. Then set the + the MDICommand property of the button to:: + + T#<tool_number_entry> M6 G43 + + When the button is pressed ``#<tool_number_entry>`` will be substituted with + the current text in the QLineEdit. + + Button for issuing MDI commands. + + Args: + parent (QWidget, optional) : The parent widget of the button, or None. + command (str, optional) : A gcode command string for the button to trigger. + """ + + def __init__(self, parent=None, command=''): + super(MDIButton, self).__init__(parent) + + self._mdi_cmd = command + + issue_mdi.bindOk(widget=self) + self.clicked.connect(self.issueMDI) + + def issueMDI(self): + window = QApplication.instance().activeWindow() + + try: + cmd = self._mdi_cmd.format(ch=self._data_channels) + except IndexError: + LOG.exception("Failed to format MDI command.") + return + + vars = PARSE_VARS.findall(self._mdi_cmd) + for cmd_word, object_name in vars: + + try: + + # get the value from the GUI input widget + wid = getattr(window, object_name) + + try: + # QSpinBox, QSlider, QDial + val = wid.value() + except AttributeError: + # QLabel, QLineEdit + val = wid.text() + + cmd = cmd.replace("{}#<{}>".format(cmd_word, object_name), + "{}{}".format(cmd_word, val)) + except: + LOG.exception("Couldn't expand '{}' variable.".format(object_name)) + return + + issue_mdi(cmd) + + @Property(str) + def MDICommand(self): + """Sets the MDI command property (str). + + A valid RS274 gcode command string. It can include variables to be + expanded from UI widgets present in the active window. + + Example: + Assuming there is a QLineEdit in the active window with the + objectName ``tool_number_entry``, the ``#<tool_number_entry>`` + variable would be substituted with the current text in the QLineEdit:: + + T#<tool_number_entry> M6 G43 + """ + return self._mdi_cmd + + @MDICommand.setter + def MDICommand(self, mdi_cmd): + self._mdi_cmd = mdi_cmd
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/button_widgets/subcall_button.html b/_modules/qtpyvcp/widgets/button_widgets/subcall_button.html new file mode 100644 index 000000000..d0d2021ce --- /dev/null +++ b/_modules/qtpyvcp/widgets/button_widgets/subcall_button.html @@ -0,0 +1,279 @@ + + + + + + qtpyvcp.widgets.button_widgets.subcall_button — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.button_widgets.subcall_button

+import os
+import re
+from qtpy.QtWidgets import qApp
+from qtpy.QtCore import Property
+
+from qtpyvcp.utilities.info import Info
+INFO = Info()
+
+from qtpyvcp.widgets import VCPButton
+from qtpyvcp.actions.machine_actions import issue_mdi
+
+from qtpyvcp.utilities import logger
+LOG = logger.getLogger(__name__)
+
+# input: #<param_name> = #1 (=0.125 comment)
+# result: [("param_name", "1", "0.125", "comment")]
+# if a group is not present it will be an empty string
+PARSE_POSITIONAL_ARGS = re.compile(r' *# *<([a-z0-9_-]+)> *= *#([0-9]+) *(?:\(= *([0-9.+-]+[0-9.]*?|) *(.*)\))?', re.I)
+
+SUBROUTINE_SEARCH_DIRS = INFO.getSubroutineSearchDirs()
+
+
[docs]class SubCallButton(VCPButton): + """Button for calling ngc subroutines. + + Args: + parent (QWidget, optional) : The parent widget of the button, or None. + filename (str, optional) : The filename of the NGCGUI style subroutine + the button should call, including any extension. The subroutine must + be on the subroutine path specified in the INI. The name of the + subroutine must match exactly the sub/endsub name. The parameter + #<parameter1> if found in the VCP the value from that widget will be + used instead of the default value. If you don't have a default value + you must have a widget by the same name. The widget can be a line + edit, a spin box or a double spin box. + :: + + example.ngc + o<example> sub + #<parameter1> = #1 + #<parameter2> = #2 (=default_value) + + ;Body of the subroutine + + o<example> endsub + """ + def __init__(self, parent=None, filename=''): + super(SubCallButton, self).__init__(parent) + + self._filename = filename + + issue_mdi.bindOk(widget=self) + self.clicked.connect(self.callSub) + + def callSub(self): + window = qApp.activeWindow() + + subfile = None + for dir in SUBROUTINE_SEARCH_DIRS: + tempfile = os.path.join(dir, self._filename) + if os.path.isfile(tempfile): + subfile = tempfile + break + + if subfile is None: + LOG.error('Subroutine file could not be found: yellow<{}>'.format(self._filename)) + return False + + with open(subfile, 'r') as fh: + lines = fh.readlines() + + args = [] + for line in lines: + line = line.strip() + if not line.startswith('#'): + continue + result_list = PARSE_POSITIONAL_ARGS.findall(line) + if len(result_list) == 0: + continue + + pname, pnumber, default_val, comment = result_list[0] + + if int(pnumber) > 30: + # only #1-#30 are passed to the sub + continue + + try: + # get the value from the GUI input widget + val = getattr(window, pname).text() or default_val + except: + val = default_val + LOG.warning('No input for red<{}> parameter, using default value blue<{}>'.format(pname, val)) + + if val == '': + LOG.error('No value given for parameter red<{}>, and no default specified'.format(pname)) + return False + + try: + val = float(val) + except ValueError: + LOG.error('Input value "{}" given for parameter "{}" is not a valid number'.format(val, pname)) + return False + + index = int(pnumber) - 1 + while len(args) <= index: + args.append("[0.0000]") + + args[index] = "[{}]".format(val) + + arg_str = ' '.join(args) + sub_name = os.path.splitext(self._filename)[0] + cmd_str = "o<{}> call {}".format(sub_name, arg_str) + + LOG.debug('Calling sub file: yellow<%s> with args blue<%s>', subfile, arg_str) + issue_mdi(cmd_str) + + @Property(str) + def filename(self): + """Gets or sets the filename of the subroutine the button should call (str). + + The subroutine file must be on the subroutine path as specified in the INI. + """ + return self._filename + + @filename.setter + def filename(self, filename): + self._filename = filename
+ +if __name__ == "__main__": + import sys + from qtpy.QtWidgets import QApplication + app = QApplication(sys.argv) + w = SubCallButton() + w.show() + sys.exit(app.exec_()) +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/containers/frame.html b/_modules/qtpyvcp/widgets/containers/frame.html new file mode 100644 index 000000000..a4dc8d1e7 --- /dev/null +++ b/_modules/qtpyvcp/widgets/containers/frame.html @@ -0,0 +1,164 @@ + + + + + + qtpyvcp.widgets.containers.frame — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.containers.frame

+
+from qtpy.QtWidgets import QFrame
+from qtpyvcp.widgets.base_widgets import VCPWidget
+
+
+
[docs]class VCPFrame(QFrame, VCPWidget): + """VCPFrame + + VCP Frame + + A frame widget that can be controlled via rules. + """ + + DEFAULT_RULE_PROPERTY = 'Enable' + + def __init__(self, parent): + super(VCPFrame, self).__init__(parent=parent)
+ + +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/containers/widget.html b/_modules/qtpyvcp/widgets/containers/widget.html new file mode 100644 index 000000000..ea650d5bc --- /dev/null +++ b/_modules/qtpyvcp/widgets/containers/widget.html @@ -0,0 +1,163 @@ + + + + + + qtpyvcp.widgets.containers.widget — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.containers.widget

+
+
+from qtpy.QtWidgets import QWidget
+from qtpyvcp.widgets.base_widgets import VCPWidget
+
+
+
[docs]class VCPWidget(QWidget, VCPWidget): + """VCPWidget + + VCP Widget Container + + A widget container that can be controlled via rules. + """ + DEFAULT_RULE_PROPERTY = 'Enable' + + def __init__(self, parent): + super(VCPWidget, self).__init__(parent=parent)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/dialogs/about_dialog.html b/_modules/qtpyvcp/widgets/dialogs/about_dialog.html new file mode 100644 index 000000000..e555504b5 --- /dev/null +++ b/_modules/qtpyvcp/widgets/dialogs/about_dialog.html @@ -0,0 +1,204 @@ + + + + + + qtpyvcp.widgets.dialogs.about_dialog — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.dialogs.about_dialog

+#   Copyright (c) 2019 Kurt Jacobson
+#      <kurtcjacobson@gmail.com>
+#
+#   This file is part of QtPyVCP.
+#
+#   QtPyVCP is free software: you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 2 of the License, or
+#   (at your option) any later version.
+#
+#   QtPyVCP is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with QtPyVCP.  If not, see <http://www.gnu.org/licenses/>.
+
+from qtpy import uic
+from qtpy.QtWidgets import QVBoxLayout, QDialog, QDialogButtonBox, QLabel
+
+from qtpyvcp.widgets.dialogs.base_dialog import BaseDialog
+
+
+
[docs]class AboutDialog(BaseDialog): + def __init__(self, *args, **kwargs): + super(AboutDialog, self).__init__(stay_on_top=True) + + self.ui_file = kwargs.get('ui_file') + + if self.ui_file: + uic.loadUi(self.ui_file, self) + else: + + self.setFixedSize(600, 200) + + self.setWindowTitle("About QtPyVCP") + + self.layout = QVBoxLayout() + self.setLayout(self.layout) + + self.button_box = QDialogButtonBox(QDialogButtonBox.Ok) + self.button_box.accepted.connect(self.close) + + self.about_text = QLabel() + self.about_text.setOpenExternalLinks(True) + self.about_text.setText( + """ + <center> + QtPyVCP is a Qt and Python based framework for LinuxCNC.<br /> + Copyright (c) 2018 - 2021 Kurt Jacobson<br /> + <a href="https://www.qtpyvcp.com">https://www.qtpyvcp.com</a> + </center> + """ + ) + + self.layout.addWidget(self.about_text) + self.layout.addWidget(self.button_box)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/dialogs/base_dialog.html b/_modules/qtpyvcp/widgets/dialogs/base_dialog.html new file mode 100644 index 000000000..04de01c88 --- /dev/null +++ b/_modules/qtpyvcp/widgets/dialogs/base_dialog.html @@ -0,0 +1,257 @@ + + + + + + qtpyvcp.widgets.dialogs.base_dialog — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.dialogs.base_dialog

+
+
+import os
+
+from qtpy import uic
+from qtpy.QtCore import Qt
+from qtpy.QtWidgets import QDialog
+
+from qtpyvcp.utilities.logger import getLogger
+
+LOG = getLogger(__name__)
+
+
+
[docs]class BaseDialog(QDialog): + """Base Dialog + + Base QtPyVCP dialog class. + + This is intended to be used as a base class for custom dialogs, as well + as a provider for use in YAML config files. This allows loading custom + dialogs from .ui files without needing to write any python code. + + You can launch dialogs using a + :doc:`Dialog Button </widgets/buttons/index>` or + from a window menu item. + + Example: + + YAML config for loading a custom dialog called `my_dialog` from a .ui + file named ``my_diloag.ui`` located in the same dir as the .yml file:: + + dialogs: + my_dialog: + provider: qtpyvcp.widgets.dialogs.base_dialog:BaseDialog + kwargs: + ui_file: {{ file.dir }}/my_dialog.ui + title: My Dialog Title # optional, set the dialog title + modal: false # optional, whether the dialog is modal + popup: false # optional, whether the dialog is a popup + frameless: false # optional, whether the dialog is frameless + stay_on_top: true # optional, whether the dialog stays on top + + Args: + parent (QWidget, optional) : The dialog's parent window, or None. + ui_file (str, optional) : The path of a .ui file to load the dialog + from. The ui base widget should be a QDialog. + title (str, optional) : The title to use for the dialog. This will + override any title property set in QtDesigner. + modal (bool, optional) : Whether the dialog should be application modal. + This will override any modality hints set in QtDesigner. + frameless (bool, optional) : Whether the window has a frame or not. + If the window does not have a frame you will need some way to + close it, like an Ok or Cancel button. + popup: (bool, optional) : Makes the dialog use a frame less window + that automatically hides when it looses focus. + stay_on_top (bool, optional) : Sets the stay on top hint window flag. + This overrides any window flags set in QtDesiger. + """ + def __init__(self, parent=None, ui_file=None, title=None, modal=None, + popup=None, frameless=None, stay_on_top=None): + super(BaseDialog, self).__init__(parent) + + if ui_file is not None: + self.loadUiFile(ui_file) + + if title is not None: + self.setWindowTitle(title) + + if modal is not None: + if modal: + self.setWindowModality(Qt.ApplicationModal) + else: + self.setWindowModality(Qt.NonModal) + + if popup is not None: + self.setWindowFlags(Qt.Popup) + + if frameless is not None: + self.setWindowFlag(Qt.FramelessWindowHint, frameless) + + if stay_on_top is not None: + self.setWindowFlag(Qt.WindowStaysOnTopHint, stay_on_top) + +
[docs] def loadUiFile(self, ui_file): + """Load dialog from a .ui file. + + The .ui file base class should be a QDialog. + + Args: + ui_file (str) : path to the .ui file to load. + """ + ui_file = os.path.realpath(ui_file) + if not os.path.isfile(ui_file): + LOG.error("Specified UI for dialog does not exist: %s", ui_file) + return + + LOG.debug("Loading dialog from ui_file: %s", ui_file) + uic.loadUi(ui_file, self)
+ +
[docs] def setWindowFlag(self, flag, on): + """BackPort QWidget.setWindowFlag() implementation from Qt 5.9 + + This method was introduced in Qt 5.9 so is not present + in Qt 5.7.1 which is standard on Debian 9 (stretch), so + add our own implementation. + """ + if on: + # add flag + self.setWindowFlags(self.windowFlags() | flag) + else: + # remove flag + self.setWindowFlags(self.windowFlags() ^ flag)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/dialogs/error_dialog.html b/_modules/qtpyvcp/widgets/dialogs/error_dialog.html new file mode 100644 index 000000000..b6c33d802 --- /dev/null +++ b/_modules/qtpyvcp/widgets/dialogs/error_dialog.html @@ -0,0 +1,284 @@ + + + + + + qtpyvcp.widgets.dialogs.error_dialog — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.dialogs.error_dialog

+import os
+from traceback import format_exception
+
+from qtpy import uic
+from qtpy.QtCore import Slot
+from qtpy.QtWidgets import QDialog, QApplication
+
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.widgets.dialogs.base_dialog import BaseDialog
+
+LOG = getLogger(__name__)
+
+IGNORE_LIST = []
+
+
+
[docs]class ErrorDialog(BaseDialog): + def __init__(self, exc_info): + super(ErrorDialog, self).__init__(stay_on_top=True) + + uic.loadUi(os.path.join(os.path.dirname(__file__), 'error_dialog.ui'), self) + + self.exc_info = exc_info + exc_type, exc_msg, exc_tb = exc_info + + self.exc_typ = exc_type.__name__ + self.exc_msg = exc_msg + self.exc_tb = "".join(format_exception(*exc_info)) + color = 'orange' if 'warning'in self.exc_typ.lower() else 'red' + self.errorType.setText("<font color='{}'>{}:</font>" + .format(color, self.exc_typ)) + self.errorValue.setText(str(exc_msg)) + self.setWindowTitle('Unhandled Exception - {}'.format(self.exc_typ)) + self.tracebackText.setText(self.exc_tb) + self.show() + + @Slot() + def on_quitApp_clicked(self): + if os.getenv('DESIGNER', False): + self.accept() + else: + QApplication.exit() + + @Slot() + def on_ignoreException_clicked(self): + if self.ignoreCheckBox.isChecked(): + LOG.warn("User selected to ignore future occurrences of exception.", + exc_info=self.exc_info) + IGNORE_LIST.append((str(self.exc_info[0]), + str(self.exc_info[1]), + self.exc_info[2].tb_lineno)) + print(IGNORE_LIST) + self.accept() + + @Slot() + def on_reportIssue_clicked(self): + import qtpy + import urllib.request, urllib.parse, urllib.error + import webbrowser + import subprocess + import linuxcnc + # import hiyapyco + import json + import qtpyvcp + issue_title = "{}: {}".format(self.exc_typ, self.exc_msg) + issue_body = ISSUE_TEMPLATE.format( + tracback=self.exc_tb.strip(), + qt_version=qtpy.QT_VERSION, + qt_api=qtpy.API_NAME, + api_version=qtpy.PYQT_VERSION or qtpy.PYSIDE_VERSION, + dist=subprocess.check_output(['lsb_release', '-d']).strip(), + kernel=subprocess.check_output(['uname', '-r']).strip(), + lcnc_version=linuxcnc.version, + qtpyvcp_version=qtpyvcp.__version__, + # config=hiyapyco.dump(qtpyvcp.CONFIG, default_flow_style=False), + options=json.dumps(qtpyvcp.OPTIONS, indent=4, sort_keys=True), + log_file=qtpyvcp.OPTIONS.get('log_file'), + config_file=qtpyvcp.OPTIONS.get('config_file'), + ) + + new_issue_url = "https://github.com/kcjengr/qtpyvcp/issues/new?" \ + "title={title}&body={body}&&labels=bug,auto+generated"\ + .format(title=urllib.parse.quote(issue_title), + body=urllib.parse.quote(issue_body)) + + webbrowser.open(new_issue_url, new=2, autoraise=True)
+ + +ISSUE_TEMPLATE = \ +"""(Please fill in this issue template with as much information as you can about the +circumstances under which the issue occurred, and the steps needed to reproduce it.) + +## Steps to reproduce the problem + +(provide as detailed a step by step as you can) + + 1. + 2. + 3. + +## This is what I expected to happen + +(explain what you thought should have happened) + +## This is what happened instead + +(explain what happened instead) + +## It worked properly before this + +(did it work before? what changed?) + +## Traceback + +```python +{tracback} +``` + +## Options + +```json +{options} +``` + +## System Info +``` + * {dist} + * Kernel: {kernel} + * Qt version: v{qt_version} + * Qt bindings: {qt_api} v{api_version} + * LinuxCNC version: v{lcnc_version} + * QtPyVCP version: {qtpyvcp_version} +``` + +## Attachments + +Please also find and attach the following files, along with any others that may be helpful: +* {log_file} +* {config_file} +""" +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/dialogs/offsets_dialog.html b/_modules/qtpyvcp/widgets/dialogs/offsets_dialog.html new file mode 100644 index 000000000..1ab5c251d --- /dev/null +++ b/_modules/qtpyvcp/widgets/dialogs/offsets_dialog.html @@ -0,0 +1,252 @@ + + + + + + qtpyvcp.widgets.dialogs.offsets_dialog — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.dialogs.offsets_dialog

+#   Copyright (c) 2018 Kurt Jacobson
+#      <kurtcjacobson@gmail.com>
+#
+#   This file is part of QtPyVCP.
+#
+#   QtPyVCP is free software: you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 2 of the License, or
+#   (at your option) any later version.
+#
+#   QtPyVCP is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with QtPyVCP.  If not, see <http://www.gnu.org/licenses/>.
+
+from collections import OrderedDict
+
+from qtpy.QtCore import Qt
+from qtpy.QtWidgets import QComboBox, QDialog, QHBoxLayout, QLabel, \
+    QDoubleSpinBox, QPushButton, QVBoxLayout
+
+from qtpyvcp.utilities.info import Info
+from qtpyvcp.utilities import logger
+from qtpyvcp.actions.machine_actions import issue_mdi
+from qtpyvcp.widgets.dialogs.base_dialog import BaseDialog
+
+Log = logger.getLogger(__name__)
+
+
+
[docs]class OffsetsDialog(BaseDialog): + + def __init__(self, parent=None): + super(OffsetsDialog, self).__init__(parent=parent, stay_on_top=True) + + self.info = Info() + self.log = Log + + axis_list = self.info.getAxisList() + + self.axis_combo = QComboBox() + for axis in axis_list: + self.axis_combo.addItem(axis.upper(), axis) + + coords_msg = QLabel("Coordinate relative to workpiece:") + system_msg = QLabel("Coordinate System:") + + self.coords_input = QDoubleSpinBox() + self.coords_input.setDecimals(4) + self.coords_input.setRange(-999999, 999999) + + self.system_combo = QComboBox() + + coord_systems = {"P0": "P0 Current", + "P1": "P1 G54", + "P2": "P2 G55", + "P3": "P3 G56", + "P4": "P4 G57", + "P5": "P5 G58", + "P6": "P6 G59", + "P7": "P7 G59.1", + "P8": "P8 G59.2", + "P9": "P9 G59.3" + } + + for key, value in list(OrderedDict(sorted(list(coord_systems.items()), key=lambda t: t[0])).items()): + self.system_combo.addItem(value, key) + + close_button = QPushButton("Close") + set_button = QPushButton("Set") + + main_layout = QVBoxLayout() + button_layout = QHBoxLayout() + + button_layout.addWidget(close_button) + button_layout.addWidget(set_button) + + main_layout.addWidget(self.axis_combo, alignment=Qt.AlignTop) + main_layout.addWidget(coords_msg, alignment=Qt.AlignLeft | Qt.AlignTop) + main_layout.addWidget(self.coords_input, alignment=Qt.AlignTop) + main_layout.addWidget(system_msg, alignment=Qt.AlignLeft | Qt.AlignTop) + main_layout.addWidget(self.system_combo, alignment=Qt.AlignBottom) + main_layout.addLayout(button_layout) + + self.setLayout(main_layout) + self.setWindowTitle("Regular Offsets") + + set_button.clicked.connect(self.set_method) + close_button.clicked.connect(self.close_method) + + + def set_method(self): + system = self.system_combo.currentData() + axis = self.axis_combo.currentData() + coords = self.coords_input.value() + + offset_mdi = "G10 L20 {} {}{:f}".format(system, axis, coords) + + if issue_mdi.ok(): + issue_mdi(offset_mdi) + else: + self.log.debug("Error issuing MDI: {}".format(issue_mdi.ok.msg)) + + def close_method(self): + self.hide()
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/dialogs/open_file_dialog.html b/_modules/qtpyvcp/widgets/dialogs/open_file_dialog.html new file mode 100644 index 000000000..796cad6e6 --- /dev/null +++ b/_modules/qtpyvcp/widgets/dialogs/open_file_dialog.html @@ -0,0 +1,211 @@ + + + + + + qtpyvcp.widgets.dialogs.open_file_dialog — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.dialogs.open_file_dialog

+from qtpy.QtCore import QUrl, QFileInfo
+from qtpy.QtWidgets import QFileDialog
+
+from qtpyvcp.plugins import getPlugin
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.widgets import VCPPrimitiveWidget
+
+LOG = getLogger(__name__)
+
+from qtpyvcp.utilities.info import Info
+INFO = Info()
+
+from qtpyvcp.actions.program_actions import load as loadProgram
+
+
+
[docs]class OpenFileDialog(QFileDialog, VCPPrimitiveWidget): + """NGC file chooser dialog.""" + def __init__(self, parent=None): + super(OpenFileDialog, self).__init__(parent) + + self.dm = getPlugin('persistent_data_manager') + + nc_file_dir = INFO.getProgramPrefix() + nc_file_types = INFO.getQtFilefilter() + + self.setDirectory(nc_file_dir) + self.setNameFilters(nc_file_types.split(';;')) + + self.setOption(self.DontUseNativeDialog) + self.setModal(True) + + urls = self.sidebarUrls() + urls.append(QUrl.fromLocalFile(nc_file_dir)) + self.setSidebarUrls(urls) + +
[docs] def accept(self): + path = self.selectedFiles()[0] + stats = QFileInfo(path) + if stats.isDir(): + self.setDirectory(path) + return + if not stats.exists(): + return + loadProgram(path) + self.hide()
+ + def sidbarUrlsToStringList(self): + return [qurl.toString() for qurl in self.sidebarUrls()] + + def setSidebarUrlsFromStringList(self, urls): + if urls is not None: + self.setSidebarUrls([QUrl(url) for url in urls]) + +
[docs] def initialize(self): + self.setViewMode(self.dm.getData('app.openFileDialog.viewMode', + OpenFileDialog.Detail)) + + urls = self.dm.getData('app.openFileDialog.sidebarUrls') + self.setSidebarUrlsFromStringList(urls)
+ +
[docs] def terminate(self): + self.dm.setData('app.openFileDialog.viewMode', + self.viewMode()) + + self.dm.setData('app.openFileDialog.sidebarUrls', + self.sidbarUrlsToStringList())
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/dialogs/probesim_dialog.html b/_modules/qtpyvcp/widgets/dialogs/probesim_dialog.html new file mode 100644 index 000000000..2d40d9c0e --- /dev/null +++ b/_modules/qtpyvcp/widgets/dialogs/probesim_dialog.html @@ -0,0 +1,224 @@ + + + + + + qtpyvcp.widgets.dialogs.probesim_dialog — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.dialogs.probesim_dialog

+#   Copyright (c) 2018 Kurt Jacobson
+#      <kurtcjacobson@gmail.com>
+#
+#   This file is part of QtPyVCP.
+#
+#   QtPyVCP is free software: you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 2 of the License, or
+#   (at your option) any later version.
+#
+#   QtPyVCP is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with QtPyVCP.  If not, see <http://www.gnu.org/licenses/>.
+
+import subprocess
+
+from qtpy.QtCore import QTimer
+from qtpy.QtWidgets import QPushButton, QVBoxLayout, QCheckBox
+
+from qtpyvcp.widgets.dialogs.base_dialog import BaseDialog
+
+
+from qtpyvcp.utilities.info import Info
+from qtpyvcp.utilities import logger
+
+Log = logger.getLogger(__name__)
+
+
+
[docs]class ProbeSim(BaseDialog): + + def __init__(self, parent=None): + super(ProbeSim, self).__init__(parent=parent) + + self.info = Info() + self.log = Log + + self.close_button = QPushButton("Touch") + self.pulse_checkbox = QCheckBox("Pulse") + + main_layout = QVBoxLayout() + + main_layout.addWidget(self.close_button) + main_layout.addWidget(self.pulse_checkbox) + + self.setLayout(main_layout) + self.setWindowTitle("Simulate touch probe") + + self.close_button.pressed.connect(self.touch_on) + self.close_button.released.connect(self.touch_off) + + self.timer = QTimer() + self.timer.timeout.connect(self.pulse_off) + self.timer.setSingleShot(True) + + def touch_on(self): + + if self.pulse_checkbox.checkState(): + self.timer.start(1000) + subprocess.Popen(['halcmd', 'setp', 'motion.probe-input', '1']) + + else: + subprocess.Popen(['halcmd', 'setp', 'motion.probe-input', '1']) + + def touch_off(self): + + if self.pulse_checkbox.checkState(): + return + + subprocess.Popen(['halcmd', 'setp', 'motion.probe-input', '0']) + + def pulse_off(self): + subprocess.Popen(['halcmd', 'setp', 'motion.probe-input', '0']) + +
[docs] def close(self): + self.hide()
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/dialogs/toolchange_dialog.html b/_modules/qtpyvcp/widgets/dialogs/toolchange_dialog.html new file mode 100644 index 000000000..2051d5c9b --- /dev/null +++ b/_modules/qtpyvcp/widgets/dialogs/toolchange_dialog.html @@ -0,0 +1,255 @@ + + + + + + qtpyvcp.widgets.dialogs.toolchange_dialog — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.dialogs.toolchange_dialog

+#   Copyright (c) 2023 Jose I. Romero
+#      <jir@electrumee.com>
+#
+#   This file is part of QtPyVCP.
+#
+#   QtPyVCP is free software: you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 2 of the License, or
+#   (at your option) any later version.
+#
+#   QtPyVCP is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with QtPyVCP.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+from qtpy import uic
+from qtpy.QtWidgets import QVBoxLayout, QDialog, QDialogButtonBox, QLabel
+
+from qtpyvcp.widgets.dialogs.base_dialog import BaseDialog
+from qtpyvcp.plugins import getPlugin
+
+from qtpyvcp import hal
+
+
[docs]class ToolChangeDialog(BaseDialog): + """Tool Change Dialog + + Manual tool changing dialog component + + This is a qtpyvcp replacement of axis' `hal_manualtoolchange`. It + uses the same pin names in the same way, but the HAL component they + are under is called `qtpyvcp_manualtoolchange` instead. + + Example: + Remove any references in .hal to ``hal_manualtoolchange`` + and remove ``net tool-change-loop`` if you have it. + + To your main `.hal` add:: + + # ---manual tool change signals--- + net tool-change-request <= iocontrol.0.tool-change + net tool-change-confirmed => iocontrol.0.tool-changed + net tool-number <= iocontrol.0.tool-prep-number + + # ---ignore tool prepare requests--- + net tool-prepare-loopback iocontrol.0.tool-prepare => iocontrol.0.tool-prepared + + and to you `*postgui.hal` add:: + + # ---manual tool change signals--- + net tool-change-request => qtpyvcp_manualtoolchange.change + net tool-change-confirmed <= qtpyvcp_manualtoolchange.changed + net tool-number => qtpyvcp_manualtoolchange.number + + """ + def __init__(self, *args, **kwargs): + super(ToolChangeDialog, self).__init__(stay_on_top=True) + + self.tt = getPlugin('tooltable') + self.tool_number = 0 + + default_ui = os.path.join(os.path.dirname(__file__), 'toolchange_dialog.ui') + + self.ui_file = kwargs.get('ui_file', default_ui) + + self.ui = uic.loadUi(self.ui_file, self) + + comp = hal.getComponent("qtpyvcp_manualtoolchange") + comp.addPin('number', 's32', 'in') + self.change_pin = comp.addPin('change', 'bit', 'in') + self.changed_pin = comp.addPin('changed', 'bit', 'out') + comp.addPin('change_button', 'bit', 'in') + + comp.addListener('number', self.prepare_tool) + comp.addListener('change', self.on_change) + comp.addListener('change_button', self.on_change_button) + self.startTimer(100) # Poll 10 times per second + self.hide() + +
[docs] def timerEvent(self, timer): + if not self.change_pin.value: + # Ensure that the changed pin is de-asserted when the change request pin is low + self.changed_pin.value = False + if self.isVisible(): + self.hide()
+ + def prepare_tool(self, tool_no): + if self.tool_number == tool_no: return # Already prepared this tool + tool_data = self.tt.getToolTable().get(tool_no, {}) + tool_r = tool_data.get('R', 'UNKNOWN') + self.ui.lblToolNumber.setText(str(tool_no)) + self.ui.lblToolRemark.setText(tool_r) + self.tool_number = tool_no + + def on_change(self, value=True): + if value: + self.show() + + def on_change_button(self, value=True): + if value: + self.accept() + +
[docs] def reject(self): + pass
+ +
[docs] def accept(self): + self.changed_pin.value = True
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/hal_widgets/hal_bar_indicator.html b/_modules/qtpyvcp/widgets/hal_widgets/hal_bar_indicator.html new file mode 100644 index 000000000..b0fd5e761 --- /dev/null +++ b/_modules/qtpyvcp/widgets/hal_widgets/hal_bar_indicator.html @@ -0,0 +1,220 @@ + + + + + + qtpyvcp.widgets.hal_widgets.hal_bar_indicator — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.hal_widgets.hal_bar_indicator

+from qtpyvcp import hal
+from qtpyvcp.widgets import HALWidget
+from qtpyvcp.widgets.base_widgets.bar_indicator import BarIndicatorBase
+
+# Setup logging
+from qtpyvcp.utilities.logger import getLogger
+log = getLogger(__name__)
+
+
+
[docs]class HalBarIndicator(BarIndicatorBase, HALWidget): + """ + HAL Bar Indicator + + Bar for indicating the value of `float` HAL pins. + + .. table:: Generated HAL Pins + + ============================= ===== ========= + HAL Pin Name Type Direction + ============================= ===== ========= + qtpyvcp.bar-indicator.in-i u32 in + qtpyvcp.bar-indicator.in-f float in + qtpyvcp.bar-indicator.min-val float in + qtpyvcp.bar-indicator.max-val float in + ============================= ===== ========= + """ + def __init__(self, parent=None): + super(HalBarIndicator, self).__init__(parent) + + self._int_in_pin = None + self._float_in_pin = None + + self._min_val_pin = None + self._max_val_pin = None + +
[docs] def initialize(self): + comp = hal.getComponent() + obj_name = self.getPinBaseName() + + if self.minimum < 0: + int_pin_typ = 's32' + else: + int_pin_typ = 'u32' + + # add bar-indicator.in-f HAL pin + self._int_in_pin = comp.addPin(obj_name + ".in-f", "float", "in") + self.setValue(self._int_in_pin.value) + self._int_in_pin.valueChanged.connect(self.setValue) + + # add bar-indicator.in-i HAL pin + self._float_in_pin = comp.addPin(obj_name + ".in-i", int_pin_typ, "in") + self.setValue(self._float_in_pin.value) + self._float_in_pin.valueChanged.connect(self.setValue) + + # add bar-indicator.min-val HAL pin + self._min_val_pin = comp.addPin(obj_name + ".min-val", "float", "in") + self._min_val_pin.value = self.minimum + self._min_val_pin.valueChanged.connect(lambda v: self.setProperty('minimum', v)) + + # add bar-indicator.max-val HAL pin + self._max_val_pin = comp.addPin(obj_name + ".max-val", "float", "in") + self._max_val_pin.value = self.maximum + self._max_val_pin.valueChanged.connect(lambda v: self.setProperty('maximum', v))
+ +# testing +if __name__ == "__main__": + import sys + from qtpy.QtWidgets import QApplication + app = QApplication(sys.argv) + w = HalBarIndicator() + w.setObjectName('hal-bar') + w.initialize() + w.show() + w.setValue(65) + sys.exit(app.exec_()) +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/hal_widgets/hal_button.html b/_modules/qtpyvcp/widgets/hal_widgets/hal_button.html new file mode 100644 index 000000000..fcaafb056 --- /dev/null +++ b/_modules/qtpyvcp/widgets/hal_widgets/hal_button.html @@ -0,0 +1,299 @@ + + + + + + qtpyvcp.widgets.hal_widgets.hal_button — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.hal_widgets.hal_button

+
+from qtpy.QtCore import Property, QTimer
+from qtpy.QtWidgets import QPushButton
+
+from qtpyvcp import hal
+from qtpyvcp.widgets import HALWidget
+
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.plugins import getPlugin
+
+
+LOG = getLogger(__name__)
+STATUS = getPlugin('status')
+
+
+
[docs]class HalButton(QPushButton, HALWidget): + """HAL Button + + Button for setting `bit` HAL pin values. + + .. table:: Generated HAL Pins + + ========================= ===== ========= + HAL Pin Name Type Direction + ========================= ===== ========= + qtpyvcp.button.enable bit in + qtpyvcp.button.out bit out + qtpyvcp.button.checked bit out + qtpyvcp.button.io bit io + ========================= ===== ========= + + .. note:: + + The `qtpyvcp.button.checked` halpin is only present if the :class:`.checkable` property is set to true. + + """ + def __init__(self, parent=None): + super(HalButton, self).__init__(parent) + + self.setText("HAL Button") + + self._enable_pin = None + self._pressed_pin = None + self._checked_pin = None + self._activated_pin = None + + self._pulse = False + self._pulse_duration = 100 + self.pulse_timer = None + + self.pressed.connect(self.onPress) + self.released.connect(self.onRelease) + self.toggled.connect(self.onCheckedStateChanged) + +
[docs] def mousePressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept mouse Press Event') + event.accept() + return + super().mousePressEvent(event)
+ +
[docs] def mouseReleaseEvent(self, event): + if STATUS.isLocked(): + LOG.debug('Accept mouse Release Event') + event.accept() + return + super().mouseReleaseEvent(event)
+ +
[docs] def keyPressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept keyPressEvent Event') + event.accept() + return + super().keyPressEvent(event)
+ +
[docs] def keyReleaseEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept keyReleaseEvent Event') + event.accept() + return + super().keyReleaseEvent(event)
+ + + def onPress(self): + if self._pressed_pin is not None: + self._pressed_pin.value = True + if self._activated_pin is not None: + self._activated_pin.value = True + if self._pulse: + self.pulse_timer.start(self._pulse_duration) + + def onRelease(self): + if self._pressed_pin is not None: + self._pressed_pin.value = False + if self._activated_pin is not None: + self._activated_pin.value = False + + def onCheckedStateChanged(self, checked): + if STATUS.isLocked(): + LOG.debug('Skip HAL onCheckedStateChanged') + return + if self._checked_pin is not None: + self._checked_pin.value = checked + + @Property(bool) + def pulseOnPress(self): + """If active, when the button is pressed the ``out`` pin will be `True` + for :class:`.pulseDuration` ms, otherwise the ``out`` pin will + be `True` for the duration of the button press. + """ + return self._pulse + + @pulseOnPress.setter + def pulseOnPress(self, pulse): + self._pulse = pulse + + @Property(int) + def pulseDuration(self): + """Pulse duration in ms used when :class:`.pulseOnPress` is active.""" + return self._pulse_duration + + @pulseDuration.setter + def pulseDuration(self, duration): + self._pulse_duration = duration + +
[docs] def initialize(self): + comp = hal.getComponent() + obj_name = self.getPinBaseName() + + # add button.enable HAL pin + self._enable_pin = comp.addPin(obj_name + ".enable", "bit", "in") + self._enable_pin.value = self.isEnabled() + self._enable_pin.valueChanged.connect(self.setEnabled) + + # add button.out HAL pin + self._pressed_pin = comp.addPin(obj_name + ".out", "bit", "out") + + # add button.activated HAL pin + self._activated_pin = comp.addPin(obj_name + ".io", "bit", "io") + self._activated_pin.value = self.isDown() + self._activated_pin.valueChanged.connect(self.setDown) + + if self.isCheckable(): + # add button.checked HAL pin + self._checked_pin = comp.addPin(obj_name + ".checked", "bit", "out") + self._checked_pin.value = self.isChecked() + + if self._pulse: + self.pulse_timer = QTimer() + self.pulse_timer.setSingleShot(True) + self.pulse_timer.timeout.connect(self.onRelease)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/hal_widgets/hal_checkbox.html b/_modules/qtpyvcp/widgets/hal_widgets/hal_checkbox.html new file mode 100644 index 000000000..1c49186bd --- /dev/null +++ b/_modules/qtpyvcp/widgets/hal_widgets/hal_checkbox.html @@ -0,0 +1,245 @@ + + + + + + qtpyvcp.widgets.hal_widgets.hal_checkbox — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.hal_widgets.hal_checkbox

+
+
+from qtpy.QtWidgets import QCheckBox
+from qtpy.QtCore import Property, QEvent
+
+from qtpyvcp import hal
+from qtpyvcp.widgets import HALWidget
+
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.plugins import getPlugin
+
+LOG = getLogger(__name__)
+STATUS = getPlugin('status')
+
+
+
[docs]class HalCheckBox(QCheckBox, HALWidget): + """HAL CheckBox + + CheckBox for displaying and setting `bit` HAL pin values. + + .. table:: Generated HAL Pins + + ========================= ===== ========= + HAL Pin Name Type Direction + ========================= ===== ========= + qtpyvcp.checkbox.enable bit in + qtpyvcp.checkbox.check bit in + qtpyvcp.checkbox.checked bit out + ========================= ===== ========= + """ + def __init__(self, parent=None): + super(HalCheckBox, self).__init__(parent) + + self._enable_pin = None + self._check_pin = None + self._checked_pin = None + + self.toggled.connect(self.onCheckedStateChanged) + +
[docs] def mousePressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept mouse Press Event') + event.accept() + return + super().mousePressEvent(event)
+ +
[docs] def mouseReleaseEvent(self, event): + if STATUS.isLocked(): + LOG.debug('Accept mouse Release Event') + event.accept() + return + super().mouseReleaseEvent(event)
+ +
[docs] def keyPressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept keyPressEvent Event') + event.accept() + return + super().keyPressEvent(event)
+ +
[docs] def keyReleaseEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept keyReleaseEvent Event') + event.accept() + return + super().keyReleaseEvent(event)
+ + +
[docs] def changeEvent(self, event): + super(HalCheckBox, self).changeEvent(event) + if event == QEvent.EnabledChange and self._enable_pin is not None: + self._enable_pin.value = self.isEnabled()
+ + def onCheckedStateChanged(self, checked): + if STATUS.isLocked(): + LOG.debug('Skip HAL onCheckedStateChanged') + return + if self._checked_pin is not None: + self._checked_pin.value = checked + +
[docs] def initialize(self): + comp = hal.getComponent() + obj_name = self.getPinBaseName() + + # add checkbox.enable HAL pin + self._enable_pin = comp.addPin(obj_name + ".enable", "bit", "in") + self._enable_pin.value = self.isEnabled() + self._enable_pin.valueChanged.connect(self.setEnabled) + + # add checkbox.check HAL pin + self._check_pin = comp.addPin(obj_name + ".check", "bit", "in") + self._check_pin.value = self.isChecked() + self._check_pin.valueChanged.connect(self.setChecked) + + # add checkbox.checked HAL pin + self._checked_pin = comp.addPin(obj_name + ".checked", "bit", "out") + self._checked_pin.value = self.isChecked()
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/hal_widgets/hal_double_spinbox.html b/_modules/qtpyvcp/widgets/hal_widgets/hal_double_spinbox.html new file mode 100644 index 000000000..456c9d624 --- /dev/null +++ b/_modules/qtpyvcp/widgets/hal_widgets/hal_double_spinbox.html @@ -0,0 +1,243 @@ + + + + + + qtpyvcp.widgets.hal_widgets.hal_double_spinbox — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.hal_widgets.hal_double_spinbox

+
+
+from qtpy.QtWidgets import QDoubleSpinBox
+from qtpy.QtCore import QEvent
+
+from qtpyvcp import hal
+from qtpyvcp.widgets import HALWidget
+
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.plugins import getPlugin
+
+LOG = getLogger(__name__)
+STATUS = getPlugin('status')
+
+
[docs]class HalDoubleSpinBox(QDoubleSpinBox, HALWidget): + """HAL DoubleSpinBox + + DoubleSpinBox for displaying and setting `float` HAL pin values. + + .. table:: Generated HAL Pins + + ========================= ========= ========= + HAL Pin Name Type Direction + ========================= ========= ========= + qtpyvcp.spinbox.enable bit in + qtpyvcp.spinbox.in float in + qtpyvcp.spinbox.out float out + ========================= ========= ========= + """ + def __init__(self, parent=None): + super(HalDoubleSpinBox, self).__init__(parent) + + self._value_pin = None + self._enabled_pin = None + + self._signed_int = True + + self.valueChanged.connect(self.onCheckedStateChanged) + +
[docs] def mousePressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept mouse Press Event') + event.accept() + return + super().mousePressEvent(event)
+ +
[docs] def mouseReleaseEvent(self, event): + if STATUS.isLocked(): + LOG.debug('Accept mouse Release Event') + event.accept() + return + super().mouseReleaseEvent(event)
+ +
[docs] def keyPressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept keyPressEvent Event') + event.accept() + return + super().keyPressEvent(event)
+ +
[docs] def keyReleaseEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept keyReleaseEvent Event') + event.accept() + return + super().keyReleaseEvent(event)
+ +
[docs] def changeEvent(self, event): + super(HalDoubleSpinBox, self).changeEvent(event) + if event == QEvent.EnabledChange and self._enabled_pin is not None: + self._enabled_pin.value = self.isEnabled()
+ + def onCheckedStateChanged(self, checked): + if STATUS.isLocked(): + LOG.debug('Skip HAL onCheckedStateChanged') + return + if self._value_pin is not None: + self._value_pin.value = checked + +
[docs] def initialize(self): + comp = hal.getComponent() + obj_name = self.getPinBaseName() + + # add spinbox.enabled HAL pin + self._enabled_pin = comp.addPin(obj_name + ".enable", "bit", "in") + self._enabled_pin.value = self.isEnabled() + self._enabled_pin.valueChanged.connect(self.setEnabled) + + # add spinbox.checked HAL pin + self._value_pin = comp.addPin(obj_name + ".out", "float", "out") + self._value_pin.value = self.value() + + # add spinbox.checked HAL pin + self._set_value_pin = comp.addPin(obj_name + ".in", "float", "in") + self._set_value_pin.valueChanged.connect(self.setValue)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/hal_widgets/hal_groupbox.html b/_modules/qtpyvcp/widgets/hal_widgets/hal_groupbox.html new file mode 100644 index 000000000..8c01cadc6 --- /dev/null +++ b/_modules/qtpyvcp/widgets/hal_widgets/hal_groupbox.html @@ -0,0 +1,206 @@ + + + + + + qtpyvcp.widgets.hal_widgets.hal_groupbox — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.hal_widgets.hal_groupbox

+
+from qtpy.QtWidgets import QGroupBox
+
+from qtpyvcp import hal
+from qtpyvcp.widgets import HALWidget
+
+
+
[docs]class HalGroupBox(QGroupBox, HALWidget): + """HAL GroupBox + + GroupBox that can be enabled/disabled via HAL pins. + + .. table:: Generated HAL Pins + + ========================= ===== ========= + HAL Pin Name Type Direction + ========================= ===== ========= + qtpyvcp.group-box.enable bit in + qtpyvcp.group-box.visible bit in + qtpyvcp.group-box.check bit in + qtpyvcp.group-box.checked bit out + ========================= ===== ========= + """ + + def __init__(self, parent=None): + super(HalGroupBox, self).__init__(parent) + + self._enable_pin = None + self._visible_pin = None + self._check_pin = None + self._checked_pin = None + + self.toggled.connect(self.onCheckedStateChanged) + + def onCheckedStateChanged(self, checked): + if self._checked_pin is not None: + self._checked_pin.value = checked + +
[docs] def initialize(self): + comp = hal.getComponent() + obj_name = self.getPinBaseName() + + # add group-box.enable HAL pin + self._enable_pin = comp.addPin(obj_name + ".enable", "bit", "in") + self._enable_pin.value = self.isEnabled() + self._enable_pin.valueChanged.connect(self.setEnabled) + + # add group-box.visible HAL pin + self._visible_pin = comp.addPin(obj_name + ".visible", "bit", "in") + self._visible_pin.value = self.isVisible() + self._visible_pin.valueChanged.connect(self.setVisible) + + if self.isCheckable(): + # add group-box.check HAL pin + self._check_pin = comp.addPin(obj_name + ".check", "bit", "in") + self._check_pin.value = self.isChecked() + self._check_pin.valueChanged.connect(self.setChecked) + + # add group-box.checked HAL pin + self._checked_pin = comp.addPin(obj_name + ".checked", "bit", "out") + self._checked_pin.value = self.isChecked()
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/hal_widgets/hal_label.html b/_modules/qtpyvcp/widgets/hal_widgets/hal_label.html new file mode 100644 index 000000000..69b34ef5c --- /dev/null +++ b/_modules/qtpyvcp/widgets/hal_widgets/hal_label.html @@ -0,0 +1,243 @@ + + + + + + qtpyvcp.widgets.hal_widgets.hal_label — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.hal_widgets.hal_label

+from qtpy.QtWidgets import QLabel
+from qtpy.QtCore import Property, Q_ENUMS
+
+from qtpyvcp import hal
+from qtpyvcp.widgets import HALWidget
+
+from . import HalType
+
+# Setup logging
+from qtpyvcp.utilities.logger import getLogger
+LOG = getLogger(__name__)
+
+
[docs]class HalLabel(QLabel, HALWidget, HalType): + """HAL Label + + Label for displaying HAL pin values. + + Input pin type is selectable via the :class:`.pinType` property in designer, + and can be any valid HAL type (bit, u32, s32, float). + + The text format can be specified via the :class:`.textFormat` property in + designer and can be any valid python style format string. + + .. table:: Generated HAL Pins + + ========================= =========== ========= + HAL Pin Name Type Direction + ========================= =========== ========= + qtpyvcp.label.enable bit in + qtpyvcp.label.in selecatable in + ========================= =========== ========= + """ + Q_ENUMS(HalType) + + def __init__(self, parent=None): + super(HalLabel, self).__init__(parent) + + self._in_pin = None + self._enable_pin = None + + self._typ = "float" + self._fmt = ".2f" + + self._value = 0 + + self.setValue(0) + + def setValue(self, value): + self._value = value + + try: + self.setText(f"{value:{self._fmt}}") + except Exception as e: + self.setText(f"ERR: {self._fmt}") + LOG.warning("Invalid format specified") + + + @Property(str) + def textFormat(self): + """Text Format Property + + Args: + fmt (str) : A valid python style format string. Defaults to ``%s``. + """ + + return self._fmt + + @textFormat.setter + def textFormat(self, fmt): + self._fmt = fmt + self.setValue(self._value) + + @Property(HalType) + def pinType(self): + return getattr(HalType, self._typ) + + @pinType.setter + def pinType(self, typ_enum): + self._typ = HalType.toString(typ_enum) + try: + val = {'bit': False, 'u32': 0, 's32': 0, 'float': 0.0}[self._typ] + self.setValue(val) + except Exception as ex: + LOG.debug(ex) + +
[docs] def initialize(self): + comp = hal.getComponent() + obj_name = self.getPinBaseName() + + # add label.enable HAL pin + self._enable_pin = comp.addPin(f"{obj_name}.enable", "bit", "in") + self._enable_pin.value = self.isEnabled() + self._enable_pin.valueChanged.connect(self.setEnabled) + + # add label.in HAL pin + self._in_pin = comp.addPin(f"{obj_name}.in", self._typ, "in") + self.setValue(self._in_pin.value) + self._in_pin.valueChanged.connect(self.setValue)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/hal_widgets/hal_lcd.html b/_modules/qtpyvcp/widgets/hal_widgets/hal_lcd.html new file mode 100644 index 000000000..904c0cae8 --- /dev/null +++ b/_modules/qtpyvcp/widgets/hal_widgets/hal_lcd.html @@ -0,0 +1,209 @@ + + + + + + qtpyvcp.widgets.hal_widgets.hal_lcd — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.hal_widgets.hal_lcd

+from qtpy.QtWidgets import QLCDNumber
+from qtpy.QtCore import Property
+
+from qtpyvcp import hal
+from qtpyvcp.widgets import HALWidget
+
+
+
[docs]class HalLCDNumber(QLCDNumber, HALWidget): + """HAL LCD Number + + LCD Number for displaying `float` or `s32` HAL pin values. + + .. table:: Generated HAL Pins + + ===================== ========= ========= + HAL Pin Name Type Direction + ===================== ========= ========= + qtpyvcp.lcd.in-i s32 in + qtpyvcp.lcd.in-f float in + ===================== ========= ========= + """ + + def __init__(self, parent=None): + super(HalLCDNumber, self).__init__(parent) + + self._in_pin = None + self._enable_pin = None + self._format = "%5.3f" + + self.setDigitCount(6) + self.setSegmentStyle(QLCDNumber.Flat) + + self.setValue(0.0) + + def setValue(self, val): + self.display(self._format % val) + + @Property(str) + def numberFormat(self): + """Number Format Property + + Args: + fmt (str) : A valid python style format string. Defaults to ``%i``. + """ + return self._format + + @numberFormat.setter + def numberFormat(self, fmt): + self._format = fmt + self.setValue(0) + +
[docs] def initialize(self): + comp = hal.getComponent() + obj_name = self.getPinBaseName() + + # add lcd-dro.in HAL pin + self._in_pin = comp.addPin(obj_name + ".in-f", "float", "in") + self.setValue(self._in_pin.value) + self._in_pin.valueChanged.connect(self.setValue) + + # add lcd-dro.in HAL pin + self._in_pin = comp.addPin(obj_name + ".in-i", "s32", "in") + self.setValue(self._in_pin.value) + self._in_pin.valueChanged.connect(self.setValue)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/hal_widgets/hal_led.html b/_modules/qtpyvcp/widgets/hal_widgets/hal_led.html new file mode 100644 index 000000000..bcd05158a --- /dev/null +++ b/_modules/qtpyvcp/widgets/hal_widgets/hal_led.html @@ -0,0 +1,192 @@ + + + + + + qtpyvcp.widgets.hal_widgets.hal_led — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.hal_widgets.hal_led

+
+from qtpyvcp import hal
+from qtpyvcp.widgets import HALWidget
+from qtpyvcp.widgets.base_widgets.led_widget import LEDWidget
+
+# Setup logging
+from qtpyvcp.utilities.logger import getLogger
+log = getLogger(__name__)
+
+
+
[docs]class HalLedIndicator(LEDWidget, HALWidget): + """HAL LED + + LED for indicated the state of `bit` HAL pins. + + .. table:: Generated HAL Pins + + ========================= ========= ========= + HAL Pin Name Type Direction + ========================= ========= ========= + qtpyvcp.led.on bit in + qtpyvcp.led.flash bit in + qtpyvcp.led.flash-rate u32 out + ========================= ========= ========= + """ + def __init__(self, parent=None): + super(HalLedIndicator, self).__init__(parent) + + self._value_pin = None + self._enabled_pin = None + +
[docs] def initialize(self): + comp = hal.getComponent() + obj_name = self.getPinBaseName() + + # add led.on HAL pin + self._on_pin = comp.addPin(obj_name + ".on", "bit", "in") + # self._on_pin.value = self.isO() + self._on_pin.valueChanged.connect(lambda state: self.setState(state)) + + # add led.flash HAL pin + self._flash_pin = comp.addPin(obj_name + ".flash", "bit", "in") + self._flash_pin.valueChanged.connect(lambda flash: self.setFlashing(flash)) + + # add led.flash-rate HAL pin + self._flash_rate_pin = comp.addPin(obj_name + ".flash-rate", "u32", "in") + self._flash_rate_pin.valueChanged.connect(lambda rate: self.setFlashRate(rate))
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/hal_widgets/hal_led_button.html b/_modules/qtpyvcp/widgets/hal_widgets/hal_led_button.html new file mode 100644 index 000000000..0017f6bdf --- /dev/null +++ b/_modules/qtpyvcp/widgets/hal_widgets/hal_led_button.html @@ -0,0 +1,294 @@ + + + + + + qtpyvcp.widgets.hal_widgets.hal_led_button — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.hal_widgets.hal_led_button

+from qtpy.QtWidgets import QPushButton
+from qtpy.QtCore import Qt, Slot, Property, Signal, QSize
+from qtpy.QtGui import QColor
+
+from qtpyvcp.utilities.obj_status import HALStatus
+from qtpyvcp.widgets.base_widgets.led_widget import LEDWidget
+
+hal_status = HALStatus()
+
+
+
[docs]class HALLEDButton(QPushButton): + """HAL Led Button + + Button for displaying HAL pin values. + + """ + def __init__(self, parent=None): + super(HALLEDButton, self).__init__(parent) + + self._alignment = Qt.AlignRight | Qt.AlignTop + self._pin_name = '' + self._flash_pin_name = '' + # self.setCheckable(True) + self.led = LEDWidget(self) + self.led.setDiameter(14) + # self.toggled.connect(self.updateState) + # self.updateState() + self.placeLed() + + def placeLed(self): + x = 0 + y = 0 + alignment = self._alignment + ledDiameter = self.led.getDiameter() + halfLed = ledDiameter / 2 + quarterLed = ledDiameter /4 # cheap hueristic to avoid borders + + if alignment & Qt.AlignLeft: + x = quarterLed + elif alignment & Qt.AlignRight: + x = self.width() - ledDiameter - quarterLed + elif alignment & Qt.AlignHCenter: + x = (self.width()/2) - halfLed + elif alignment & Qt.AlignJustify: + x = 0 + + if alignment & Qt.AlignTop: + y = quarterLed + elif alignment & Qt.AlignBottom: + y = self.height() - ledDiameter - quarterLed + elif alignment & Qt.AlignVCenter: + y = self.height()/2 - halfLed + # print(x, y) + self.led.move(x, y) + self.updateGeometry() + +
[docs] def resizeEvent(self, event): + self.placeLed()
+ +
[docs] def update(self): + self.placeLed() + super(HALLEDButton, self).update()
+ + def updateState(self, state): + self.led.setState(state) + + def updateFlashing(self, flashing): + self.led.setFlashing(flashing) + +
[docs] def sizeHint(self): + return QSize(80, 30)
+ + def getLedDiameter(self): + return self.led.getDiameter() + + @Slot(int) + def setLedDiameter(self, value): + self.led.setDiameter(value) + self.placeLed() + + def getLedColor(self): + return self.led.getColor() + + @Slot(QColor) + def setLedColor(self, value): + self.led.setColor(value) + + def getAlignment(self): + return self._alignment + + @Slot(Qt.Alignment) + def setAlignment(self, value): + self._alignment = Qt.Alignment(value) + self.update() + + diameter = Property(int, getLedDiameter, setLedDiameter) + color = Property(QColor, getLedColor, setLedColor) + alignment = Property(Qt.Alignment, getAlignment, setAlignment) + + @Property(str) + def flashPinName(self): + """The `actionName` property for setting the action the button + should trigger from within QtDesigner. + + Returns: + str : The action name. + """ + return self._flash_pin_name + + @flashPinName.setter + def flashPinName(self, flash_pin_name): + """Sets the name of the action the button should trigger and + binds the widget to that action. + + Args: + action_name (str) : A fully qualified action name. + """ + self._flash_pin_name = flash_pin_name + try: + hal_pin = hal_status.getHALPin(flash_pin_name) + except ValueError: + return + hal_pin.connect(self.updateState) + + + @Property(str) + def pinName(self): + """The `actionName` property for setting the action the button + should trigger from within QtDesigner. + + Returns: + str : The action name. + """ + return self._pin_name + + @pinName.setter + def pinName(self, pin_name): + """Sets the name of the action the button should trigger and + binds the widget to that action. + + Args: + action_name (str) : A fully qualified action name. + """ + self._pin_name = pin_name + try: + hal_pin = hal_status.getHALPin(pin_name) + except ValueError: + return + hal_pin.connect(self.updateState)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/hal_widgets/hal_plot.html b/_modules/qtpyvcp/widgets/hal_widgets/hal_plot.html new file mode 100644 index 000000000..b90795985 --- /dev/null +++ b/_modules/qtpyvcp/widgets/hal_widgets/hal_plot.html @@ -0,0 +1,629 @@ + + + + + + qtpyvcp.widgets.hal_widgets.hal_plot — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.hal_widgets.hal_plot

+
+import os
+
+from qtpy.QtGui import QColor
+from qtpy.QtWidgets import *
+from qtpy.QtCore import Property, Signal, Slot, QTime, QTimer, Qt
+from collections import deque
+
+import pyqtgraph as pg
+import numpy as np
+
+from qtpyvcp import hal
+from qtpyvcp.widgets import HALWidget
+
+IN_DESIGNER = os.getenv('DESIGNER', False)
+
+
+class TimeAxisItem(pg.AxisItem):
+    """Internal timestamp for x-axis"""
+    def __init__(self, *args, **kwargs):
+        super(TimeAxisItem, self).__init__(*args, **kwargs)
+
+    def tickStrings(self, values, scale, spacing):
+        """Function overloading the weak default version to provide timestamp"""
+        return [QTime().currentTime().addMSecs(int(value)).toString('mm:ss') for value in values]
+
+
+
[docs]class HalPlot(QWidget, HALWidget): + """HAL Plot + + Plots HAL pin values over time, similar to HAL scope. + Up to four HAL pin values can be plotted. + + .. table:: Generated HAL Pins + + ================================== =========== ========= + HAL Pin Name Type Direction + ================================== =========== ========= + qtpyvcp.pinBaseName.seriesXname.in float in + ================================== =========== ========= + + both pinBaseName and seriesXname can be set in the property editor in QtDesigner. + """ + + def __init__(self, parent=None): + super(HalPlot, self).__init__(parent) + + # HAL sampling frequency parameters + self._frequency = 1 # Hz + self._timeWindow = 600 # seconds + + # Internal timestamp for x-axis - data values are ms from when "timestamp" was started + self.timestamp = QTime() + self.timestamp.start() + + self._legend = False + + self._yAxisLabel = 'y label' + self._yAxisUnits = 'y units' + self._minY = 0 + self._maxY = 1 + + self._s1enable = True + self._s1name = "Series 1" + self._s1colour = QColor('red') + self._s1width = 1 + self._s1style = Qt.SolidLine + self._s1_pin = None + + self._s2enable = False + self._s2name = "Series 2" + self._s2colour = QColor('blue') + self._s2width = 1 + self._s2style = Qt.SolidLine + self._s2_pin = None + + self._s3enable = False + self._s3name = "Series 3" + self._s3colour = QColor('green') + self._s3width = 1 + self._s3style = Qt.SolidLine + self._s3_pin = None + + self._s4enable = False + self._s4name = "Series 4" + self._s4colour = QColor('yellow') + self._s4width = 1 + self._s4style = Qt.SolidLine + self._s4_pin = None + + # PyQtGraph stuff + self.graph = pg.GraphicsLayoutWidget() + + self.yAxis = pg.AxisItem(orientation='left') + self.yAxis.setLabel(self._yAxisLabel, units=self._yAxisUnits) + self.yAxis.setGrid(125) + + self.plot = self.graph.addPlot(axisItems={'bottom': TimeAxisItem(orientation='bottom'), 'left': self.yAxis}) + self.plot.setYRange(self._minY, self._maxY, padding=0.0) + + self.legend = self.plot.addLegend() + + self.p1 = pg.PlotCurveItem(name=self._s1name) + self.p2 = pg.PlotCurveItem(name=self._s2name) + self.p3 = pg.PlotCurveItem(name=self._s3name) + self.p4 = pg.PlotCurveItem(name=self._s4name) + + self.setSeries() + self.setData() + + self.Vlayout = QVBoxLayout(self) + self.Vlayout.addWidget(self.graph) + + # HAL stuff + self._typ = "float" + self._fmt = "%s" + + if IN_DESIGNER: + return + + # QTimer + self.updatetimer = QTimer(self) + self.updatetimer.timeout.connect(self.updateplot) + self.updatetimer.start(self._refreshRate) + + def setSeries(self): + # first remove the legend as it does not update correnctly + try: + self.legend.scene().removeItem(self.legend) + except: + pass + + # remove all plot items + self.plot.clear() + + # add the legend and plot itmes + if self._legend: + self.legend = self.plot.addLegend() + + if self._s1enable: + self.p1 = pg.PlotCurveItem(name=self._s1name) + self.plot.addItem(self.p1) + self.p1.setPen(QColor(self._s1colour), width=self._s1width, style=self._s1style) + + if self._s2enable: + self.p2 = pg.PlotCurveItem(name=self._s2name) + self.plot.addItem(self.p2) + self.p2.setPen(QColor(self._s2colour), width=self._s2width, style=self._s2style) + + if self._s3enable: + self.p3 = pg.PlotCurveItem(name=self._s3name) + self.plot.addItem(self.p3) + self.p3.setPen(QColor(self._s3colour), width=self._s3width, style=self._s3style) + + if self._s4enable: + self.p4 = pg.PlotCurveItem(name=self._s4name) + self.plot.addItem(self.p4) + self.p4.setPen(QColor(self._s4colour), width=self._s4width, style=self._s4style) + + def setData(self): + # Data stuff + self._period = 1.0/self._frequency + self._refreshRate = int(self._period * 1000) # sample period in milliseconds + self._timeWindowMS = self._timeWindow * 1000 # time window in milliseconds + self._bufsize = int(self._timeWindowMS / self._refreshRate) + + # Data containers: collections.deque is list-like container with fast appends and pops on either end + self.x = np.linspace(-self.timeWindow, 0.0, self._bufsize) + self.now = self.timestamp.elapsed() + self.x_data = deque(np.linspace(self.now-self._timeWindowMS, self.now, self._bufsize),self._bufsize) + + self.s1 = np.zeros(self._bufsize, dtype=float) + self.s1_data = deque([0.0] * self._bufsize, self._bufsize) + + self.s2 = np.zeros(self._bufsize, dtype=float) + self.s2_data = deque([0.0] * self._bufsize, self._bufsize) + + self.s3 = np.zeros(self._bufsize, dtype=float) + self.s3_data = deque([0.0] * self._bufsize, self._bufsize) + + self.s4 = np.zeros(self._bufsize, dtype=float) + self.s4_data = deque([0.0] * self._bufsize, self._bufsize) + + def updateplot(self): + self.x_data.append(self.timestamp.elapsed()) + self.x[:] = self.x_data + + if self._s1enable: + self.s1_data.append(self._s1_pin.value) + self.s1[:] = self.s1_data + self.p1.setData(self.x, self.s1) + + if self._s2enable: + self.s2_data.append(self._s2_pin.value) + self.s2[:] = self.s2_data + self.p2.setData(self.x, self.s2) + + if self._s3enable: + self.s3_data.append(self._s3_pin.value) + self.s3[:] = self.s3_data + self.p3.setData(self.x, self.s3) + + if self._s4enable: + self.s4_data.append(self._s4_pin.value) + self.s4[:] = self.s4_data + self.p4.setData(self.x, self.s4) + + def setyAxis(self): + self.yAxis.setLabel(self._yAxisLabel, units=self._yAxisUnits) + + def setYRange(self): + self.plot.setYRange(self._minY, self._maxY, padding = 0.0) + + + + @Property(int) + def frequency(self): + return self._frequency + + @frequency.setter + def frequency(self, frequency): + self._frequency = frequency + return self.setData() + + @Property(int) + def timeWindow(self): + return self._timeWindow + + @timeWindow.setter + def timeWindow(self, timeWindow): + self._timeWindow = timeWindow + return self.setData() + + @Property(str) + def yAxisLabel(self): + return self._yAxisLabel + + @yAxisLabel.setter + def yAxisLabel(self, yAxisLabel): + self._yAxisLabel = yAxisLabel + return self.setyAxis() + + @Property(str) + def yAxisUnits(self): + return self._yAxisUnits + + @yAxisUnits.setter + def yAxisUnits(self, yAxisUnits): + self._yAxisUnits = yAxisUnits + return self.setyAxis() + + @Property(float) + def minYRange(self): + return self._minY + + @minYRange.setter + def minYRange(self, minY): + self._minY = minY + return self.setYRange() + + @Property(float) + def maxYRange(self): + return self._maxY + + @maxYRange.setter + def maxYRange(self, maxY): + self._maxY = maxY + return self.setYRange() + + + # Legend propterties + @Property(bool) + def legendenable(self): + return self._legend + + @legendenable.setter + def legendenable(self, legendenable): + self._legend = legendenable + self.setSeries() + + + # Series 1 properties + @Property(bool) + def series1enable(self): + return self._s1enable + + @series1enable.setter + def series1enable(self, series1enable): + self._s1enable = series1enable + self.setSeries() + + @Property(str) + def series1name(self): + return self._s1name + + @series1name.setter + def series1name(self, series1name): + self._s1name = series1name + self.setSeries() + + @Property(QColor) + def series1colour(self): + return self._s1colour + + @series1colour.setter + def series1colour(self, series1colour): + self._s1colour = series1colour + self.setSeries() + + @Property(int) + def series1width(self): + return self._s1width + + @series1width.setter + def series1width(self, series1width): + self._s1width = series1width + self.setSeries() + + @Property(Qt.PenStyle) + def series1style(self): + return self._s1style + + @series1style.setter + def series1style(self, series1style): + self._s1style = series1style + self.setSeries() + + + # Series 2 properties + @Property(bool) + def series2enable(self): + return self._s2enable + + @series2enable.setter + def series2enable(self, series2enable): + self._s2enable = series2enable + self.setSeries() + + @Property(str) + def series2name(self): + return self._s2name + + @series2name.setter + def series2name(self, series2name): + self._s2name = series2name + self.setSeries() + + @Property(QColor) + def series2colour(self): + return self._s2colour + + @series2colour.setter + def series2colour(self, series2colour): + self._s2colour = series2colour + self.setSeries() + + @Property(int) + def series2width(self): + return self._s2width + + @series2width.setter + def series2width(self, series2width): + self._s2width = series2width + self.setSeries() + + @Property(Qt.PenStyle) + def series2style(self): + return self._s2style + + @series2style.setter + def series2style(self, series2style): + self._s2style = series2style + self.setSeries() + + + # Series 3 properties + @Property(bool) + def series3enable(self): + return self._s3enable + + @series3enable.setter + def series3enable(self, series3enable): + self._s3enable = series3enable + self.setSeries() + + @Property(str) + def series3name(self): + return self._s3name + + @series3name.setter + def series3name(self, series3name): + self._s3name = series3name + self.setSeries() + + @Property(QColor) + def series3colour(self): + return self._s3colour + + @series3colour.setter + def series3colour(self, series3colour): + self._s3colour = series3colour + self.setSeries() + + @Property(int) + def series3width(self): + return self._s3width + + @series3width.setter + def series3width(self, series3width): + self._s3width = series3width + self.setSeries() + + @Property(Qt.PenStyle) + def series3style(self): + return self._s3style + + @series3style.setter + def series3style(self, series3style): + self._s3style = series3style + self.setSeries() + + + # Series 4 properties + @Property(bool) + def series4enable(self): + return self._s4enable + + @series4enable.setter + def series4enable(self, series4enable): + self._s4enable = series4enable + self.setSeries() + + @Property(str) + def series4name(self): + return self._s4name + + @series4name.setter + def series4name(self, series4name): + self._s4name = series4name + self.setSeries() + + @Property(QColor) + def series4colour(self): + return self._s4colour + + @series4colour.setter + def series4colour(self, series4colour): + self._s4colour = series4colour + self.setSeries() + + @Property(int) + def series4width(self): + return self._s4width + + @series4width.setter + def series4width(self, series4width): + self._s4width = series4width + self.setSeries() + + @Property(Qt.PenStyle) + def series4style(self): + return self._s4style + + @series4style.setter + def series4style(self, series4style): + self._s4style = series4style + self.setSeries() + + def initialize(self): + comp = hal.getComponent() + obj_name = self.getPinBaseName() + + # add HAL pins + if self._s1enable: + self._s1_pin = comp.addPin(obj_name + "." + self._s1name.replace(' ', ''), self._typ, "in") + + if self._s2enable: + self._s2_pin = comp.addPin(obj_name + "." + self._s2name.replace(' ', ''), self._typ, "in") + + if self._s3enable: + self._s3_pin = comp.addPin(obj_name + "." + self._s3name.replace(' ', ''), self._typ, "in") + + if self._s4enable: + self._s4_pin = comp.addPin(obj_name + "." + self._s4name.replace(' ', ''), self._typ, "in")
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/hal_widgets/hal_slider.html b/_modules/qtpyvcp/widgets/hal_widgets/hal_slider.html new file mode 100644 index 000000000..7a173dd16 --- /dev/null +++ b/_modules/qtpyvcp/widgets/hal_widgets/hal_slider.html @@ -0,0 +1,241 @@ + + + + + + qtpyvcp.widgets.hal_widgets.hal_slider — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.hal_widgets.hal_slider

+
+from qtpy.QtWidgets import QSlider
+
+from qtpyvcp import hal
+from qtpyvcp.widgets import HALWidget
+
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.plugins import getPlugin
+
+LOG = getLogger(__name__)
+STATUS = getPlugin('status')
+
+
[docs]class HalSlider(QSlider, HALWidget): + """HAL Slider + + Slider for setting `u32` or `float` HAL pin values. + + .. table:: Generated HAL Pins + + ========================= ===== ========= + HAL Pin Name Type Direction + ========================= ===== ========= + qtpyvcp.slider.enable bit in + qtpyvcp.slider.out-i u32 out + qtpyvcp.slider.out-f float out + ========================= ===== ========= + """ + def __init__(self, parent=None): + super(HalSlider, self).__init__(parent) + + self._enable_pin = None + self._s32_value_pin = None + self._float_value_pin = None + + self._signed_int = True + + self.valueChanged.connect(self.onValueChanged) + +
[docs] def mousePressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept mouse Press Event') + event.accept() + return + super().mousePressEvent(event)
+ +
[docs] def mouseReleaseEvent(self, event): + if STATUS.isLocked(): + LOG.debug('Accept mouse Release Event') + event.accept() + return + super().mouseReleaseEvent(event)
+ +
[docs] def keyPressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept keyPressEvent Event') + event.accept() + return + super().keyPressEvent(event)
+ +
[docs] def keyReleaseEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept keyReleaseEvent Event') + event.accept() + return + super().keyReleaseEvent(event)
+ + def onValueChanged(self, val): + if self._s32_value_pin is not None: + self._s32_value_pin.value = val + self._float_value_pin.value = val / 100.0 + +
[docs] def mouseDoubleClickEvent(self, event): + if STATUS.isLocked(): + LOG.debug('Skip HAL mouseDoubleClickEvent') + return + self.setValue(100)
+ +
[docs] def initialize(self): + comp = hal.getComponent() + obj_name = self.getPinBaseName() + + # add slider.enable HAL pin + self._enable_pin = comp.addPin(obj_name + ".enable", "bit", "in") + self._enable_pin.value = self.isEnabled() + self._enable_pin.valueChanged.connect(self.setEnabled) + + # add slider.percent HAL pin + self._s32_value_pin = comp.addPin(obj_name + ".out-i", "u32", "out") + self._s32_value_pin.value = self.value() + + # add slider.scale HAL pin + self._float_value_pin = comp.addPin(obj_name + ".out-f", "float", "out") + self._float_value_pin.value = self.value() / 100.0
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/hal_widgets/hal_spinbox.html b/_modules/qtpyvcp/widgets/hal_widgets/hal_spinbox.html new file mode 100644 index 000000000..a428c7c3b --- /dev/null +++ b/_modules/qtpyvcp/widgets/hal_widgets/hal_spinbox.html @@ -0,0 +1,252 @@ + + + + + + qtpyvcp.widgets.hal_widgets.hal_spinbox — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.hal_widgets.hal_spinbox

+
+from qtpy.QtWidgets import QSpinBox
+from qtpy.QtCore import QEvent
+
+from qtpyvcp import hal
+from qtpyvcp.widgets import HALWidget
+
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.plugins import getPlugin
+
+LOG = getLogger(__name__)
+STATUS = getPlugin('status')
+
+
[docs]class HalQSpinBox(QSpinBox, HALWidget): + """HAL SpinBox + + SpinBox for displaying and setting `u32` and `s32` HAL pin values. + + .. table:: Generated HAL Pins + + ========================= ========= ========= + HAL Pin Name Type Direction + ========================= ========= ========= + qtpyvcp.spinbox.enable s32 | u32 in + qtpyvcp.spinbox.in s32 | u32 in + qtpyvcp.spinbox.out s32 | u32 out + ========================= ========= ========= + + Note: + If the ``minimum`` value property is set to 0 or greater a u32 HAL pin will + be created, if the ``minumum`` value is less than 0 then a s32 HAL pin will + be created. + """ + def __init__(self, parent=None): + super(HalQSpinBox, self).__init__(parent) + + self._value_pin = None + self._enabled_pin = None + + self._signed_int = True + + self.valueChanged.connect(self.onCheckedStateChanged) + +
[docs] def mousePressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept mouse Press Event') + event.accept() + return + super().mousePressEvent(event)
+ +
[docs] def mouseReleaseEvent(self, event): + if STATUS.isLocked(): + LOG.debug('Accept mouse Release Event') + event.accept() + return + super().mouseReleaseEvent(event)
+ +
[docs] def keyPressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept keyPressEvent Event') + event.accept() + return + super().keyPressEvent(event)
+ +
[docs] def keyReleaseEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept keyReleaseEvent Event') + event.accept() + return + super().keyReleaseEvent(event)
+ +
[docs] def changeEvent(self, event): + super(HalQSpinBox, self).changeEvent(event) + if event == QEvent.EnabledChange and self._enabled_pin is not None: + self._enabled_pin.value = self.isEnabled()
+ + def onCheckedStateChanged(self, checked): + if STATUS.isLocked(): + LOG.debug('Skip HAL onCheckedStateChanged') + return + if self._value_pin is not None: + self._value_pin.value = checked + +
[docs] def initialize(self): + comp = hal.getComponent() + obj_name = self.getPinBaseName() + + if self.minimum() < 0: + pin_typ = 's32' + else: + pin_typ = 'u32' + + # add spinbox.enable HAL pin + self._enabled_pin = comp.addPin(obj_name + ".enable", "bit", "in") + self._enabled_pin.value = self.isEnabled() + self._enabled_pin.valueChanged.connect(self.setEnabled) + + # add spinbox.out HAL pin + self._value_pin = comp.addPin(obj_name + ".out", pin_typ, "out") + self._value_pin.value = self.value() + + # add spinbox.in HAL pin + self._set_value_pin = comp.addPin(obj_name + ".in", pin_typ, "in") + self._set_value_pin.valueChanged.connect(self.setValue)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/input_widgets/action_combobox.html b/_modules/qtpyvcp/widgets/input_widgets/action_combobox.html new file mode 100644 index 000000000..b9b0f4b97 --- /dev/null +++ b/_modules/qtpyvcp/widgets/input_widgets/action_combobox.html @@ -0,0 +1,223 @@ + + + + + + qtpyvcp.widgets.input_widgets.action_combobox — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.input_widgets.action_combobox

+from qtpy.QtWidgets import QComboBox
+from qtpy.QtCore import Property
+
+from qtpyvcp.actions import bindWidget
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.plugins import getPlugin
+
+
+LOG = getLogger(__name__)
+STATUS = getPlugin('status')
+
+
+
[docs]class ActionComboBox(QComboBox): + """General purpose combobox for triggering QtPyVCP actions. + + Args: + parent (QWidget) : The parent widget of the combobox, or None. + + Attributes: + _action_name (str) : The fully qualified name of the action the + combobox triggers when the selection is changed. + """ + def __init__(self, parent=None): + super(ActionComboBox, self).__init__(parent) + + self._action_name = '' + + @Property(str) + def actionName(self): + """The `actionName` property for setting the action the combobox + should trigger from within QtDesigner. + + Returns: + str : The action name. + """ + return self._action_name + + @actionName.setter + def actionName(self, action_name): + """Sets the name of the action the button should trigger and + binds the widget to that action. + + Args: + action_name (str) : A fully qualified action name. + """ + self._action_name = action_name + bindWidget(self, action_name) + +
[docs] def mousePressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept mouse Press Event') + event.accept() + return + super().mousePressEvent(event)
+ +
[docs] def mouseReleaseEvent(self, event): + if STATUS.isLocked(): + LOG.debug('Accept mouse Release Event') + event.accept() + return + super().mouseReleaseEvent(event)
+ +
[docs] def keyPressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept keyPressEvent Event') + event.accept() + return + super().keyPressEvent(event)
+ +
[docs] def keyReleaseEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept keyReleaseEvent Event') + event.accept() + return + super().keyReleaseEvent(event)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/input_widgets/action_dial.html b/_modules/qtpyvcp/widgets/input_widgets/action_dial.html new file mode 100644 index 000000000..23ab7e109 --- /dev/null +++ b/_modules/qtpyvcp/widgets/input_widgets/action_dial.html @@ -0,0 +1,213 @@ + + + + + + qtpyvcp.widgets.input_widgets.action_dial — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.input_widgets.action_dial

+from qtpy.QtWidgets import QDial
+from qtpy.QtCore import Property
+
+from qtpyvcp.actions import bindWidget
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.plugins import getPlugin
+
+
+LOG = getLogger(__name__)
+STATUS = getPlugin('status')
+
+
+
[docs]class ActionDial(QDial): + """docstring for ActionDial.""" + def __init__(self, parent=None): + super(ActionDial, self).__init__(parent) + + self._action_name = '' + + @Property(str) + def actionName(self): + """The fully qualified name of the action the dial should trigger. + + Returns: + str : The action name. + """ + return self._action_name + + @actionName.setter + def actionName(self, action_name): + """Sets the name of the action the dial should trigger. + + Args: + action_name (str) : A fully qualified action name. + """ + self._action_name = action_name + bindWidget(self, action_name) + +
[docs] def mousePressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept mouse Press Event') + event.accept() + return + super().mousePressEvent(event)
+ +
[docs] def mouseReleaseEvent(self, event): + if STATUS.isLocked(): + LOG.debug('Accept mouse Release Event') + event.accept() + return + super().mouseReleaseEvent(event)
+ +
[docs] def keyPressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept keyPressEvent Event') + event.accept() + return + super().keyPressEvent(event)
+ +
[docs] def keyReleaseEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept keyReleaseEvent Event') + event.accept() + return + super().keyReleaseEvent(event)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/input_widgets/action_slider.html b/_modules/qtpyvcp/widgets/input_widgets/action_slider.html new file mode 100644 index 000000000..3e14e98fc --- /dev/null +++ b/_modules/qtpyvcp/widgets/input_widgets/action_slider.html @@ -0,0 +1,222 @@ + + + + + + qtpyvcp.widgets.input_widgets.action_slider — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.input_widgets.action_slider

+import linuxcnc
+from qtpy.QtWidgets import QSlider
+from qtpy.QtCore import Slot, Property
+
+from qtpyvcp.actions import bindWidget
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.plugins import getPlugin
+
+
+LOG = getLogger(__name__)
+STATUS = getPlugin('status')
+
+
+
[docs]class ActionSlider(QSlider): + """docstring for ActionSlider.""" + def __init__(self, parent=None): + super(ActionSlider, self).__init__(parent) + + self._action_name = '' + + @Property(str) + def actionName(self): + """The fully qualified name of the action the slider should trigger. + + Returns: + str : The action name. + """ + return self._action_name + + @actionName.setter + def actionName(self, action_name): + """Sets the name of the action the slider should trigger. + + Args: + action_name (str) : A fully qualified action name. + """ + self._action_name = action_name + bindWidget(self, action_name) + +
[docs] def mouseDoubleClickEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept mouse Double Click Event') + event.accept() + return + self.setValue(100)
+ +
[docs] def mousePressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept mouse Press Event') + event.accept() + return + super().mousePressEvent(event)
+ +
[docs] def mouseReleaseEvent(self, event): + if STATUS.isLocked(): + LOG.debug('Accept mouse Release Event') + event.accept() + return + super().mouseReleaseEvent(event)
+ +
[docs] def keyPressEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept keyPressEvent Event') + event.accept() + return + super().keyPressEvent(event)
+ +
[docs] def keyReleaseEvent(self, event): + # Test for UI LOCK and consume event but do nothing if LOCK in place + if STATUS.isLocked(): + LOG.debug('Accept keyReleaseEvent Event') + event.accept() + return + super().keyReleaseEvent(event)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/input_widgets/dro_line_edit.html b/_modules/qtpyvcp/widgets/input_widgets/dro_line_edit.html new file mode 100644 index 000000000..3708a97b8 --- /dev/null +++ b/_modules/qtpyvcp/widgets/input_widgets/dro_line_edit.html @@ -0,0 +1,220 @@ + + + + + + qtpyvcp.widgets.input_widgets.dro_line_edit — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.input_widgets.dro_line_edit

+"""
+DROLineEdit
+-----------
+
+"""
+
+from qtpy.QtCore import Qt, Property
+from qtpyvcp.widgets.base_widgets.eval_line_edit import EvalLineEdit
+
+from qtpyvcp.widgets.base_widgets.dro_base_widget import DROBaseWidget, Axis, LatheMode
+from qtpyvcp.actions.machine_actions import issue_mdi
+
+from qtpyvcp.utilities import logger
+LOG = logger.getLogger(__name__)
+
+
+
[docs]class DROLineEdit(EvalLineEdit, DROBaseWidget): + """DROLineEdit + + DRO that supports typing in desired position to set work coordinate offset. + """ + + def __init__(self, parent=None): + super(DROLineEdit, self).__init__(parent) + + self.returnPressed.connect(self.onReturnPressed) + self.editingFinished.connect(self.onEditingFinished) + self.textEdited.connect(self.setCurrentPos) + + issue_mdi.bindOk(widget=self) + + self.last_commanded_pos = self.status.stat.position[self._anum] + + def onReturnPressed(self): + try: + val = float(self.text().strip().replace('mm', '').replace('in', '')) + g5x_index = self.status.stat.g5x_index + axis = 'XYZABCUVW'[self._anum] + + if self._is_lathe and self._anum == Axis.X: + if self._lathe_mode == LatheMode.Diameter and not self._g7_active: + val = val / 2 + elif self._lathe_mode == LatheMode.Radius and self._g7_active: + val = val * 2 + + cmd = 'G10 L20 P{0:d} {1}{2:.12f}'.format(g5x_index, axis, val) + issue_mdi(cmd) + except Exception: + LOG.exception("Error setting work coordinate offset.") + + self.blockSignals(True) + self.clearFocus() + self.blockSignals(False) + + def onEditingFinished(self): + self.updateValue() + +
[docs] def updateValue(self, pos=None): + if self.pos.report_actual_pos and self.isModified(): + # Only update if commanded position changes when user is editing. + if self.last_commanded_pos == self.status.stat.position[self._anum]: + return None + + super(DROLineEdit, self).updateValue(pos)
+ + def setCurrentPos(self): + # Run once user starts editing field. + self.isEdited = True + self.last_commanded_pos = self.status.stat.position[self._anum] + +
[docs] def keyPressEvent(self, e): + if e.key() == Qt.Key_Escape: + super(DROLineEdit, self).updateValue() + else: + super(DROLineEdit, self).keyPressEvent(e)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/input_widgets/file_system.html b/_modules/qtpyvcp/widgets/input_widgets/file_system.html new file mode 100644 index 000000000..10053a512 --- /dev/null +++ b/_modules/qtpyvcp/widgets/input_widgets/file_system.html @@ -0,0 +1,675 @@ + + + + + + qtpyvcp.widgets.input_widgets.file_system — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.input_widgets.file_system

+import os
+import subprocess
+
+import linuxcnc
+import psutil
+
+
+from dateutil.parser import parse
+from pyudev.pyqt5 import MonitorObserver
+from pyudev import Context, Monitor, Devices
+
+from qtpy.QtCore import Qt, Slot, Property, Signal, QFile, QFileInfo, QDir, QIODevice
+from qtpy.QtWidgets import QFileSystemModel, QComboBox, QTableView, QMessageBox, \
+    QApplication, QAbstractItemView, QInputDialog, QLineEdit
+
+from qtpyvcp.plugins import getPlugin
+from qtpyvcp.actions.program_actions import load as loadProgram
+from qtpyvcp.utilities.info import Info
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.utilities.encode_utils import allEncodings
+from qtpyvcp.lib.decorators import deprecated
+
+
+LOG = getLogger(__name__)
+
+IN_DESIGNER = os.getenv('DESIGNER') != None
+
+
+class TableType(object):
+    Local = 0
+    Remote = 1
+
+
+
[docs]class RemovableDeviceComboBox(QComboBox): + """ComboBox for choosing from a list of removable devices.""" + usbPresent = Signal(bool) + currentPathChanged = Signal(str) + currentDeviceEjectable = Signal(bool) + + def __init__(self, parent=None): + super(RemovableDeviceComboBox, self).__init__(parent) + + self._first_show = True + + self.setSizeAdjustPolicy(QComboBox.AdjustToContents) + + self._file_locations = getPlugin('file_locations') + self._file_locations.removable_devices.notify(self.onRemovableDevicesChanged) + self._file_locations.new_device.notify(self.onNewDeviceAdded) + + self.info = Info() + self._program_prefix = self.info.getProgramPrefix() + + self.currentTextChanged.connect(self.onCurrentTextChanged) + + # initialize device list + self.onRemovableDevicesChanged(self._file_locations.removable_devices.value) + +
[docs] def showEvent(self, event=None): + if self._first_show: + self._first_show = False + self.setCurrentText(self._file_locations.default_location) + data = self.currentData() or {} + self.currentDeviceEjectable.emit(data.get('removable', False)) + super(RemovableDeviceComboBox, self).showEvent(event)
+ + def onCurrentTextChanged(self, text): + data = self.currentData() + if data: + self.currentPathChanged.emit(data.get('path', '/')) + self.currentDeviceEjectable.emit(data.get('removable', False)) + + def onRemovableDevicesChanged(self, devices): + + self.blockSignals(True) + + self.clear() + + for label, path in list(self._file_locations.local_locations.items()): + self.addItem(label, {'path': os.path.expanduser(path)}) + + self.insertSeparator(100) + + if devices: + for devices_node, device_data in list(devices.items()): + self.addItem(device_data.get('label', 'Unknown'), device_data) + + self.blockSignals(False) + + def onNewDeviceAdded(self, device): + if device: + self.setCurrentText(device.get('label')) + else: + self.setCurrentText(self._file_locations.default_location) + + @Slot() + def ejectDevice(self): + data = self.currentData() + if data: + self._file_locations.ejectDevice(data.get('device'))
+ +
[docs]class QtpyVCPQFileSystemModel(QFileSystemModel): + +
[docs] def data(self, index, role=Qt.DisplayRole): + # Column nº 3 is datem, align it to right + col = index.column() + + if role == Qt.DisplayRole: + data = QFileSystemModel.data(self, index, role) + if col == 3: + date = parse(data) + return f"{date:%m/%d/%y %I:%M %p}" + else: + return f"{data}" + + if role == Qt.TextAlignmentRole: + if col == 3: + return Qt.AlignVCenter | Qt.AlignRight + + return QFileSystemModel.data(self, index, role)
+ +
[docs]class FileSystemTable(QTableView, TableType): + + if IN_DESIGNER: + from PyQt5.QtCore import Q_ENUMS + Q_ENUMS(TableType) + + gcodeFileSelected = Signal(bool) + filePreviewText = Signal(str) + fileNamePreviewText = Signal(str) + transferFileRequest = Signal(str) + rootChanged = Signal(str) + atDeviceRoot = Signal(bool) + + def __init__(self, parent=None): + super(FileSystemTable, self).__init__(parent) + + self._table_type = TableType.Local + self._hidden_columns = '' + self._name_columns_width = 0 + self._fixed_name_column = False + + # This prevents doing unneeded initialization + # when QtDesginer loads the plugin. + if parent is None: + return + + self.parent = parent + self.path_data = dict() + + self.selected_row = None + self.clipboard = QApplication.clipboard() + + self.model = QtpyVCPQFileSystemModel() + self.model.setReadOnly(True) + self.model.setFilter(QDir.AllDirs | QDir.NoDotAndDotDot | QDir.AllEntries) + + self.setModel(self.model) + + self.verticalHeader().hide() + self.horizontalHeader().setStretchLastSection(True) + self.setAlternatingRowColors(True) + self.setSelectionBehavior(QAbstractItemView.SelectRows) + + self.selection_model = self.selectionModel() + self.selection_model.selectionChanged.connect(self.onSelectionChanged) + + # open selected item on double click or enter pressed + self.activated.connect(self.openSelectedItem) + + self.info = Info() + self.nc_file_editor = self.info.getEditor() + self.nc_file_dir = self.info.getProgramPrefix() + self.nc_file_exts = self.info.getProgramExtentions() + + self.setRootPath(self.nc_file_dir) + + self.model.rootPathChanged.connect(self.onRootPathChanged) + +
[docs] def showEvent(self, event=None): + root_path = self.model.rootPath() + self.rootChanged.emit(root_path) + self.atDeviceRoot.emit(os.path.ismount(root_path))
+ + def onRootPathChanged(self, path): + self.atDeviceRoot.emit(os.path.ismount(path)) + + def onSelectionChanged(self, selected, deselected): + + if len(selected) == 0: + return + + index = selected.indexes()[0] + path = self.model.filePath(index) + + if os.path.isfile(path): + self.gcodeFileSelected.emit(True) + encodings = allEncodings() + enc = None + for enc in encodings: + try: + with open(path, 'r', encoding=enc) as f: + content = f.read() + break + except Exception as e: + # LOG.debug(e) + LOG.info(f"File encoding doesn't match {enc}, trying others") + LOG.info(f"File encoding: {enc}") + self.filePreviewText.emit(content) + self.fileNamePreviewText.emit(path) + else: + self.gcodeFileSelected.emit(False) + self.filePreviewText.emit('') + self.fileNamePreviewText.emit('') + +
[docs] @Slot() + def openSelectedItem(self, index=None): + """If ngc file, opens in LinuxCNC, if dir displays dir.""" + if index is None: + selection = self.getSelection() + if selection is None: + return + index = selection[0] + + path = self.model.filePath(self.rootIndex()) + name = self.model.filePath(index) + + absolute_path = os.path.join(path, name) + + file_info = QFileInfo(absolute_path) + if file_info.isDir(): + self.model.setRootPath(absolute_path) + self.setRootIndex(self.model.index(absolute_path)) + self.rootChanged.emit(absolute_path) + + elif file_info.isFile(): + # if file_info.completeSuffix() not in self.nc_file_exts: + # LOG.warn("Unsuported NC program type with extention .%s", + # file_info.completeSuffix()) + loadProgram(absolute_path)
+ +
[docs] @Slot() + def editSelectedFile(self): + """Open the selected file in editor.""" + selection = self.getSelection() + if selection is not None: + path = self.model.filePath(selection[0]) + subprocess.Popen([self.nc_file_editor, path]) + return False
+ +
[docs] @Slot() + def loadSelectedFile(self): + """Loads the selected file into LinuxCNC.""" + selection = self.getSelection() + if selection is not None: + path = self.model.filePath(selection[0]) + loadProgram(path) + return True + return False
+ +
[docs] @Slot() + def selectPrevious(self): + """Select the previous item in the view.""" + selection = self.getSelection() + if selection is None: + # select last item in view + self.selectRow(self.model.rowCount(self.rootIndex()) - 1) + else: + self.selectRow(selection[0].row() - 1) + return True
+ +
[docs] @Slot() + def selectNext(self): + """Select the next item in the view.""" + selection = self.getSelection() + if selection is None: + # select first item in view + self.selectRow(0) + else: + self.selectRow(selection[-1].row() + 1) + return True
+ +
[docs] @Slot() + def rename(self): + """renames the selected file or folder""" + index = self.selectionModel().currentIndex() + path = self.model.filePath(index) + if path: + file_info = QFileInfo(path) + + if file_info.isFile(): + filename = self.rename_dialog("file") + + if filename: + q_file = QFile(path) + file_info.absolutePath() + new_path = os.path.join(file_info.absolutePath(), str(filename)) + q_file.rename(new_path) + + elif file_info.isDir(): + filename = self.rename_dialog("directory") + + if filename: + directory = QDir(path) + file_info.absolutePath() + new_path = os.path.join(file_info.absolutePath(), str(filename)) + directory.rename(path, new_path)
+ +
[docs] @Slot() + def newFile(self): + """Create a new empty file""" + path = self.model.filePath(self.rootIndex()) + new_file_path = os.path.join(path, "New File.ngc") + + count = 1 + while os.path.exists(new_file_path): + new_file_path = os.path.join(path, "New File {}.ngc".format(count)) + count += 1 + + new_file = QFile(new_file_path) + new_file.open(QIODevice.ReadWrite)
+ + @Slot() + def newFolder(self): + path = self.model.filePath(self.rootIndex()) + + new_name = 'New Folder' + + count = 1 + while os.path.exists(os.path.join(path, new_name)): + new_name = "New Folder {}".format(count) + count += 1 + + directory = QDir(path) + directory.mkpath(new_name) + directory.setPath(new_name) + + @Slot() + @deprecated(replaced_by='newFolder', + reason='for consistency with newFile method name') + def createDirectory(self): + self.newFolder() + +
[docs] @Slot() + def deleteItem(self): + """Delete the selected item (either a file or folder).""" + # ToDo: use Move2Trash, instead of deleting the file + index = self.selectionModel().currentIndex() + path = self.model.filePath(index) + if path: + file_info = QFileInfo(path) + if file_info.isFile(): + if not self.ask_dialog("Do you want to delete the selected file?"): + return + q_file = QFile(path) + q_file.remove() + + elif file_info.isDir(): + if not self.ask_dialog("Do you want to delete the selected directory?"): + return + directory = QDir(path) + directory.removeRecursively()
+ + @Slot() + @deprecated(replaced_by='deleteItem', + reason='because of unclear method name') + def deleteFile(self): + self.deleteItem() + +
[docs] @Slot(str) + def setRootPath(self, root_path): + """Sets the currently displayed path.""" + + self.rootChanged.emit(root_path) + self.model.setRootPath(root_path) + self.setRootIndex(self.model.index(root_path)) + + return True
+ +
[docs] @Slot() + def viewParentDirectory(self): + """View the parent directory of the current view.""" + + path = self.model.filePath(self.rootIndex()) + + file_info = QFileInfo(path) + directory = file_info.dir() + new_path = directory.absolutePath() + + if os.path.ismount(path): + return + + currentRoot = self.rootIndex() + + self.model.setRootPath(new_path) + self.setRootIndex(currentRoot.parent()) + self.rootChanged.emit(new_path)
+ + + @Slot() + @deprecated(replaced_by='viewParentDirectory') + def goUP(self): + self.viewParentDirecotry() + + @Slot() + def viewHomeDirectory(self): + self.setRootPath(os.path.expanduser('~/')) + + @Slot() + def viewNCFilesDirectory(self): + # ToDo: Make preset user definable + path = os.path.expanduser(self._nc_files_dir) + self.setRootPath(path) + + @Slot() + def viewPresetDirectory(self): + # ToDo: Make preset user definable + preset = os.path.expanduser(self._nc_files_dir) + self.setRootPath(preset) + + @Slot() + def doFileTransfer(self): + index = self.selectionModel().currentIndex() + path = self.model.filePath(index) + self.transferFileRequest.emit(path) + + @Slot(str) + def transferFile(self, src_path): + dest_path = self.model.filePath(self.rootIndex()) + + src_file = QFile() + src_file.setFileName(src_path) + + src_file_info = QFileInfo(src_path) + + dst_path = os.path.join(dest_path, src_file_info.fileName()) + + src_file.copy(dst_path) + +
[docs] @Slot() + def getSelection(self): + """Returns list of selected indexes, or None.""" + selection = self.selection_model.selectedIndexes() + if len(selection) == 0: + return None + return selection
+ + @Slot() + def getCurrentDirectory(self): + return self.model.rootPath() + + @Property(TableType) + def tableType(self): + return self._table_type + + @tableType.setter + def tableType(self, table_type): + self._table_type = table_type + if table_type == TableType.Local: + self.setRootPath(self.nc_file_dir) + else: + self.setRootPath('/media/') + + @Property(str) + def hiddenColumns(self): + """String of comma separated column numbers to hide.""" + return self._hidden_columns + + @hiddenColumns.setter + def hiddenColumns(self, columns): + try: + col_list = [int(c) for c in columns.split(',') if c != ''] + except: + return False + + self._hidden_columns = columns + + header = self.horizontalHeader() + for col in range(4): + if col in col_list: + header.hideSection(col) + else: + header.showSection(col) + + + @Property(bool) + def fixedNameColumn(self): + """Allows to set a fixed width defined in the nameColumnsWidth property""" + return self._fixed_name_column + + @fixedNameColumn.setter + def fixedNameColumn(self, value): + self._fixed_name_column = value + + if self._fixed_name_column: + self.setColumnWidth(0, self._fixed_name_column) + + + @Property(int) + def nameColumnsWidth(self): + """If the fixedNameColumn is enabled sets its width.""" + return self._name_columns_width + + @nameColumnsWidth.setter + def nameColumnsWidth(self, width): + self._name_columns_width = width + + if self._fixed_name_column: + self.setColumnWidth(0, self._name_columns_width) + + + def ask_dialog(self, message): + box = QMessageBox.question(self.parent, + 'Are you sure?', + message, + QMessageBox.Yes, + QMessageBox.No) + if box == QMessageBox.Yes: + return True + else: + return False + + def rename_dialog(self, data_type): + text, ok_pressed = QInputDialog.getText(self.parent, "Rename", "New {} name:".format(data_type), + QLineEdit.Normal, "") + + if ok_pressed and text != '': + return text + else: + return False
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/input_widgets/gcode_editor.html b/_modules/qtpyvcp/widgets/input_widgets/gcode_editor.html new file mode 100644 index 000000000..c690ec781 --- /dev/null +++ b/_modules/qtpyvcp/widgets/input_widgets/gcode_editor.html @@ -0,0 +1,811 @@ + + + + + + qtpyvcp.widgets.input_widgets.gcode_editor — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.input_widgets.gcode_editor

+#    Gcode display / edit widget for QT_VCP
+#    Copyright 2016 Chris Morley
+#
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program; if not, write to the Free Software
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+# This was based on
+# QScintilla sample with PyQt
+# Eli Bendersky (eliben@gmail.com)
+# Which is code in the public domain
+#
+# See also:
+# http://pyqt.sourceforge.net/Docs/QScintilla2/index.html
+# https://qscintilla.com/
+
+import sys
+import os
+
+from qtpy.QtCore import Property, QObject, Slot, QFile, QFileInfo, QTextStream, Signal
+from qtpy.QtGui import QFont, QFontMetrics, QColor
+from qtpy.QtWidgets import QInputDialog, QLineEdit, QDialog, QHBoxLayout, QVBoxLayout, QLabel, QPushButton, QCheckBox
+
+from qtpyvcp.utilities import logger
+from qtpyvcp.plugins import getPlugin
+from qtpyvcp.utilities.info import Info
+
+
+LOG = logger.getLogger(__name__)
+
+try:
+    from PyQt5.Qsci import QsciScintilla, QsciLexerCustom
+except ImportError as e:
+    LOG.critical("Can't import QsciScintilla - is package python-pyqt5.qsci installed?", exc_info=e)
+    sys.exit(1)
+
+STATUS = getPlugin('status')
+INFO = Info()
+
+
+# ==============================================================================
+# Simple custom lexer for Gcode
+# ==============================================================================
+
[docs]class GcodeLexer(QsciLexerCustom): + def __init__(self, parent=None, standalone=False): + super(GcodeLexer, self).__init__(parent) + + # This prevents doing unneeded initialization + # when QtDesginer loads the plugin. + if parent is None and not standalone: + return + + self._styles = { + 0: 'Default', + 1: 'Comment', + 2: 'Key', + 3: 'Assignment', + 4: 'Value', + } + for key, value in self._styles.items(): + setattr(self, value, key) + font = QFont() + font.setFamily('Courier') + font.setFixedPitch(True) + font.setPointSize(10) + font.setBold(True) + self.setFont(font, 2) + + # Paper sets the background color of each style of text + def setPaperBackground(self, color, style=None): + if style is None: + for i in range(0, 5): + self.setPaper(color, i) + else: + self.setPaper(color, style) + +
[docs] def description(self, style): + return self._styles.get(style, '')
+ +
[docs] def defaultColor(self, style): + if style == self.Default: + return QColor('#000000') # black + elif style == self.Comment: + return QColor('#000000') # black + elif style == self.Key: + return QColor('#0000CC') # blue + elif style == self.Assignment: + return QColor('#CC0000') # red + elif style == self.Value: + return QColor('#00CC00') # green + return QsciLexerCustom.defaultColor(self, style)
+ +
[docs] def styleText(self, start, end): + editor = self.editor() + if editor is None: + return + + # scintilla works with encoded bytes, not decoded characters. + # this matters if the source contains non-ascii characters and + # a multi-byte encoding is used (e.g. utf-8) + source = '' + if end > editor.length(): + end = editor.length() + if end > start: + if sys.hexversion >= 0x02060000: + # faster when styling big files, but needs python 2.6 + source = bytearray(end - start) + editor.SendScintilla( + editor.SCI_GETTEXTRANGE, start, end, source) + else: + source = str(editor.text()).encode('utf-8')[start:end] + if not source: + return + + # the line index will also be needed to implement folding + index = editor.SendScintilla(editor.SCI_LINEFROMPOSITION, start) + if index > 0: + # the previous state may be needed for multi-line styling + pos = editor.SendScintilla( + editor.SCI_GETLINEENDPOSITION, index - 1) + state = editor.SendScintilla(editor.SCI_GETSTYLEAT, pos) + else: + state = self.Default + + set_style = self.setStyling + self.startStyling(start, 0x1f) + + # scintilla always asks to style whole lines + for line in source.splitlines(True): + # print(line) + length = len(line) + graymode = False + msg = ('msg'.encode('utf-8') in line.lower() or 'debug'.encode('utf-8') in line.lower()) + for char in str(line): + # print(char) + if char == '(': + graymode = True + set_style(1, self.Comment) + continue + elif char == ')': + graymode = False + set_style(1, self.Comment) + continue + elif graymode: + if msg and char.lower() in ('m', 's', 'g', ',', 'd', 'e', 'b', 'u'): + set_style(1, self.Assignment) + if char == ',': msg = False + else: + set_style(1, self.Comment) + continue + elif char in ('%', '<', '>', '#', '='): + state = self.Assignment + elif char in ('[', ']'): + state = self.Value + elif char.isalpha(): + state = self.Key + elif char.isdigit(): + state = self.Default + else: + state = self.Default + set_style(1, state) + + # folding implementation goes here + index += 1
+ + +# ============================================================================== +# Base editor class +# ============================================================================== +
[docs]class EditorBase(QsciScintilla): + ARROW_MARKER_NUM = 8 + + def __init__(self, parent=None): + super(EditorBase, self).__init__(parent) + # linuxcnc defaults + self.idle_line_reset = False + # don't allow editing by default + self.setReadOnly(True) + # Set the default font + font = QFont() + font.setFamily('Courier') + font.setFixedPitch(True) + font.setPointSize(10) + self.setFont(font) + self.setMarginsFont(font) + + # Margin 0 is used for line numbers + fontmetrics = QFontMetrics(font) + self.setMarginsFont(font) + self.setMarginWidth(0, fontmetrics.width("0000") + 6) + self.setMarginLineNumbers(0, True) + self.setMarginsBackgroundColor(QColor("#cccccc")) + + # Clickable margin 1 for showing markers + self.setMarginSensitivity(1, True) + # setting marker margin width to zero make the marker highlight line + self.setMarginWidth(1, 10) + self.marginClicked.connect(self.on_margin_clicked) + self.markerDefine(QsciScintilla.RightArrow, + self.ARROW_MARKER_NUM) + self.setMarkerBackgroundColor(QColor("#ffe4e4"), + self.ARROW_MARKER_NUM) + + # Brace matching: enable for a brace immediately before or after + # the current position + # + self.setBraceMatching(QsciScintilla.SloppyBraceMatch) + + # Current line visible with special background color + self.setCaretLineVisible(True) + self.setCaretLineBackgroundColor(QColor("#ffe4e4")) + + # Set custom gcode lexer + self.lexer = GcodeLexer(self) + self.lexer.setDefaultFont(font) + self.setLexer(self.lexer) + + # default gray background + self.set_background_color('#C0C0C0') + + self.highlit = None + + # not too small + # self.setMinimumSize(200, 100) + +
[docs] def find_text_occurences(self, text): + """Return byte positions of start and end of all 'text' occurences in the document""" + + text_len = len(text) + end_pos = self.SendScintilla(QsciScintilla.SCI_GETLENGTH) + self.SendScintilla(QsciScintilla.SCI_SETTARGETSTART, 0) + self.SendScintilla(QsciScintilla.SCI_SETTARGETEND, end_pos) + + occurences = [] + + match = self.SendScintilla(QsciScintilla.SCI_SEARCHINTARGET, text_len, text) + print(match) + while match != -1: + match_end = self.SendScintilla(QsciScintilla.SCI_GETTARGETEND) + occurences.append((match, match_end)) + # -- if there's a match, the target is modified so we shift its start + # -- and restore its end -- + self.SendScintilla(QsciScintilla.SCI_SETTARGETSTART, match_end) + self.SendScintilla(QsciScintilla.SCI_SETTARGETEND, end_pos) + # -- find it again in the new (reduced) target -- + match = self.SendScintilla(QsciScintilla.SCI_SEARCHINTARGET, text_len, text) + + return occurences
+ + def highlight_occurences(self, text): + + occurences = self.find_text_occurences(text) + text_len = len(text) + self.SendScintilla(QsciScintilla.SCI_SETSTYLEBITS, 8) + for occs in occurences: + self.SendScintilla(QsciScintilla.SCI_SETINDICATORCURRENT, 0) + self.SendScintilla(QsciScintilla.SCI_INDICATORFILLRANGE, + occs[0], text_len) + + # -- this is somewhat buggy : it was meant to change the color + # -- but somewhy the colouring suddenly changes colour. + + # self.SendScintilla(Qsci.QsciScintilla.SCI_STARTSTYLING, occs[0], 0xFF) + # self.SendScintilla(Qsci.QsciScintilla.SCI_SETSTYLING, + # textLen, + # styles["HIGHLIGHT"][0]) + + self.highlit = occurences + + def clear_highlights(self): + if self.highlit is None: + return + + for occs in self.highlit: + self.SendScintilla(QsciScintilla.SCI_SETINDICATORCURRENT, 0) + self.SendScintilla(QsciScintilla.SCI_INDICATORCLEARRANGE, + occs[0], occs[1] - occs[0]) + self.highlit = None + + def text_search(self, text, from_start, highlight_all, re=False, + cs=True, wo=False, wrap=True, forward=True, + line=-1, index=-1, show=True): + + if text is not None: + if highlight_all: + self.clear_highlights() + self.highlight_occurences(text) + + if from_start: + self.setCursorPosition(0, 0) + + match = self.findFirst(text, re, cs, wo, wrap, forward, line, index, show) + + def text_replace(self, text, sub, from_start, re=False, + cs=True, wo=False, wrap=True, forward=True, + line=-1, index=-1, show=True): + + if text is not None and sub is not None: + self.clear_highlights() + self.highlight_occurences(text) + + if from_start: + self.setCursorPosition(0, 0) + + match = self.findFirst(text, re, cs, wo, wrap, forward, line, index, show) + if match: + self.replace(sub) + + def text_replace_all(self, text, sub, from_start, re=False, + cs=True, wo=False, wrap=True, forward=True, + line=-1, index=-1, show=True): + + if text is not None and sub is not None: + self.clear_highlights() + + self.SendScintilla(QsciScintilla.SCI_SETTARGETSTART, 0) + end_pos = self.SendScintilla(QsciScintilla.SCI_GETLENGTH) + self.SendScintilla(QsciScintilla.SCI_SETTARGETEND, end_pos) + + print((self.SendScintilla(QsciScintilla.SCI_SEARCHINTARGET, len(text), text))) + + # match = self.findFirst(text, re, cs, wo, wrap, forward, line, index, show) + # if match: + # self.replace(sub) + + # must set lexer paper background color _and_ editor background color it seems + def set_background_color(self, color): + self.SendScintilla(QsciScintilla.SCI_STYLESETBACK, QsciScintilla.STYLE_DEFAULT, QColor(color)) + self.lexer.setPaperBackground(QColor(color)) + + def set_margin_background_color(self, color): + self.setMarginsBackgroundColor(QColor(color)) + + def on_margin_clicked(self, nmargin, nline, modifiers): + # Toggle marker for the line the margin was clicked on + if self.markersAtLine(nline) != 0: + self.markerDelete(nline, self.ARROW_MARKER_NUM) + else: + self.markerAdd(nline, self.ARROW_MARKER_NUM)
+ + +# ============================================================================== +# Gcode widget +# ============================================================================== +
[docs]class GcodeEditor(EditorBase, QObject): + ARROW_MARKER_NUM = 8 + + somethingHasChanged = Signal(bool) + + def __init__(self, parent=None): + super(GcodeEditor, self).__init__(parent) + + self.filename = "" + self._last_filename = None + self.auto_show_mdi = True + self.last_line = None + # self.setEolVisibility(True) + + self.is_editor = False + self.text_before_edit = '' + + self.dialog = FindReplaceDialog(parent=self) + + # QSS Hack + + self.backgroundcolor = '' + self.marginbackgroundcolor = '' + + # register with the status:task_mode channel to + # drive the mdi auto show behaviour + #STATUS.task_mode.notify(self.onMdiChanged) + #self.prev_taskmode = STATUS.task_mode + + #self.cursorPositionChanged.connect(self.line_changed) + self.somethingHasChanged.emit(False) + + @Slot(bool) + def setEditable(self, state): + if state: + self.setReadOnly(False) + self.setCaretLineVisible(True) + if self.text_before_edit != '': + self.text_before_edit = self.text() + self.somethingHasChanged.emit(False) + else: + self.setReadOnly(True) + self.setCaretLineVisible(False) + self.somethingHasChanged.emit(self.text_before_edit != self.text()) + + @Slot(str) + def setFilename(self, path): + self.filename = path + + @Slot() + def save(self): + save_file = QFile(str(STATUS.file)) + + result = save_file.open(QFile.WriteOnly) + if result: + LOG.debug("---self.text(): {}".format(self.text())) + save_stream = QTextStream(save_file) + save_stream << self.text() + save_file.close() + self.text_before_edit = '' + self.somethingHasChanged.emit(False) + else: + LOG.debug("---save error") + + @Slot() + def saveAs(self): + file_name = self.save_as_dialog(self.filename) + + if file_name is False: + print("saveAs file name error") + return + self.filename = str(STATUS.file) + + original_file = QFileInfo(self.filename) + path = original_file.path() + + new_absolute_path = os.path.join(path, file_name) + new_file = QFile(new_absolute_path) + + result = new_file.open(QFile.WriteOnly) + if result: + save_stream = QTextStream(new_file) + save_stream << self.text() + new_file.close() + self.text_before_edit = '' + self.somethingHasChanged.emit(False) + + @Slot() + def find_replace(self): + self.dialog.show() + + def search_text(self, find_text, highlight_all): + from_start = False + if find_text != "": + self.text_search(find_text, from_start, highlight_all) + + def replace_text(self, find_text, replace_text): + from_start = False + if find_text != "" and replace_text != "": + self.text_replace(find_text, replace_text, from_start) + + def replace_all_text(self, find_text, replace_text): + from_start = True + if find_text != "" and replace_text != "": + self.text_replace_all(find_text, find_text, from_start) + + @Property(bool) + def is_editor(self): + return self._is_editor + + @is_editor.setter + def is_editor(self, enabled): + self._is_editor = enabled + if not self._is_editor: + STATUS.file.notify(self.load_program) + STATUS.motion_line.onValueChanged(self.highlight_line) + + # STATUS.connect('line-changed', self.highlight_line) + # if self.idle_line_reset: + # STATUS.connect('interp_idle', lambda w: self.set_line_number(None, 0)) + + @Property(str) + def backgroundcolor(self): + """Property to set the background color of the GCodeEditor (str). + + sets the background color of the GCodeEditor + """ + return self._backgroundcolor + + @backgroundcolor.setter + def backgroundcolor(self, color): + self._backgroundcolor = color + self.set_background_color(color) + + @Property(str) + def marginbackgroundcolor(self): + """Property to set the background color of the GCodeEditor margin (str). + + sets the background color of the GCodeEditor margin + """ + return self._marginbackgroundcolor + + @marginbackgroundcolor.setter + def marginbackgroundcolor(self, color): + self._marginbackgroundcolor = color + self.set_margin_background_color(color) + + def load_program(self, fname=None): + if fname is None: + fname = self._last_filename + else: + self._last_filename = fname + self.load_text(fname) + # self.zoomTo(6) + self.setCursorPosition(0, 0) + + + def load_text(self, fname): + try: + fp = os.path.expanduser(fname) + self.setText(open(fp).read()) + except: + LOG.error('File path is not valid: {}'.format(fname)) + self.setText('') + return + + self.last_line = None + self.ensureCursorVisible() + self.SendScintilla(QsciScintilla.SCI_VERTICALCENTRECARET) + + def highlight_line(self, line): + # if STATUS.is_auto_running(): + # if not STATUS.old['file'] == self._last_filename: + # LOG.debug('should reload the display') + # self.load_text(STATUS.old['file']) + # self._last_filename = STATUS.old['file'] + self.markerAdd(line, self.ARROW_MARKER_NUM) + if self.last_line: + self.markerDelete(self.last_line, self.ARROW_MARKER_NUM) + self.setCursorPosition(line, 0) + self.ensureCursorVisible() + self.SendScintilla(QsciScintilla.SCI_VERTICALCENTRECARET) + self.last_line = line + + def set_line_number(self, line): + pass + + def select_lineup(self): + line, col = self.getCursorPosition() + LOG.debug(line) + self.setCursorPosition(line - 1, 0) + self.highlight_line(line - 1) + + def select_linedown(self): + line, col = self.getCursorPosition() + LOG.debug(line) + self.setCursorPosition(line + 1, 0) + self.highlight_line(line + 1) + + + # simple input dialog for save as + def save_as_dialog(self, filename): + text, ok_pressed = QInputDialog.getText(self, "Save as", "New name:", QLineEdit.Normal, filename) + + if ok_pressed and text != '': + return text + else: + return False
+ + +# more complex dialog required by find replace +
[docs]class FindReplaceDialog(QDialog): + def __init__(self, parent): + super(FindReplaceDialog, self).__init__(parent) + + self.parent = parent + self.setWindowTitle("Find Replace") + self.setFixedSize(400, 200) + + main_layout = QVBoxLayout() + + find_layout = QHBoxLayout() + replace_layout = QHBoxLayout() + options_layout = QHBoxLayout() + buttons_layout = QHBoxLayout() + + find_label = QLabel() + find_label.setText("Find:") + + self.find_input = QLineEdit() + + find_layout.addWidget(find_label) + find_layout.addWidget(self.find_input) + + replace_label = QLabel() + replace_label.setText("Replace:") + + self.replace_input = QLineEdit() + + replace_layout.addWidget(replace_label) + replace_layout.addWidget(self.replace_input) + + self.close_button = QPushButton() + self.close_button.setText("Close") + + self.find_button = QPushButton() + self.find_button.setText("Find") + + self.replace_button = QPushButton() + self.replace_button.setText("Replace") + + self.all_button = QPushButton() + self.all_button.setText("Replace All") + + buttons_layout.addWidget(self.close_button) + buttons_layout.addWidget(self.find_button) + buttons_layout.addWidget(self.replace_button) + buttons_layout.addWidget(self.all_button) + + self.highlight_result = QCheckBox() + self.highlight_result.setText("highlight results") + + options_layout.addWidget(self.highlight_result) + + main_layout.addLayout(find_layout) + main_layout.addLayout(replace_layout) + main_layout.addLayout(options_layout) + main_layout.addLayout(buttons_layout) + + self.setLayout(main_layout) + + self.find_button.clicked.connect(self.find_text) + self.replace_button.clicked.connect(self.replace_text) + self.all_button.clicked.connect(self.replace_all_text) + self.close_button.clicked.connect(self.hide_dialog) + + def find_text(self): + find_text = self.find_input.text() + highlight = self.highlight_result.isChecked() + + self.parent.search_text(find_text, highlight) + + def replace_text(self): + find_text = self.find_input.text() + replace_text = self.replace_input.text() + + self.parent.replace_text(find_text, replace_text) + + def replace_all_text(self): + find_text = self.find_input.text() + replace_text = self.replace_input.text() + + if find_text == "": + return + + self.parent.replace_all_text(find_text, replace_text) + + def hide_dialog(self): + self.hide()
+ +# ============================================================================== +# For testing +# ============================================================================== +# if __name__ == "__main__": +# from qtpy.QtGui import QApplication +# +# app = QApplication(sys.argv) +# editor = GcodeEditor(standalone=True) +# editor.show() +# +# editor.setText(open(sys.argv[0]).read()) +# app.exec_() +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/input_widgets/gcode_text_edit.html b/_modules/qtpyvcp/widgets/input_widgets/gcode_text_edit.html new file mode 100644 index 000000000..35ee715d5 --- /dev/null +++ b/_modules/qtpyvcp/widgets/input_widgets/gcode_text_edit.html @@ -0,0 +1,798 @@ + + + + + + qtpyvcp.widgets.input_widgets.gcode_text_edit — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.input_widgets.gcode_text_edit

+"""
+GcodeTextEdit
+-------------
+
+QPlainTextEdit based G-code editor with syntax highlighting.
+"""
+
+import os
+import yaml
+
+from qtpy.QtCore import (Qt, QRect, QRegularExpression, QEvent, Slot, Signal,
+                         Property, QFile, QTextStream)
+
+from qtpy.QtGui import (QFont, QColor, QPainter, QSyntaxHighlighter,
+                        QTextDocument, QTextOption, QTextFormat,
+                        QTextCharFormat, QTextCursor)
+
+from qtpy.QtWidgets import (QApplication, QInputDialog, QTextEdit, QLineEdit,
+                            QPlainTextEdit, QWidget, QMenu,
+                            QPlainTextDocumentLayout)
+
+from qtpyvcp import DEFAULT_CONFIG_FILE
+from qtpyvcp.plugins import getPlugin
+from qtpyvcp.actions import program_actions
+from qtpyvcp.utilities.info import Info
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.utilities.encode_utils import allEncodings
+
+from qtpyvcp.widgets.dialogs.find_replace_dialog import FindReplaceDialog
+
+LOG = getLogger(__name__)
+INFO = Info()
+STATUS = getPlugin('status')
+YAML_DIR = os.path.dirname(DEFAULT_CONFIG_FILE)
+
+
+
[docs]class GcodeSyntaxHighlighter(QSyntaxHighlighter): + def __init__(self, document, font): + super(GcodeSyntaxHighlighter, self).__init__(document) + + self.font = font + + self.rules = [] + self.char_fmt = QTextCharFormat() + + self._abort = False + + self.loadSyntaxFromYAML() + + def loadSyntaxFromYAML(self): + + if INFO.getGcodeSyntaxFile() is not None: + YAML_DIR = os.environ['CONFIG_DIR'] + gcode_syntax_file = INFO.getGcodeSyntaxFile() + else: + YAML_DIR = os.path.dirname(DEFAULT_CONFIG_FILE) + gcode_syntax_file = 'gcode_syntax.yml' + + with open(os.path.join(YAML_DIR, gcode_syntax_file)) as fh: + syntax_specs = yaml.load(fh, Loader=yaml.FullLoader) + + cio = QRegularExpression.CaseInsensitiveOption + + for lang_name, language in list(syntax_specs.items()): + + definitions = language.get('definitions', {}) + + default_fmt_spec = definitions.get('default', {}).get('textFormat', {}) + + for context_name, spec in list(definitions.items()): + + base_fmt = default_fmt_spec.copy() + fmt_spec = spec.get('textFormat', {}) + + # update the default fmt spec + base_fmt.update(fmt_spec) + + char_fmt = self.charFormatFromSpec(fmt_spec) + + patterns = spec.get('match', []) + for pattern in patterns: + self.rules.append([QRegularExpression(pattern, cio), char_fmt]) + + def charFormatFromSpec(self, fmt_spec): + + char_fmt = self.defaultCharFormat() + + for option, value in list(fmt_spec.items()): + if value is None: + continue + + if option in ['foreground', 'background']: + value = QColor(value) + + if isinstance(value, str) and value.startswith('QFont:'): + value = getattr(QFont, value[6:]) + + attr = 'set' + option[0].capitalize() + option[1:] + getattr(char_fmt, attr)(value) + + return char_fmt + + def defaultCharFormat(self): + char_fmt = QTextCharFormat() + char_fmt.setFont(self.font()) + return char_fmt + +
[docs] def highlightBlock(self, text): + """Apply syntax highlighting to the given block of text. + """ + + QApplication.processEvents() + LOG.debug(f'Highlight light block: {tex}') + + + for regex, fmt in self.rules: + + nth = 0 + match = regex.match(text, offset=0) + index = match.capturedStart() + + while index >= 0: + + # We actually want the index of the nth match + index = match.capturedStart(nth) + length = match.capturedLength(nth) + self.setFormat(index, length, fmt) + + # check the rest of the string + match = regex.match(text, offset=index + length) + index = match.capturedStart()
+ + +
[docs]class GcodeTextEdit(QPlainTextEdit): + """G-code Text Edit + + QPlainTextEdit based G-code editor with syntax heightening. + """ + focusLine = Signal(int) + + def __init__(self, parent=None): + super(GcodeTextEdit, self).__init__(parent) + + self.parent = parent + + self.setCenterOnScroll(True) + self.setGeometry(50, 50, 800, 640) + self.setWordWrapMode(QTextOption.NoWrap) + + self.block_number = None + self.focused_line = 1 + self.current_line_background = QColor(self.palette().alternateBase()) + self.readonly = False + self.syntax_highlighting = False + + self.old_docs = [] + # set the custom margin + self.margin = NumberMargin(self) + + # set the syntax highlighter # Fixme un needed init here + self.gCodeHighlighter = None + + if parent is not None: + + self.find_case = None + self.find_words = None + + self.search_term = "" + self.replace_term = "" + + # context menu + self.menu = QMenu(self) + self.menu.addAction(self.tr("Run from line {}".format(self.focused_line)), self.runFromHere) + self.menu.addSeparator() + self.menu.addAction(self.tr('Cut'), self.cut) + self.menu.addAction(self.tr('Copy'), self.copy) + self.menu.addAction(self.tr('Paste'), self.paste) + self.menu.addAction(self.tr('Find'), self.findForward) + self.menu.addAction(self.tr('Find All'), self.findAll) + self.menu.addAction(self.tr('Replace'), self.replace) + self.menu.addAction(self.tr('Replace All'), self.replace) + + # FixMe: Picks the first action run from here, should not be by index + self.run_action = self.menu.actions()[0] + self.run_action.setEnabled(program_actions.run_from_line.ok()) + program_actions.run_from_line.bindOk(self.run_action) + + self.dialog = FindReplaceDialog(parent=self) + + # connect signals + self.cursorPositionChanged.connect(self.onCursorChanged) + + # connect status signals + STATUS.file.notify(self.loadProgramFile) + STATUS.motion_line.onValueChanged(self.setCurrentLine) + + @Slot(str) + def set_search_term(self, text): + LOG.debug(f"Set search term :{text}") + self.search_term = text + + @Slot(str) + def set_replace_term(self, text): + LOG.debug(f"Set replace term :{text}") + self.replace_term = text + + @Slot() + def findDialog(self): + LOG.debug("Show find dialog") + self.dialog.show() + + @Slot(bool) + def findCase(self, enabled): + LOG.debug(f"Find case sensitive :{enabled}") + self.find_case = enabled + + @Slot(bool) + def findWords(self, enabled): + LOG.debug(f"Find whole words :{enabled}") + self.find_words = enabled + + def findAllText(self, text): + flags = QTextDocument.FindFlag(0) + + if self.find_case: + flags |= QTextDocument.FindCaseSensitively + if self.find_words: + flags |= QTextDocument.FindWholeWords + + searching = True + cursor = self.textCursor() + + while searching: + found = self.find(text, flags) + if found: + cursor = self.textCursor() + else: + searching = False + + if cursor.hasSelection(): + self.setTextCursor(cursor) + + def findForwardText(self, text): + flags = QTextDocument.FindFlag() + + if self.find_case: + flags |= QTextDocument.FindCaseSensitively + if self.find_words: + flags |= QTextDocument.FindWholeWords + + found = self.find(text, flags) + + # if found: + # cursor = self.document().find(text, flags) + # if cursor.position() > 0: + # self.setTextCursor(cursor) + + def findBackwardText(self, text): + flags = QTextDocument.FindFlag() + flags |= QTextDocument.FindBackward + + if self.find_case: + flags |= QTextDocument.FindCaseSensitively + if self.find_words: + flags |= QTextDocument.FindWholeWords + + found = self.find(text, flags) + + # if found: + # cursor = self.document().find(text, flags) + # if cursor.position() > 0: + # self.setTextCursor(cursor) + + def replaceText(self, search, replace): + + flags = QTextDocument.FindFlag() + + if self.find_case: + flags |= QTextDocument.FindCaseSensitively + if self.find_words: + flags |= QTextDocument.FindWholeWords + + found = self.find(search, flags) + if found: + cursor = self.textCursor() + cursor.beginEditBlock() + if cursor.hasSelection(): + cursor.insertText(replace) + cursor.endEditBlock(); + + def replaceAllText(self, search, replace): + + flags = QTextDocument.FindFlag() + + if self.find_case: + flags |= QTextDocument.FindCaseSensitively + if self.find_words: + flags |= QTextDocument.FindWholeWords + + searching = True + while searching: + found = self.find(search, flags) + if found: + cursor = self.textCursor() + cursor.beginEditBlock() + if cursor.hasSelection(): + cursor.insertText(replace) + cursor.endEditBlock(); + else: + searching = False + + @Slot() + def findAll(self): + + text = self.search_term + LOG.debug(f"Find all text :{text}") + self.findAllText(text) + + @Slot() + def findForward(self): + text = self.search_term + LOG.debug(f"Find forward :{text}") + self.findForwardText(text) + + @Slot() + def findBackward(self): + text = self.search_term + LOG.debug(f"Find backwards :{text}") + self.findBackwardText(text) + + @Slot() + def replace(self): + + search_text = self.search_term + replace_text = self.replace_term + + LOG.debug(f"Replace text :{search_text} with {replace_text}") + + self.replaceText(search_text, replace_text) + + @Slot() + def replaceAll(self): + + search_text = self.search_term + replace_text = self.replace_term + + LOG.debug(f"Replace all text :{search_text} with {replace_text}") + + self.replaceAllText(search_text, replace_text) + + @Slot() + def saveFile(self, save_file_name = None): + if save_file_name == None: + save_file = QFile(str(STATUS.file)) + else: + save_file = QFile(str(save_file_name)) + + result = save_file.open(QFile.WriteOnly) + if result: + LOG.debug(f'---Save file: {save_file.fileName()}') + save_stream = QTextStream(save_file) + save_stream << self.toPlainText() + save_file.close() + else: + LOG.debug("---save error") + + # simple input dialog for save as + def save_as_dialog(self, filename): + text, ok_pressed = QInputDialog.getText(self, "Save as", "New name:", QLineEdit.Normal, filename) + + if ok_pressed and text != '': + return text + else: + return False + + @Slot() + def saveFileAs(self): + open_file = QFile(str(STATUS.file)) + if open_file == None: + return + + save_file = self.save_as_dialog(open_file.fileName()) + self.saveFile(save_file) + +
[docs] def keyPressEvent(self, event): + # keep the cursor centered + if event.key() == Qt.Key_Up: + self.moveCursor(QTextCursor.Up) + self.centerCursor() + + elif event.key() == Qt.Key_Down: + self.moveCursor(QTextCursor.Down) + self.centerCursor() + + else: + super(GcodeTextEdit, self).keyPressEvent(event)
+ +
[docs] def changeEvent(self, event): + if event.type() == QEvent.FontChange: + # Update syntax highlighter with new font + self.gCodeHighlighter = GcodeSyntaxHighlighter(self.document(), self.font) + + super(GcodeTextEdit, self).changeEvent(event)
+ +
[docs] @Slot(bool) + def syntaxHighlightingOnOff(self, state): + """Toggle syntax highlighting on/off""" + pass
+ + @Property(bool) + def syntaxHighlighting(self): + return self.syntax_highlighting + + @syntaxHighlighting.setter + def syntaxHighlighting(self, state): + self.syntax_highlighting = state + +
[docs] def setPlainText(self, p_str): + # FixMe: Keep a reference to old QTextDocuments form previously loaded + # files. This is needed to prevent garbage collection which results in a + # seg fault if the document is discarded while still being highlighted. + self.old_docs.append(self.document()) + + LOG.debug('setPlanText') + doc = QTextDocument() + doc.setDocumentLayout(QPlainTextDocumentLayout(doc)) + doc.setPlainText(p_str) + + # start syntax highlighting + if self.syntax_highlighting == True: + self.gCodeHighlighter = GcodeSyntaxHighlighter(doc, self.font) + LOG.debug('Syntax highlighting enabled.') + + self.setDocument(doc) + self.margin.updateWidth() + LOG.debug('Document set with text.')
+ + # start syntax highlighting + # self.gCodeHighlighter = GcodeSyntaxHighlighter(self) + +
[docs] @Slot(bool) + def EditorReadOnly(self, state): + """Set to Read Only to disable editing""" + + if state: + self.setReadOnly(True) + else: + self.setReadOnly(False) + + self.readonly = state
+ +
[docs] @Slot(bool) + def EditorReadWrite(self, state): + """Set to Read Only to disable editing""" + + if state: + self.setReadOnly(False) + else: + self.setReadOnly(True) + + self.readonly != state
+ + @Property(bool) + def readOnly(self): + return self.readonly + + @readOnly.setter + def readOnly(self, state): + + if state: + self.setReadOnly(True) + else: + self.setReadOnly(False) + + self.readonly = state + + @Property(QColor) + def currentLineBackground(self): + return self.current_line_background + + @currentLineBackground.setter + def currentLineBackground(self, color): + self.current_line_background = color + # Hack to get background to update + self.setCurrentLine(2) + self.setCurrentLine(1) + + @Property(QColor) + def marginBackground(self): + return self.margin.background + + @marginBackground.setter + def marginBackground(self, color): + self.margin.background = color + self.margin.update() + + @Property(QColor) + def marginCurrentLineBackground(self): + return self.margin.highlight_background + + @marginCurrentLineBackground.setter + def marginCurrentLineBackground(self, color): + self.margin.highlight_background = color + self.margin.update() + + @Property(QColor) + def marginColor(self): + return self.margin.color + + @marginColor.setter + def marginColor(self, color): + self.margin.color = color + self.margin.update() + + @Property(QColor) + def marginCurrentLineColor(self): + return self.margin.highlight_color + + @marginCurrentLineColor.setter + def marginCurrentLineColor(self, color): + self.margin.highlight_color = color + self.margin.update() + + @Slot(str) + @Slot(object) + def loadProgramFile(self, fname=None): + if fname: + encodings = allEncodings() + enc = None + for enc in encodings: + try: + with open(fname, 'r', encoding=enc) as f: + gcode = f.read() + break + except Exception as e: + # LOG.debug(e) + LOG.info(f"File encoding doesn't match {enc}, trying others") + LOG.info(f"File encoding: {enc}") + # set the syntax highlighter + self.setPlainText(gcode) + # self.gCodeHighlighter = GcodeSyntaxHighlighter(self.document(), self.font) + + @Slot(int) + @Slot(object) + def setCurrentLine(self, line): + cursor = QTextCursor(self.document().findBlockByLineNumber(line - 1)) + self.setTextCursor(cursor) + self.centerCursor() + + def getCurrentLine(self): + return self.textCursor().blockNumber() + 1 + + def onCursorChanged(self): + # highlights current line, find a way not to use QTextEdit + block_number = self.textCursor().blockNumber() + if block_number != self.block_number: + self.block_number = block_number + selection = QTextEdit.ExtraSelection() + selection.format.setBackground(self.current_line_background) + selection.format.setProperty(QTextFormat.FullWidthSelection, True) + selection.cursor = self.textCursor() + selection.cursor.clearSelection() + self.setExtraSelections([selection]) + + # emit signals for backplot etc. + self.focused_line = block_number + 1 + self.focusLine.emit(self.focused_line) + +
[docs] def contextMenuEvent(self, event): + self.run_action.setText("Run from line {}".format(self.focused_line)) + self.menu.popup(event.globalPos()) + event.accept()
+ + def runFromHere(self, *args, **kwargs): + line = self.getCurrentLine() + program_actions.run(line) + +
[docs] def resizeEvent(self, *e): + cr = self.contentsRect() + rec = QRect(cr.left(), cr.top(), self.margin.getWidth(), cr.height()) + self.margin.setGeometry(rec) + QPlainTextEdit.resizeEvent(self, *e)
+ + +
[docs]class NumberMargin(QWidget): + def __init__(self, parent): + QWidget.__init__(self, parent) + self.parent = parent + # this only happens when lines are added or subtracted + self.parent.blockCountChanged.connect(self.updateWidth) + # this happens quite often + self.parent.updateRequest.connect(self.updateContents) + + self.background = QColor('#e8e8e8') + self.highlight_background = QColor('#e8e8e8') + self.color = QColor('#717171') + self.highlight_color = QColor('#000000') + + def getWidth(self): + blocks = self.parent.blockCount() + return self.parent.fontMetrics().width(str(blocks)) + 5 + + def updateWidth(self): # check the number column width and adjust + width = self.getWidth() + if self.width() != width: + self.setFixedWidth(width) + self.parent.setViewportMargins(width, 0, 0, 0) + + def updateContents(self, rect, scroll): + if scroll: + self.scroll(0, scroll) + else: + self.update(0, rect.y(), self.width(), rect.height()) + + if rect.contains(self.parent.viewport().rect()): + self.updateWidth() + +
[docs] def paintEvent(self, event): # this puts the line numbers in the margin + painter = QPainter(self) + painter.fillRect(event.rect(), self.background) + block = self.parent.firstVisibleBlock() + + font = self.parent.font() + + while block.isValid(): + block_num = block.blockNumber() + block_top = self.parent.blockBoundingGeometry(block).translated(self.parent.contentOffset()).top() + + # if the block is not visible stop wasting time + if not block.isVisible() or block_top >= event.rect().bottom(): + break + + if block_num == self.parent.textCursor().blockNumber(): + font.setBold(True) + painter.setFont(font) + painter.setPen(self.highlight_color) + background = self.highlight_background + else: + font.setBold(False) + painter.setFont(font) + painter.setPen(self.color) + background = self.background + + paint_rec = QRect(0, int(block_top), self.width(), + self.parent.fontMetrics().height()) + text_rec = QRect(0, int(block_top), self.width() - + 4, self.parent.fontMetrics().height()) + painter.fillRect(paint_rec, background) + painter.drawText(text_rec, Qt.AlignRight, str(block_num + 1)) + block = block.next() + + painter.end() + QWidget.paintEvent(self, event)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/input_widgets/jog_increment.html b/_modules/qtpyvcp/widgets/input_widgets/jog_increment.html new file mode 100644 index 000000000..280dd7c49 --- /dev/null +++ b/_modules/qtpyvcp/widgets/input_widgets/jog_increment.html @@ -0,0 +1,294 @@ + + + + + + qtpyvcp.widgets.input_widgets.jog_increment — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.input_widgets.jog_increment

+#   Copyright (c) 2018 Kurt Jacobson
+#      <kurtcjacobson@gmail.com>
+#
+#   This file is part of QtPyVCP.
+#
+#   QtPyVCP is free software: you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 2 of the License, or
+#   (at your option) any later version.
+#
+#   QtPyVCP is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with QtPyVCP.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from qtpy.QtGui import QColor
+from qtpy.QtCore import Qt, Slot, Property
+from qtpy.QtWidgets import QWidget, QBoxLayout, QSizePolicy
+
+from qtpyvcp.utilities.info import Info
+from qtpyvcp.actions.machine_actions import jog
+from qtpyvcp.plugins import getPlugin
+from qtpyvcp.utilities.settings import getSetting, setSetting
+from qtpyvcp.widgets.button_widgets.led_button import LEDButton
+
+STATUS = getPlugin('status')
+INFO = Info()
+
+
+
[docs]class JogIncrementWidget(QWidget): + + def __init__(self, parent=None, standalone=False): + super(JogIncrementWidget, self).__init__(parent) + + self._container = hBox = QBoxLayout(QBoxLayout.LeftToRight, self) + + hBox.setContentsMargins(0, 0, 0, 0) + self._ledDiameter = 15 + self._ledColor = QColor('green') + self._alignment = Qt.AlignTop | Qt.AlignRight + # This prevents doing unneeded initialization + # when QtDesginer loads the plugin. + if parent is None and not standalone: + return + + enable_default = True + + increments = INFO.getIncrements() + for increment in increments: + button = LEDButton() + + button.setCheckable(True) + button.setAutoExclusive(True) + button.setFocusPolicy(Qt.NoFocus) + button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + button.setMinimumSize(50, 42) + + if increment != 0: + raw_increment = increment.strip() + # print('[', raw_increment, ']') + button.setText(raw_increment) + button.clicked.connect(self.setJogIncrement) + + if enable_default: + enable_default = False + + button.setDefault(True) + button.setChecked(True) + + hBox.addWidget(button) + + self.placeLed() + + def setJogIncrement(self): + setSetting('machine.jog.increment', self.sender().text()) + + def layoutWidgets(self, layout): + return (layout.itemAt(i) for i in range(layout.count())) + + def placeLed(self): + for w in self.layoutWidgets(self._container): + w.widget().setLedDiameter(self._ledDiameter) + w.widget().setLedColor(self._ledColor) + w.widget().setAlignment(self._alignment) + + def getLedDiameter(self): + return self._ledDiameter + + @Slot(int) + def setLedDiameter(self, value): + self._ledDiameter = value + self.placeLed() + + def getLedColor(self): + return self._ledColor + + @Slot(QColor) + def setLedColor(self, value): + self._ledColor = value + self.placeLed() + + def getAlignment(self): + return self._alignment + + @Slot(Qt.Alignment) + def setAlignment(self, value): + self._alignment = Qt.Alignment(value) + self.placeLed() + + def getOrientation(self): + if self._container.direction() == QBoxLayout.LeftToRight: + return Qt.Horizontal + else: + return Qt.Vertical + + @Slot(Qt.Orientation) + def setOrientation(self, value): + if value == Qt.Horizontal: + self._container.setDirection(QBoxLayout.LeftToRight) + else: + self._container.setDirection(QBoxLayout.TopToBottom) + self.adjustSize() + + def getLayoutSpacing(self): + return self._container.spacing() + + @Slot(int) + def setLayoutSpacing(self, value): + self._container.setSpacing(value) + + diameter = Property(int, getLedDiameter, setLedDiameter) + color = Property(QColor, getLedColor, setLedColor) + alignment = Property(Qt.Alignment, getAlignment, setAlignment) + orientation = Property(Qt.Orientation, getOrientation, setOrientation) + layoutSpacing = Property(int, getLayoutSpacing, setLayoutSpacing)
+ + +if __name__ == "__main__": + import sys + from qtpy.QtWidgets import QApplication + app = QApplication(sys.argv) + w = JogIncrementWidget(standalone=True) + w.show() + sys.exit(app.exec_()) +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/input_widgets/line_edit.html b/_modules/qtpyvcp/widgets/input_widgets/line_edit.html new file mode 100644 index 000000000..dc4cf50e5 --- /dev/null +++ b/_modules/qtpyvcp/widgets/input_widgets/line_edit.html @@ -0,0 +1,199 @@ + + + + + + qtpyvcp.widgets.input_widgets.line_edit — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.input_widgets.line_edit

+"""
+Line Edit
+---------
+"""
+
+from qtpy.QtCore import Property
+from qtpy.QtWidgets import QLineEdit
+
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.widgets.base_widgets.base_widget import CMDWidget
+
+LOG = getLogger(__name__)
+
+
+
[docs]class VCPLineEdit(QLineEdit, CMDWidget): + """VCP Entry Widget""" + + DEFAULT_RULE_PROPERTY = "Text" + RULE_PROPERTIES = CMDWidget.RULE_PROPERTIES.copy() + RULE_PROPERTIES.update({ + 'Text': ['setText', str], + }) + + def __init__(self, parent=None): + super(VCPLineEdit, self).__init__(parent) + + self._action_name = '' + + self.returnPressed.connect(self.onReturnPressed) + + def onReturnPressed(self): + self.clearFocus() + LOG.debug("Action entry activated with text: %s", self.text()) + + @Property(str) + def actionName(self): + """The name of the action the entry should trigger. + + Returns: + str : The action name. + """ + return self._action_name + + @actionName.setter + def actionName(self, action_name): + self._action_name = action_name + # ToDo: activate action on enter + # bindWidget(self, action_name) + +
[docs] def initialize(self): + pass
+ +
[docs] def terminate(self): + pass
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/input_widgets/mdientry_widget.html b/_modules/qtpyvcp/widgets/input_widgets/mdientry_widget.html new file mode 100644 index 000000000..2fb1c1afe --- /dev/null +++ b/_modules/qtpyvcp/widgets/input_widgets/mdientry_widget.html @@ -0,0 +1,263 @@ + + + + + + qtpyvcp.widgets.input_widgets.mdientry_widget — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.input_widgets.mdientry_widget

+
+from qtpy.QtCore import Qt, Slot, Property, QStringListModel
+from qtpy.QtGui import QValidator
+from qtpy.QtWidgets import QLineEdit, QListWidgetItem, QCompleter
+
+from qtpyvcp.plugins import getPlugin
+from qtpyvcp.utilities.info import Info
+from qtpyvcp.actions.machine_actions import issue_mdi
+from qtpyvcp.widgets.base_widgets.base_widget import CMDWidget
+
+STATUS = getPlugin('status')
+INFO = Info()
+
+
+
[docs]class Validator(QValidator): +
[docs] def validate(self, string, pos): + # eventually could do some actual validating here + inputVal = string + if not(inputVal.startswith(";") or inputVal.startswith("(")): + inputVal = inputVal.upper(); + return QValidator.Acceptable, inputVal, pos
+ + +
[docs]class MDIEntry(QLineEdit, CMDWidget): + """MDI Entry + + Input any valid g Code. Enter sends the g Code. + """ + def __init__(self, parent=None): + super(MDIEntry, self).__init__(parent) + + self.mdi_rtnkey_behaviour_supressed = False + self.mdi_history_size = 100 + + self.validator = Validator(self) + self.setValidator(self.validator) + + # The completer is what creates the visual list mdi history + # that can be selected from. This is useful when mdientry is used + # stand alone but can get in the way when using this widget in + # conjunction with mdihistory widget and a rich on-screen data + # entry UI. So this designer exposed setting allows this to be enabled + # or disabled as needed. + self._completer_enabled = True + + self.returnPressed.connect(self.submit) + + @Property(int) + def mdi_history_size(self): + return self._mdi_history_size + + @mdi_history_size.setter + def mdi_history_size(self, size): + self._mdi_history_size = size + + @Property(bool) + def completerEnabled(self): + return self._completer_enabled + + @completerEnabled.setter + def completerEnabled(self, flag): + self._completer_enabled = flag + + @Slot() + def submit(self): + # Only support submit from Return Key if not suppressed + # This is used to stop standalone behaviour by the MDIHistory widget. + # MDIHisttory uses its handle to this widget to set + # mdi_rtnkey_behaviour_supressed = True. + # MDIHistory will now manage the MDILine submission process + if not self.mdi_rtnkey_behaviour_supressed: + cmd = str(self.text()).strip() + issue_mdi(cmd) + self.setText('') + STATUS.mdi_history.setValue(cmd) + + @Slot(QListWidgetItem) + def setMDIText(self, listItem): + if listItem is not None: + self.setText(listItem.text()) + +
[docs] def keyPressEvent(self, event): + if ( self._completer_enabled + and + ( event.key() == Qt.Key_Up + or event.key() == Qt.Key_Down + ) + ): + self.completer().complete() + else: + super(MDIEntry, self).keyPressEvent(event)
+ +
[docs] def focusInEvent(self, event): + super(MDIEntry, self).focusInEvent(event) + if self._completer_enabled: + self.completer().complete()
+ + def supress_rtn_key_behaviour(self): + self.mdi_rtnkey_behaviour_supressed = True + + def enable_rtn_key_behaviour(self): + self.mdi_rtnkey_behaviour_supressed = False + +
[docs] def initialize(self): + history = STATUS.mdi_history.value + if self._completer_enabled: + completer = QCompleter() + completer.setCaseSensitivity(Qt.CaseInsensitive) + self.model = QStringListModel() + completer.setModel(self.model) + self.setCompleter(completer) + self.model.setStringList(history) + STATUS.mdi_history.notify(self.model.setStringList) + + STATUS.max_mdi_history_length = self.mdi_history_size
+ +
[docs] def terminate(self): + pass
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/input_widgets/mdihistory_widget.html b/_modules/qtpyvcp/widgets/input_widgets/mdihistory_widget.html new file mode 100644 index 000000000..003e45f6e --- /dev/null +++ b/_modules/qtpyvcp/widgets/input_widgets/mdihistory_widget.html @@ -0,0 +1,499 @@ + + + + + + qtpyvcp.widgets.input_widgets.mdihistory_widget — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.input_widgets.mdihistory_widget

+# -*- coding: utf-8 -*-
+"""QtPyVCP MDI History Widget
+
+This widget implements the following key elements:
+[1] A history display of MDI commands issued with the latest
+command at the top of the list and the oldest at the bottom
+of the list.
+
+[2] A queue system of commands that have been entered
+but have not yet been executed. This allows the rapid entry
+of MDI commands to be executed without having to wait for any
+running commands to complete.
+
+ToDo:
+    * add/test for styling based on the class queue codes
+
+"""
+import os
+
+from qtpy.QtCore import Qt, Slot, Property, QTimer
+from qtpy.QtGui import QIcon
+from qtpy.QtWidgets import QListWidget
+from qtpy.QtWidgets import QListWidgetItem
+
+import qtpyvcp
+from qtpyvcp.plugins import getPlugin
+from qtpyvcp.utilities.info import Info
+from qtpyvcp.actions.machine_actions import issue_mdi
+from qtpyvcp.actions.program_actions import load as loadProgram
+from qtpyvcp.widgets.base_widgets.base_widget import CMDWidget
+
+import linuxcnc
+
+
+STATUS = getPlugin('status')
+STAT = STATUS.stat
+INFO = Info()
+
+
+
[docs]class MDIHistory(QListWidget, CMDWidget): + """MDI History and Queuing Widget. + + This widget implements a visual view of the MDI command + history. It also implements a command queuing startegy + so that commands can be entered and queued up for execution. + Visual style is used to identify items that have been completed, + are running and are yet to run. + """ + + # MDI Queue status constants + MDIQ_DONE = 0 + MDIQ_RUNNING = 1 + MDIQ_TODO = 2 + MDQQ_ROLE = 256 + + def __init__(self, parent=None): + super(MDIHistory, self).__init__(parent) + + # name and widget handle for MDI cmd entry widget + self.mdi_entryline_name = None + self.mdi_entry_widget = None + + # List order direction where Natural is latest at bottom. + # Default is Natural = False + self.mdi_listorder_natural = False + + self.heart_beat_timer = None + + self.icon_run_name = 'media-playback-start' + self.icon_run = QIcon.fromTheme(self.icon_run_name) + self.icon_waiting_name = 'media-playback-pause' + self.icon_waiting = QIcon.fromTheme(self.icon_waiting_name) + + #self.returnPressed.connect(self.submit) + + @Property(str) + def mdiEntrylineName(self): + """Return name of entry object to Designer""" + return self.mdi_entryline_name + + @mdiEntrylineName.setter + def mdiEntrylineName(self, object_name): + """Set the name for Designer""" + self.mdi_entryline_name = object_name + + @Property(bool) + def mdiListOrderNatural(self): + """Return if list order is Natural - True/False""" + return self.mdi_listorder_natural + + @mdiListOrderNatural.setter + def mdiListOrderNatural(self, order_flag): + """Set Order flag for Designer""" + self.mdi_listorder_natural = order_flag + +
[docs] @Slot(bool) + def toggleQueue(self, toggle): + """Toggle queue pause. + Starting point is the queue is active. + """ + if toggle: + self.heart_beat_timer.stop() + else: + self.heart_beat_timer.start()
+ +
[docs] @Slot() + def clearQueue(self): + """Clear queue items pending run state for items yet to be run.""" + if self.mdi_listorder_natural: + list_length = list(range(self.count())) + else: + list_length = list(range(self.count()-1, 0, -1)) + + for list_item in list_length: + row_item = self.item(list_item) + row_item_data = row_item.data(MDIHistory.MDQQ_ROLE) + + if row_item_data == MDIHistory.MDIQ_TODO: + row_item.setData(MDIHistory.MDQQ_ROLE, MDIHistory.MDIQ_DONE) + row_item.setIcon(QIcon())
+ +
[docs] @Slot() + def removeSelectedItem(self): + """Remove the selected lines""" + selItems = self.selectedItems() + if len(selItems) > 0: + if self.mdi_listorder_natural: + firstItemRow = self.row(selItems[0]) - 1 + else: + firstItemRow = self.row(selItems[len(selItems)-1]) + for item in selItems: + row = self.row(item) + self.takeItem(row) + if not self.mdi_listorder_natural: + STATUS.mdi_remove_entry(row) + else: + # The order of MDI is latest LAST in the list. + # framework mdi history is latest FIRST in the list. + history_length = len(STATUS.mdi_history.value) + history_target_row = history_length-1-row + STATUS.mdi_remove_entry(history_target_row) + # select an item that makes sense for the case of a single + # item having been selected and deleted. + if firstItemRow < 0 and self.count() > 0: + row = 0 + else: + row = firstItemRow + self.setCurrentRow(row)
+ +
[docs] @Slot() + def removeAll(self): + """Remove all items from list and from history""" + self.clear() + STATUS.mdi_remove_all()
+ +
[docs] @Slot() + def runFromSelection(self): + """Start running MDI from the selected row back to correct end.""" + if self.mdi_listorder_natural: + row_list = list(range(self.currentRow(), self.count(), 1)) + else: + row_list = list(range(self.currentRow(), -1, -1)) + + # from selected row loop back to top/bottom and set ready for run + for row in row_list: + row_item = self.item(row) + row_item.setData(MDIHistory.MDQQ_ROLE, MDIHistory.MDIQ_TODO) + row_item.setIcon(self.icon_waiting)
+ +
[docs] @Slot() + def runSelection(self): + """Run the selected row only.""" + row = self.currentRow() + # from selected row loop back to top and set ready for run + row_item = self.item(row) + row_item.setData(MDIHistory.MDQQ_ROLE, MDIHistory.MDIQ_TODO) + row_item.setIcon(self.icon_waiting)
+ +
[docs] @Slot() + def submit(self): + """Put a new command on the queue for later execution. + """ + # put the new command on the queue + cmd = str(self.mdi_entry_widget.text()).strip() + row_item = QListWidgetItem() + row_item.setText(cmd) + row_item.setData(MDIHistory.MDQQ_ROLE, MDIHistory.MDIQ_TODO) + row_item.setIcon(self.icon_waiting) + if self.mdi_listorder_natural: + self.addItem(row_item) + else: + self.insertItem(0, row_item) + + # Set the recently added item as the "current" item + # if the queue is not paused this will quickly get overridden + # to the executing item highlight mode + self.clearSelection() + self.setCurrentItem(row_item) + + # put the command onto the status channel mdi history + # This always adds this item at position Zero on the channel + STATUS.mdi_history.setValue(cmd) + + # now clear down the mdi entry text ready for new input + self.mdi_entry_widget.clear()
+ +
[docs] def rowClicked(self): + """Item row clicked.""" + pass
+ + @Slot() + def copySelectionToGcodeEditor(self): + fname = '/tmp/mdi_gcode.ngc' + selection = self.selectedItems() + with open(fname, 'w') as fh: + for item in selection: + cmd = str(item.text()).strip() + fh.write(cmd + '\n') + fh.write('M2\n') + loadProgram(fname) + + @Slot() + def moveRowItemUp(self): + row = self.currentRow() + if row == 0: + return + item = self.takeItem(row) + self.insertItem(row-1, item) + self.setCurrentRow(row-1) + if not self.mdi_listorder_natural: + STATUS.mdi_swap_entries(row, row-1) + else: + history_length = len(STATUS.mdi_history.value) + history_target_row = history_length-1-row + STATUS.mdi_swap_entries(history_target_row, history_target_row+1) + + @Slot() + def moveRowItemDown(self): + row = self.currentRow() + if row == self.count()-1: + return + item = self.takeItem(row) + self.insertItem(row+1, item) + self.setCurrentRow(row+1) + if not self.mdi_listorder_natural: + STATUS.mdi_swap_entries(row, row+1) + else: + history_length = len(STATUS.mdi_history.value) + history_target_row = history_length-1-row + STATUS.mdi_swap_entries(history_target_row, history_target_row-1) + +
[docs] def keyPressEvent(self, event): + """Key movement processing. + Arrow keys move the selected list item up/down + Return key generates a submit situation by making the item as + the next available command to processes. + """ + row = self.currentRow() + if event.key() == Qt.Key_Up: + if row > 0: + row -= 1 + elif event.key() == Qt.Key_Down: + if row < self.count()-1: + row += 1 + else: + super(MDIHistory, self).keyPressEvent(event) + + self.setCurrentRow(row)
+ + #def focusInEvent(self, event): + # super(MDIHistory, self).focusInEvent(event) + # pass + +
[docs] def setHistory(self, items_list): + """Clear and reset the history in the list. + item_list is a list of strings.""" + print('Clear and load history to list') + self.clear() + + # check that there is anything do to + if len(items_list) == 0: + return + + # load the history based on natural order or not + if self.mdi_listorder_natural: + items_list.reverse() + + for item in items_list: + row_item = QListWidgetItem() + row_item.setText(item) + row_item.setData(MDIHistory.MDQQ_ROLE, MDIHistory.MDIQ_DONE) + row_item.setIcon(QIcon()) + self.addItem(row_item)
+ +
[docs] def heartBeat(self): + """Supports heart beat on the MDI History execution queue. + Issue the next command from the queue. + Double check machine is in ok state to accept next command. + Issue the command and if success mark command as being active. + Mark last command as done. + """ + # check if machine is idle and ready to run another command + if STAT.interp_state != linuxcnc.INTERP_IDLE: + # RS274NGC interpreter not in a state to execute, bail + return + + # scan for the next command to execute from bottom up. + if self.mdi_listorder_natural: + list_length = list(range(self.count())) + else: + list_length = list(range(self.count()-1, 0, -1)) + + for list_item in list_length: + row_item = self.item(list_item) + row_item_data = row_item.data(MDIHistory.MDQQ_ROLE) + + if row_item_data == MDIHistory.MDIQ_RUNNING: + # machine is in idle state so the running command is done + row_item.setData(MDIHistory.MDQQ_ROLE, MDIHistory.MDIQ_DONE) + row_item.setIcon(QIcon()) + + elif row_item_data == MDIHistory.MDIQ_TODO: + self.clearSelection() + self.setCurrentItem(row_item) + cmd = str(row_item.text()).strip() + row_item.setData(MDIHistory.MDQQ_ROLE, MDIHistory.MDIQ_RUNNING) + row_item.setIcon(self.icon_run) + issue_mdi(cmd) + break
+ +
[docs] def initialize(self): + """Load up starting data and set signal connections.""" + # get a proper copy of the list so changes to it are contained. + history = STATUS.mdi_history.value[:] + self.setHistory(history) + self.clicked.connect(self.rowClicked) + + # Get handle to windows list and search through them + # for the widget referenced in mdi_entryline_name + for win_name, obj in list(qtpyvcp.WINDOWS.items()): + if hasattr(obj, str(self.mdi_entryline_name)): + self.mdi_entry_widget = getattr(obj, self.mdi_entryline_name) + # Use the handle to supress the widgets Rtn key behaviour + self.mdi_entry_widget.supress_rtn_key_behaviour() + break + # Setup the basic timer system as a heart beat on the queue + self.heart_beat_timer = QTimer(self) + # use a sub-second second timer + self.heart_beat_timer.start(250) + self.heart_beat_timer.timeout.connect(self.heartBeat)
+ +
[docs] def terminate(self): + """Teardown processing.""" + self.heart_beat_timer.stop()
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/input_widgets/offset_table.html b/_modules/qtpyvcp/widgets/input_widgets/offset_table.html new file mode 100644 index 000000000..46684ded6 --- /dev/null +++ b/_modules/qtpyvcp/widgets/input_widgets/offset_table.html @@ -0,0 +1,472 @@ + + + + + + qtpyvcp.widgets.input_widgets.offset_table — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.input_widgets.offset_table

+#   Copyright (c) 2018 Kurt Jacobson
+#      <kurtcjacobson@gmail.com>
+#
+#   This file is part of QtPyVCP.
+#
+#   QtPyVCP is free software: you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 2 of the License, or
+#   (at your option) any later version.
+#
+#   QtPyVCP is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with QtPyVCP.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from qtpy.QtCore import Qt, Slot, Property, QModelIndex, QSortFilterProxyModel
+from qtpy.QtGui import QStandardItemModel, QColor, QBrush
+from qtpy.QtWidgets import QTableView, QStyledItemDelegate, QDoubleSpinBox, QMessageBox
+
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.plugins import getPlugin
+from qtpyvcp.utilities.settings import connectSetting, getSetting
+
+STATUS = getPlugin('status')
+LOG = getLogger(__name__)
+
+
+
[docs]class ItemDelegate(QStyledItemDelegate): + + def __init__(self, columns): + super(ItemDelegate, self).__init__() + + + + self._columns = columns + self._padding = ' ' * 2 + + def setColumns(self, columns): + self._columns = columns + +
[docs] def displayText(self, value, locale): + + if type(value) == float: + return "{0:.4f}".format(value) + + return "{}{}".format(self._padding, value)
+ +
[docs] def createEditor(self, parent, option, index): + # ToDo: set dec placed for IN and MM machines + col = self._columns[index.column()] + + if col in 'XYZABCUVWR': + editor = QDoubleSpinBox(parent) + editor.setFrame(False) + editor.setAlignment(Qt.AlignCenter) + editor.setDecimals(4) + # editor.setStepType(QSpinBox.AdaptiveDecimalStepType) + editor.setProperty('stepType', 1) # stepType was added in 5.12 + + min_range = getSetting('offset_table.min_range').value + max_range = getSetting('offset_table.max_range').value + + if min_range and max_range: + editor.setRange(min_range, max_range) + else: + editor.setRange(-1000, 1000) + return editor + + return None
+ + +
[docs]class OffsetModel(QStandardItemModel): + def __init__(self, parent=None): + super(OffsetModel, self).__init__(parent) + + self.ot = getPlugin('offsettable') + + self.current_row_color = QColor(Qt.darkGreen) + + self._columns = self.ot.columns + self._rows = self.ot.rows + + self._column_labels = self.ot.COLUMN_LABELS + self._row_labels = self.ot.ROW_LABELS + + self._offset_table = self.ot.getOffsetTable() + + self.setColumnCount(self.columnCount()) + self.setRowCount(len(self._rows)) # (self.rowCount()) + + self.ot.offset_table_changed.connect(self.updateModel) + + def refreshModel(self): + # refresh model so current row gets highlighted + self.beginResetModel() + self.endResetModel() + + def updateModel(self, offset_table): + # update model with new data + self.beginResetModel() + self._offset_table = offset_table + self.endResetModel() + + def setColumns(self, columns): + self._columns = columns + self.setColumnCount(len(columns)) + +
[docs] def headerData(self, section, orientation, role=Qt.DisplayRole): + if role == Qt.DisplayRole and orientation == Qt.Horizontal: + return self._columns[section] + elif role == Qt.DisplayRole and orientation == Qt.Vertical: + return self._row_labels[section] + + return QStandardItemModel.headerData(self, section, orientation, role)
+ +
[docs] def columnCount(self, parent=None): + return len(self._columns)
+ +
[docs] def rowCount(self, parent=None): + return len(self._rows)
+ +
[docs] def flags(self, index): + return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
+ +
[docs] def data(self, index, role=Qt.DisplayRole): + if role == Qt.DisplayRole or role == Qt.EditRole: + + column_index = self._columns[index.column()] + index_column = self._column_labels.index(column_index) + + return self._offset_table[index.row()][index_column] + + elif role == Qt.TextAlignmentRole: + return Qt.AlignVCenter | Qt.AlignRight + + elif role == Qt.TextColorRole: + + offset = index.row() + 1 + + if self.ot.current_index == offset: + + return QBrush(self.current_row_color) + + else: + + return QStandardItemModel.data(self, index, role) + + return QStandardItemModel.data(self, index, role)
+ +
[docs] def setData(self, index, value, role): + columns_index = index.column() + rows_index = index.row() + + column_index = self._columns[index.column()] + index_column = self._column_labels.index(column_index) + + self._offset_table[rows_index][index_column] = value + + return True
+ + def clearRow(self, row): + + for col in range(len(self._columns)): + index_column = self._column_labels.index(self._columns[col]) + self._offset_table[row][index_column] = 0.0 + + self.refreshModel() + + def clearRows(self): + + for row in range(len(self._rows)): + for col in range(len(self._columns)): + index_column = self._column_labels.index(self._columns[col]) + self._offset_table[row][index_column] = 0.0 + + self.refreshModel() + + def offsetDataFromRow(self, row): + o_num = sorted(self._offset_table)[row] + return self._offset_table[o_num] + + def saveOffsetTable(self): + self.ot.saveOffsetTable(self._offset_table, columns=self._columns) + return True + + def loadOffsetTable(self): + # the tooltable plugin will emit the tool_table_changed signal + # so we don't need to do any more here + self.ot.loadOffsetTable() + return True
+ + +
[docs]class OffsetTable(QTableView): + def __init__(self, parent=None): + super(OffsetTable, self).__init__(parent) + + self.setEnabled(False) + + self.offset_model = OffsetModel(self) + + # Properties + self._columns = self.offset_model._columns + self._confirm_actions = False + self._current_row_color = QColor('sage') + + self.proxy_model = QSortFilterProxyModel() + self.proxy_model.setFilterKeyColumn(0) + self.proxy_model.setSourceModel(self.offset_model) + + self.item_delegate = ItemDelegate(columns=self._columns) + self.setItemDelegate(self.item_delegate) + + self.setModel(self.proxy_model) + + # Appearance/Behaviour settings + self.setSortingEnabled(False) + self.setAlternatingRowColors(True) + self.setSelectionBehavior(QTableView.SelectRows) + self.setSelectionMode(QTableView.SingleSelection) + self.horizontalHeader().setStretchLastSection(False) + self.horizontalHeader().setSortIndicator(0, Qt.AscendingOrder) + + STATUS.all_axes_homed.notify(self.handle_home_signal) + + def handle_home_signal(self, all_axes): + if all_axes: + self.setEnabled(True) + else: + self.setEnabled(False) + + @Slot() + def saveOffsetTable(self): + + if self.isEnabled(): + if not self.confirmAction("Do you want to save changes and\n" + "load offset table into LinuxCNC?"): + return + self.offset_model.saveOffsetTable() + + @Slot() + def loadOffsetTable(self): + if not self.confirmAction("Do you want to re-load the offset table?\n" + "All unsaved changes will be lost."): + return + self.offset_model.loadOffsetTable() + +
[docs] @Slot() + def deleteSelectedOffset(self): + """Delete the currently selected item""" + current_row = self.selectedRow() + if current_row == -1: + # no row selected + return + + if not self.confirmAction("Are you sure you want to delete offset {}?".format(current_row)): + return + + self.offset_model.clearRow(current_row)
+ + # @Slot() + # def selectPrevious(self): + # """Select the previous item in the view.""" + # self.selectRow(self.selectedRow() - 1) + # return True + + # @Slot() + # def selectNext(self): + # """Select the next item in the view.""" + # self.selectRow(self.selectedRow() + 1) + # return True + +
[docs] @Slot() + def clearOffsetTable(self, confirm=True): + """Remove all items from the model""" + if confirm: + if not self.confirmAction("Do you want to delete the whole offsets table?"): + return + + self.offset_model.clearRows()
+ +
[docs] def selectedRow(self): + """Returns the row number of the currently selected row, or 0""" + return self.selectionModel().currentIndex().row()
+ + def confirmAction(self, message): + if not self._confirm_actions: + return True + + box = QMessageBox.question(self, + 'Confirm Action', + message, + QMessageBox.Yes, + QMessageBox.No) + if box == QMessageBox.Yes: + return True + else: + return False + + @Property(str) + def displayColumns(self): + return "".join(self._columns) + + @displayColumns.setter + def displayColumns(self, columns): + self._columns = [col for col in columns.upper() if col in 'XYZABCUVWR'] + self.offset_model.setColumns(self._columns) + self.itemDelegate().setColumns(self._columns) + + @Property(bool) + def confirmActions(self): + return self._confirm_actions + + @confirmActions.setter + def confirmActions(self, confirm): + self._confirm_actions = confirm + + @Property(QColor) + def currentRowColor(self): + return self.offset_model.current_row_color + + @currentRowColor.setter + def currentRowColor(self, color): + self.offset_model.current_row_color = color
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/input_widgets/recent_file_combobox.html b/_modules/qtpyvcp/widgets/input_widgets/recent_file_combobox.html new file mode 100644 index 000000000..18f0074d7 --- /dev/null +++ b/_modules/qtpyvcp/widgets/input_widgets/recent_file_combobox.html @@ -0,0 +1,184 @@ + + + + + + qtpyvcp.widgets.input_widgets.recent_file_combobox — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.input_widgets.recent_file_combobox

+import os
+
+from qtpy.QtWidgets import QComboBox
+
+from qtpyvcp.actions.program_actions import load as loadProgram
+from qtpyvcp.plugins import getPlugin
+from qtpyvcp.widgets.dialogs import getDialog
+
+
+
[docs]class RecentFileComboBox(QComboBox): + def __init__(self, parent=None): + super(RecentFileComboBox, self).__init__(parent) + + self.status = getPlugin('status') + + self.activated.connect(self.onItemActivated) + self.updateRecentFiles(self.status.recent_files) + + self.insertItem(0, 'No File Loaded', None) + self.setCurrentIndex(0) + self.status.recent_files.notify(self.updateRecentFiles) + + def updateRecentFiles(self, recent_files): + self.clear() + for file in recent_files: + self.addItem(os.path.basename(file), file) + + # Add separator and item to launch the file dialog + self.insertSeparator(len(self.status.recent_files.getValue())) + self.addItem("Browse for files ...", 'browse_files') + + def onItemActivated(self): + data = self.currentData() + if data == 'browse_files': + getDialog('open_file').show() + elif data is None: + pass + else: + loadProgram(data)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/input_widgets/setting_slider.html b/_modules/qtpyvcp/widgets/input_widgets/setting_slider.html new file mode 100644 index 000000000..1b571353c --- /dev/null +++ b/_modules/qtpyvcp/widgets/input_widgets/setting_slider.html @@ -0,0 +1,455 @@ + + + + + + qtpyvcp.widgets.input_widgets.setting_slider — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.input_widgets.setting_slider

+from qtpy.QtCore import Property
+from qtpy.QtWidgets import QLineEdit, QSlider, QSpinBox, QDoubleSpinBox, QCheckBox, QComboBox, QPushButton
+from qtpy.QtGui import QIntValidator, QDoubleValidator
+
+from qtpyvcp import SETTINGS
+from qtpyvcp.widgets import VCPWidget
+
+
+
[docs]class VCPAbstractSettingsWidget(VCPWidget): + def __init__(self): + super(VCPAbstractSettingsWidget, self).__init__() + self._setting = None + self._setting_name = '' + + @Property(str) + def settingName(self): + return self._setting_name + + @settingName.setter + def settingName(self, name): + self._setting_name = name
+ + +
[docs]class VCPSettingsLineEdit(QLineEdit, VCPAbstractSettingsWidget): + """Settings LineEdit""" + + DEFAULT_RULE_PROPERTY = 'Enable' + RULE_PROPERTIES = VCPAbstractSettingsWidget.RULE_PROPERTIES.copy() + RULE_PROPERTIES.update({ + 'Text': ['setText', str], + 'Value': ['setValue', float], + }) + + def __init__(self, parent): + super(VCPSettingsLineEdit, self).__init__(parent=parent) + self._setting_name = '' + self._text_format = '' + self._tmp_value = None + + self.returnPressed.connect(self.onReturnPressed) + + def formatValue(self, value): + if self._setting.value_type in (int, float): + return self._text_format.format(value) + + if isinstance(value, str): + return value + + else: + return str(value) + + def setValue(self, text): + if self._setting is not None: + value = self._setting.normalizeValue(text) + self.setDisplayValue(value) + self._setting.setValue(value) + else: + self._tmp_value = text + + def onReturnPressed(self): + self.clearFocus() + + def setDisplayValue(self, value): + self.blockSignals(True) + self.setText(self.formatValue(value)) + self.blockSignals(False) + +
[docs] def initialize(self): + self._setting = SETTINGS.get(self._setting_name) + if self._setting is not None: + + val = self._setting.getValue() + + validator = None + if type(val) == int: + validator = QIntValidator() + elif type(val) == float: + validator = QDoubleValidator() + + self.setValidator(validator) + + if self._tmp_value: + self.setDisplayValue(self._tmp_value) + self._setting.setValue(self._tmp_value) + else: + self.setDisplayValue(self._setting.getValue()) + + self._setting.notify(self.setDisplayValue) + + self.editingFinished.connect(self.onEditingFinished)
+ + def onEditingFinished(self): + value = self._setting.normalizeValue(self.text()) + self.setDisplayValue(value) + self._setting.setValue(value) + + @Property(str) + def textFormat(self): + return self._text_format + + @textFormat.setter + def textFormat(self, text_fmt): + if self._setting_name != "": + setting = SETTINGS.get(self._setting_name) + if setting: + str = text_fmt.format(setting.getValue()) + else: + return + + self._text_format = text_fmt
+ + +
[docs]class VCPSettingsSlider(QSlider, VCPAbstractSettingsWidget): + """Settings Slider + + Set action options like:: + + machine.jog.linear-speed + + """ + + DEFAULT_RULE_PROPERTY = 'Enable' + RULE_PROPERTIES = VCPAbstractSettingsWidget.RULE_PROPERTIES.copy() + RULE_PROPERTIES.update({ + 'Value': ['setValue', int], + }) + + def __init__(self, parent): + super(VCPSettingsSlider, self).__init__(parent=parent) + self._setting_name = '' + + def setDisplayValue(self, value): + self.blockSignals(True) + self.setValue(int(value)) + self.blockSignals(False) + +
[docs] def mouseDoubleClickEvent(self, event): + self.setValue(100)
+ + +
[docs] def initialize(self): + self._setting = SETTINGS.get(self._setting_name) + if self._setting is not None: + if self._setting.max_value is not None: + self.setMaximum(int(self._setting.max_value)) + if self._setting.min_value is not None: + self.setMinimum(int(self._setting.min_value)) + + self.setDisplayValue(self._setting.getValue()) + self._setting.notify(self.setDisplayValue) + self.valueChanged.connect(self._setting.setValue)
+ + +
[docs]class VCPSettingsSpinBox(QSpinBox, VCPAbstractSettingsWidget): + """Settings SpinBox""" + + DEFAULT_RULE_PROPERTY = 'Enable' + RULE_PROPERTIES = VCPAbstractSettingsWidget.RULE_PROPERTIES.copy() + RULE_PROPERTIES.update({ + 'Value': ['setValue', int], + }) + + def __init__(self, parent): + super(VCPSettingsSpinBox, self).__init__(parent=parent) + + def setDisplayValue(self, value): + self.blockSignals(True) + self.setValue(value) + self.blockSignals(False) + +
[docs] def initialize(self): + self._setting = SETTINGS.get(self._setting_name) + if self._setting is not None: + if self._setting.max_value is not None: + self.setMaximum(int(self._setting.max_value)) + if self._setting.min_value is not None: + self.setMinimum(int(self._setting.min_value)) + + self.setDisplayValue(self._setting.getValue()) + self._setting.notify(self.setDisplayValue) + self.valueChanged.connect(self._setting.setValue)
+ + +
[docs]class VCPSettingsDoubleSpinBox(QDoubleSpinBox, VCPAbstractSettingsWidget): + """Settings DoubleSpinBox""" + + DEFAULT_RULE_PROPERTY = 'Enable' + RULE_PROPERTIES = VCPAbstractSettingsWidget.RULE_PROPERTIES.copy() + RULE_PROPERTIES.update({ + 'Value': ['setValue', float], + }) + + def __init__(self, parent): + super(VCPSettingsDoubleSpinBox, self).__init__(parent=parent) + + def setDisplayValue(self, value): + self.blockSignals(True) + self.setValue(value) + self.blockSignals(False) + + def editingEnded(self): + self._setting.setValue(self.value()) + +
[docs] def initialize(self): + self._setting = SETTINGS.get(self._setting_name) + if self._setting is not None: + if self._setting.max_value is not None: + self.setMaximum(self._setting.max_value) + if self._setting.min_value is not None: + self.setMinimum(self._setting.min_value) + + self.setDisplayValue(self._setting.getValue()) + self._setting.notify(self.setDisplayValue) + #self.valueChanged.connect(self._setting.setValue) + self.editingFinished.connect(self.editingEnded)
+ + +
[docs]class VCPSettingsCheckBox(QCheckBox, VCPAbstractSettingsWidget): + """Settings CheckBox""" + + DEFAULT_RULE_PROPERTY = 'Enable' + RULE_PROPERTIES = VCPAbstractSettingsWidget.RULE_PROPERTIES.copy() + RULE_PROPERTIES.update({ + 'Checked': ['setChecked', bool], + }) + + def __init__(self, parent): + super(VCPSettingsCheckBox, self).__init__(parent=parent) + + def setDisplayChecked(self, checked): + self.blockSignals(True) + self.setChecked(checked) + self.blockSignals(False) + +
[docs] def initialize(self): + self._setting = SETTINGS.get(self._setting_name) + if self._setting is not None: + + value = self._setting.getValue() + + self.setDisplayChecked(value) + self.toggled.emit(value) + + self._setting.notify(self.setDisplayChecked) + self.toggled.connect(self._setting.setValue)
+ + +
[docs]class VCPSettingsPushButton(QPushButton, VCPAbstractSettingsWidget): + """Settings PushButton""" + + DEFAULT_RULE_PROPERTY = 'Enable' + RULE_PROPERTIES = VCPAbstractSettingsWidget.RULE_PROPERTIES.copy() + RULE_PROPERTIES.update({ + 'Text': ['setText', str], + 'Checked': ['setChecked', bool], + }) + + def __init__(self, parent): + super(VCPSettingsPushButton, self).__init__(parent=parent) + self.setCheckable(True) + self.setEnabled(False) + + def setDisplayChecked(self, checked): + self.blockSignals(True) + self.setChecked(checked) + self.blockSignals(False) + +
[docs] def initialize(self): + self._setting = SETTINGS.get(self._setting_name) + if self._setting is not None: + self.setEnabled(True) + + value = self._setting.getValue() + + self.setDisplayChecked(value) + self.toggled.emit(value) + + self._setting.notify(self.setDisplayChecked) + self.toggled.connect(self._setting.setValue)
+ + +
[docs]class VCPSettingsComboBox(QComboBox, VCPAbstractSettingsWidget): + """Settings ComboBox""" + + DEFAULT_RULE_PROPERTY = 'Enable' + + def __init__(self, parent): + super(VCPSettingsComboBox, self).__init__(parent=parent) + + def setDisplayIndex(self, index): + self.blockSignals(True) + self.setCurrentIndex(index) + self.blockSignals(False) + +
[docs] def initialize(self): + self._setting = SETTINGS.get(self._setting_name) + if self._setting is not None: + + value = self._setting.getValue() + + options = self._setting.enum_options + if isinstance(options, list): + for option in options: + self.addItem(option) + + self.setDisplayIndex(value) + self.currentIndexChanged.emit(value) + + self._setting.notify(self.setDisplayIndex) + self.currentIndexChanged.connect(self._setting.setValue)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/input_widgets/tool_table.html b/_modules/qtpyvcp/widgets/input_widgets/tool_table.html new file mode 100644 index 000000000..99098e8d1 --- /dev/null +++ b/_modules/qtpyvcp/widgets/input_widgets/tool_table.html @@ -0,0 +1,548 @@ + + + + + + qtpyvcp.widgets.input_widgets.tool_table — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.input_widgets.tool_table

+from qtpy.QtCore import Qt, Slot, Signal, Property, QModelIndex, QSortFilterProxyModel
+from qtpy.QtGui import QStandardItemModel, QColor, QBrush
+from qtpy.QtWidgets import QTableView, QStyledItemDelegate, QDoubleSpinBox, \
+     QSpinBox, QLineEdit, QMessageBox
+
+from qtpyvcp.actions.machine_actions import issue_mdi
+
+from qtpyvcp.utilities.logger import getLogger
+from qtpyvcp.plugins import getPlugin
+from qtpyvcp.utilities.settings import connectSetting, getSetting
+
+LOG = getLogger(__name__)
+
+
+
[docs]class ItemDelegate(QStyledItemDelegate): + + def __init__(self, columns): + super(ItemDelegate, self).__init__() + + self._columns = columns + self._padding = ' ' * 2 + + def setColumns(self, columns): + self._columns = columns + +
[docs] def displayText(self, value, locale): + + if type(value) == float: + return f"{value:.4f}" + if type(value) == str: + return f"{self._padding}{value}" + + return f"{self._padding}{value}"
+ +
[docs] def createEditor(self, parent, option, index): + # ToDo: set dec placed for IN and MM machines + col = self._columns[index.column()] + + if col == 'R': + editor = QLineEdit(parent) + editor.setFrame(False) + margins = editor.textMargins() + padding = editor.fontMetrics().width(self._padding) + 1 + margins.setLeft(margins.left() + padding) + editor.setTextMargins(margins) + return editor + + elif col in 'TPQ': + editor = QSpinBox(parent) + editor.setFrame(False) + editor.setAlignment(Qt.AlignCenter) + if col == 'Q': + editor.setMaximum(9) + else: + editor.setMaximum(99999) + return editor + + elif col in 'XYZABCUVWD': + editor = QDoubleSpinBox(parent) + editor.setFrame(False) + editor.setAlignment(Qt.AlignCenter) + editor.setDecimals(4) + # editor.setStepType(QSpinBox.AdaptiveDecimalStepType) + editor.setProperty('stepType', 1) # stepType was added in 5.12 + + min_range = getSetting('offset_table.min_range').value + max_range = getSetting('offset_table.max_range').value + + if min_range and max_range: + editor.setRange(min_range, max_range) + else: + editor.setRange(-1000, 1000) + + return editor + + elif col in 'IJ': + editor = QDoubleSpinBox(parent) + editor.setFrame(False) + editor.setAlignment(Qt.AlignCenter) + editor.setMaximum(360.0) + editor.setMinimum(0.0) + editor.setDecimals(4) + # editor.setStepType(QSpinBox.AdaptiveDecimalStepType) + editor.setProperty('stepType', 1) # stepType was added in 5.12 + return editor + + return None
+ + +
[docs]class ToolModel(QStandardItemModel): + def __init__(self, parent=None): + super(ToolModel, self).__init__(parent) + + self.status = getPlugin('status') + self.stat = self.status.stat + self.tt = getPlugin('tooltable') + + self.current_tool_color = QColor(Qt.darkGreen) + self.current_tool_bg = None + + self._columns = self.tt.columns + self._column_labels = self.tt.COLUMN_LABELS + + self._tool_table = self.tt.getToolTable() + + self.setColumnCount(self.columnCount()) + self.setRowCount(1000) # (self.rowCount()) + + self.status.tool_in_spindle.notify(self.refreshModel) + self.tt.tool_table_changed.connect(self.updateModel) + + def refreshModel(self): + # refresh model so current tool gets highlighted + self.beginResetModel() + self.endResetModel() + + def updateModel(self, tool_table): + # update model with new data + self.beginResetModel() + self._tool_table = tool_table + self.endResetModel() + + def setColumns(self, columns): + self._columns = columns + self.setColumnCount(len(columns)) + +
[docs] def headerData(self, section, orientation, role=Qt.DisplayRole): + if role == Qt.DisplayRole and orientation == Qt.Horizontal: + return self._column_labels[self._columns[section]] + + return QStandardItemModel.headerData(self, section, orientation, role)
+ +
[docs] def columnCount(self, parent=None): + return len(self._columns)
+ +
[docs] def rowCount(self, parent=None): + return len(self._tool_table) - 1
+ +
[docs] def flags(self, index): + return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
+ +
[docs] def data(self, index, role=Qt.DisplayRole): + if role == Qt.DisplayRole or role == Qt.EditRole: + key = self._columns[index.column()] + tnum = sorted(self._tool_table)[index.row() + 1] + return self._tool_table[tnum][key] + + elif role == Qt.TextAlignmentRole: + col = self._columns[index.column()] + if col == 'R': # Remark + return Qt.AlignVCenter | Qt.AlignLeft + elif col in 'TPQ': # Integers (Tool, Pocket, Orient) + return Qt.AlignVCenter | Qt.AlignCenter + else: # All the other floats + return Qt.AlignVCenter | Qt.AlignRight + + elif role == Qt.TextColorRole: + tnum = sorted(self._tool_table)[index.row() + 1] + if self.stat.tool_in_spindle == tnum: + return QBrush(self.current_tool_color) + else: + return QStandardItemModel.data(self, index, role) + + elif role == Qt.BackgroundRole and self.current_tool_bg is not None: + tnum = sorted(self._tool_table)[index.row() + 1] + if self.stat.tool_in_spindle == tnum: + return QBrush(self.current_tool_bg) + else: + return QStandardItemModel.data(self, index, role) + + return QStandardItemModel.data(self, index, role)
+ +
[docs] def setData(self, index, value, role): + key = self._columns[index.column()] + tnum = sorted(self._tool_table)[index.row() + 1] + self._tool_table[tnum][key] = value + return True
+ + def removeTool(self, row): + self.beginRemoveRows(QModelIndex(), row, row) + tnum = sorted(self._tool_table)[row + 1] + del self._tool_table[tnum] + self.endRemoveRows() + return True + + def addTool(self): + try: + tnum = sorted(self._tool_table)[-1] + 1 + except IndexError: + tnum = 1 + + row = len(self._tool_table) - 1 + + if row == 1000: + # max 1000 tools + return False + + self.beginInsertRows(QModelIndex(), row, row) + self._tool_table[tnum] = self.tt.newTool(tnum=tnum) + self.endInsertRows() + return True + +
[docs] def toolDataFromRow(self, row): + """Returns dictionary of tool data""" + tnum = sorted(self._tool_table)[row + 1] + return self._tool_table[tnum]
+ + def saveToolTable(self): + self.tt.saveToolTable(self._tool_table, self._columns) + return True + + def clearToolTable(self): + self.beginRemoveRows(QModelIndex(), 0, 100) + # delete all but the spindle, which can't be deleted + self._tool_table = {0: self._tool_table[0]} + self.endRemoveRows() + return True + + def loadToolTable(self): + # the tooltable plugin will emit the tool_table_changed signal + # so we don't need to do any more here + self.tt.loadToolTable() + return True
+ + +
[docs]class ToolTable(QTableView): + toolSelected = Signal(int) + + def __init__(self, parent=None): + super(ToolTable, self).__init__(parent) + + self.clicked.connect(self.onClick) + + self.tool_model = ToolModel(self) + + self.item_delegate = ItemDelegate(columns=self.tool_model._columns) + self.setItemDelegate(self.item_delegate) + + self.proxy_model = QSortFilterProxyModel() + self.proxy_model.setFilterKeyColumn(0) + self.proxy_model.setSourceModel(self.tool_model) + + self.setModel(self.proxy_model) + + # Properties + self._columns = self.tool_model._columns + self._confirm_actions = False + self._current_tool_color = QColor('sage') + self._current_tool_bg = None + + # Appearance/Behaviour settings + self.setSortingEnabled(True) + self.verticalHeader().hide() + self.setAlternatingRowColors(True) + self.setSelectionBehavior(QTableView.SelectRows) + self.setSelectionMode(QTableView.SingleSelection) + self.horizontalHeader().setStretchLastSection(True) + self.horizontalHeader().setSortIndicator(0, Qt.AscendingOrder) + + @Slot() + def saveToolTable(self): + if not self.confirmAction("Do you want to save changes and\n" + "load tool table into LinuxCNC?"): + return + self.tool_model.saveToolTable() + + @Slot() + def loadToolTable(self): + if not self.confirmAction("Do you want to re-load the tool table?\n" + "All unsaved changes will be lost."): + return + self.tool_model.loadToolTable() + +
[docs] @Slot() + def deleteSelectedTool(self): + """Delete the currently selected item""" + current_row = self.selectedRow() + if current_row == -1: + # no row selected + return + + tdata = self.tool_model.toolDataFromRow(current_row) + tnum = tdata['T'] + + # should not delete tool if currently loaded in spindle. Warn user + if tnum == self.tool_model.stat.tool_in_spindle: + + box = QMessageBox(QMessageBox.Warning, + "Can't delete current tool!", + "Tool #{} is currently loaded in the spindle.\n" + "Please remove tool from spindle and try again.".format(tnum), + QMessageBox.Ok, + parent=self) + box.show() + return False + + if not self.confirmAction('Are you sure you want to delete T{tdata[T]}?\n' + '"{tdata[R]}"'.format(tdata=tdata)): + return + + self.tool_model.removeTool(current_row)
+ +
[docs] @Slot() + def selectPrevious(self): + """Select the previous item in the view.""" + self.selectRow(self.selectedRow() - 1) + return True
+ +
[docs] @Slot() + def selectNext(self): + """Select the next item in the view.""" + self.selectRow(self.selectedRow() + 1) + return True
+ +
[docs] @Slot() + def clearToolTable(self, confirm=True): + """Remove all items from the model""" + if confirm: + if not self.confirmAction("Do you want to delete the whole tool table?"): + return + + self.tool_model.clearToolTable()
+ +
[docs] @Slot() + def addTool(self): + """Appends a new item to the model""" + self.tool_model.addTool() + self.selectRow(self.tool_model.rowCount() - 1)
+ +
[docs] @Slot() + def loadSelectedTool(self): + """Loads the currently selected tool""" + # see: https://forum.linuxcnc.org/41-guis/36042?start=50#151820 + current_row = self.selectedRow() + if current_row == -1: + # no row selected + return + + tnum = self.tool_model.toolDataFromRow(current_row)['T'] + issue_mdi("T%s M6" % tnum)
+ +
[docs] def selectedRow(self): + """Returns the row number of the currently selected row, or 0""" + tool_no = self.selectionModel().currentIndex().row() + return tool_no
+ + def onClick(self, index): + row = index.row() + tnum = self.tool_model.toolDataFromRow(row)['T'] + + self.toolSelected.emit(tnum) + + def confirmAction(self, message): + if not self._confirm_actions: + return True + + box = QMessageBox.question(self, + 'Confirm Action', + message, + QMessageBox.Yes, + QMessageBox.No) + + if box == QMessageBox.Yes: + return True + else: + return False + + @Property(bool) + def confirmActions(self): + return self._confirm_actions + + @confirmActions.setter + def confirmActions(self, confirm): + self._confirm_actions = confirm + + @Property(QColor) + def currentToolColor(self): + return self.tool_model.current_tool_color + + @currentToolColor.setter + def currentToolColor(self, color): + self.tool_model.current_tool_color = color + + @Property(QColor) + def currentToolBackground(self): + return self.tool_model.current_tool_bg or QColor() + + @currentToolBackground.setter + def currentToolBackground(self, color): + self.tool_model.current_tool_bg = color + + + def insertToolAbove(self): + # it does not make sense to insert tools, since the numbering + # of all the other tools would have to change. + self.addTool() + raise DeprecationWarning("insertToolAbove() will be removed in " + "the future, use addTool() instead") + + def insertToolBelow(self): + self.addTool() + raise DeprecationWarning("insertToolBelow() will be removed in " + "the future, use addTool() instead")
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/menus/homing_menu.html b/_modules/qtpyvcp/widgets/menus/homing_menu.html new file mode 100644 index 000000000..3064d2059 --- /dev/null +++ b/_modules/qtpyvcp/widgets/menus/homing_menu.html @@ -0,0 +1,187 @@ + + + + + + qtpyvcp.widgets.menus.homing_menu — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.menus.homing_menu

+"""Homing Menu Provider"""
+
+import linuxcnc
+from qtpy.QtWidgets import QMenu, QAction
+
+from qtpyvcp import actions
+from qtpyvcp.plugins import getPlugin
+from qtpyvcp.utilities.info import Info
+
+INFO = Info()
+
+
+
[docs]class HomingMenu(QMenu): + """Homing Menu Provider + + Args: + parent (QWidget, optional) : The menus parent. Default to None. + axes (list, optional) : List of axes for which to show a homing action. + If not specified the axis letter list from the INI file will be used. + + ToDO: + Add un-homing actions if the axis is already homed. + """ + def __init__(self, parent=None, axes=None): + super(HomingMenu, self).__init__(parent) + + self.status = getPlugin('status') + + home_all = QAction(parent=self, text="Home &All") + actions.bindWidget(home_all, 'machine.home.all') + self.addAction(home_all) + + # add homing actions for each axis + for aletter in axes or INFO.AXIS_LETTER_LIST: + home_axis = QAction(parent=self, + text="Home &{}".format(aletter.upper())) + actions.bindWidget(home_axis, 'machine.home.axis:{}'.format(aletter.lower())) + self.addAction(home_axis) + home_axis.setVisible(True) + + self.setEnabled(self.status.stat.state == linuxcnc.STATE_ON) + self.status.on.notify(lambda on: self.setEnabled(on))
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/qtpyvcp/widgets/recent_files_menu.html b/_modules/qtpyvcp/widgets/recent_files_menu.html new file mode 100644 index 000000000..f44914418 --- /dev/null +++ b/_modules/qtpyvcp/widgets/recent_files_menu.html @@ -0,0 +1,211 @@ + + + + + + qtpyvcp.widgets.recent_files_menu — QtPyVCP 4.0 + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for qtpyvcp.widgets.recent_files_menu

+import os
+from qtpy.QtWidgets import QMenu, QAction
+
+from qtpyvcp import actions
+from qtpyvcp.plugins import getPlugin
+from qtpyvcp.widgets.dialogs import showDialog
+
+
[docs]class RecentFilesMenu(QMenu): + """Recent Files Menu + + Recent files menu provider. + + Args: + parent (QWidget, optional) : The menus parent. Default to None. + files (list, optional) : List of initial files in the menu. Defaults to None. + max_files (int, optional) : Max number of files to show. Defaults to 10. + + Example: + + YAML config:: + + main_window: + menu: + - title: File + items: + - title: &Recent Files + provider: qtpyvcp.widgets.recent_files_menu:RecentFilesMenu + kwargs: # optional keyword arguments to pass to the constructor + max_files: 15 + """ + def __init__(self, parent=None, files=None, max_files=10): + super(RecentFilesMenu, self).__init__(parent) + + self._actions = [] + + self.status = getPlugin('status') + + for i in range(max_files): + action = QAction(parent=self, + visible=False, + triggered=lambda: actions.program.load(self.sender().data()), + ) + + self._actions.append(action) + self.addAction(action) + + self.addSeparator() + + action = QAction(parent=self, + text='Browse for files ...', + triggered=lambda: showDialog('open_file'), + ) + + self.addAction(action) + + self.update(files or self.status.recent_files()) + self.status.recent_files.notify(self.update) + +
[docs] def update(self, files): + for i, fname in enumerate(files): + + text = "&{} {}".format(i + 1, os.path.basename(fname)) + action = self._actions[i] + action.setText(text) + action.setData(fname) + action.setVisible(True)
+
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_sources/404.rst.txt b/_sources/404.rst.txt new file mode 100644 index 000000000..739a38362 --- /dev/null +++ b/_sources/404.rst.txt @@ -0,0 +1,10 @@ +:orphan: + +=========================== + 404 Error - Page Not Found +=========================== + +The requested page has either moved, or does not exist. +Please try using the links from the sidebar to find what you are looking for. + +If you think this page *should* exit, please contact one of the site administrators or open an issue on GitHub. diff --git a/_sources/acknowledgements.rst.txt b/_sources/acknowledgements.rst.txt new file mode 100644 index 000000000..f8344b55f --- /dev/null +++ b/_sources/acknowledgements.rst.txt @@ -0,0 +1,18 @@ +Acknowledgements +================ + +The QtPyVCP team would like to acknowledge the following projects +which have been particularly valuable as sources of inspiration and +code snippets. + +PyDM +---- + +| **Project URL:** https://github.com/slaclab/pydm + + +QtVCP +----- + +| **Project URL:** Included with LinuxCNC. + diff --git a/_sources/actions/action_helpers.rst.txt b/_sources/actions/action_helpers.rst.txt new file mode 100644 index 000000000..3add24c5b --- /dev/null +++ b/_sources/actions/action_helpers.rst.txt @@ -0,0 +1,9 @@ +============== +Action Helpers +============== + +.. automodule:: qtpyvcp.actions + :members: + +.. automodule:: qtpyvcp.actions.base_actions + :members: diff --git a/_sources/actions/coolant_actions.rst.txt b/_sources/actions/coolant_actions.rst.txt new file mode 100644 index 000000000..8d040a4ac --- /dev/null +++ b/_sources/actions/coolant_actions.rst.txt @@ -0,0 +1,6 @@ +=============== +Coolant Actions +=============== + +.. automodule:: qtpyvcp.actions.coolant_actions + :members: diff --git a/_sources/actions/index.rst.txt b/_sources/actions/index.rst.txt new file mode 100644 index 000000000..2595ca6ef --- /dev/null +++ b/_sources/actions/index.rst.txt @@ -0,0 +1,14 @@ +======= +Actions +======= + +.. toctree:: + :maxdepth: 1 + + action_helpers + machine_actions + program_actions + spindle_actions + coolant_actions + tool_actions + diff --git a/_sources/actions/machine_actions.rst.txt b/_sources/actions/machine_actions.rst.txt new file mode 100644 index 000000000..f6c266d7f --- /dev/null +++ b/_sources/actions/machine_actions.rst.txt @@ -0,0 +1,6 @@ +=============== +Machine Actions +=============== + +.. automodule:: qtpyvcp.actions.machine_actions + :members: diff --git a/_sources/actions/program_actions.rst.txt b/_sources/actions/program_actions.rst.txt new file mode 100644 index 000000000..00e03ff28 --- /dev/null +++ b/_sources/actions/program_actions.rst.txt @@ -0,0 +1,6 @@ +=============== +Program Actions +=============== + +.. automodule:: qtpyvcp.actions.program_actions + :members: diff --git a/_sources/actions/spindle_actions.rst.txt b/_sources/actions/spindle_actions.rst.txt new file mode 100644 index 000000000..30c359e89 --- /dev/null +++ b/_sources/actions/spindle_actions.rst.txt @@ -0,0 +1,6 @@ +=============== +Spindle Actions +=============== + +.. automodule:: qtpyvcp.actions.spindle_actions + :members: diff --git a/_sources/actions/tool_actions.rst.txt b/_sources/actions/tool_actions.rst.txt new file mode 100644 index 000000000..66f8553e4 --- /dev/null +++ b/_sources/actions/tool_actions.rst.txt @@ -0,0 +1,6 @@ +============ +Tool Actions +============ + +.. automodule:: qtpyvcp.actions.tool_actions + :members: diff --git a/_sources/application.rst.txt b/_sources/application.rst.txt new file mode 100644 index 000000000..5f181b3c1 --- /dev/null +++ b/_sources/application.rst.txt @@ -0,0 +1,6 @@ +=========== +Application +=========== + +.. automodule:: qtpyvcp.app.application + :members: diff --git a/_sources/components/action_buttons.rst.txt b/_sources/components/action_buttons.rst.txt new file mode 100644 index 000000000..3087ddb29 --- /dev/null +++ b/_sources/components/action_buttons.rst.txt @@ -0,0 +1,257 @@ +============== +Action Buttons +============== + +**Syntax** + +Action button syntax for the `actionName` is +`group.action.subaction:argument=item`. Some action items take an optional +argument like ``spindle.override.reset:2`` to reset the override for a +spindle number 2. + +------------------- +**Coolant Actions** +------------------- + +**Flood** `actionNames` for `ActionButtons` +:: + + coolant.flood.off + coolant.flood.on + coolant.flood.toggle + +**Mist** `actionNames` for `ActionButtons` +:: + + coolant.mist.off + coolant.mist.on + coolant.mist.toggle + +------------------- +**Machine Actions** +------------------- + +**E Stop** `actionNames` for `ActionButtons` +:: + + machine.estop.activate + machine.estop.reset + machine.estop.toggle + +**Feed Override** `actionNames` for `ActionButtons` +:: + + machine.feed-override.disable + machine.feed-override.enable + machine.feed-override.reset + machine.feed-override.set:value + machine.feed-override.toggle-enable + +**Home** `actionNames` for `ActionButtons` +:: + + machine.home.all + machine.home.axis:axis letter + machine.home.joint:joint number + +**Issue MDI** `actionNames` for `ActionButtons` +Note: Use the MDIButton for MDI commands +:: + + machine.issue-mdi:command + +**Jog** `actionNames` for `ActionButtons` +:: + + machine.jog.axis.axis letter,direction,speed,distance + machine.jog.set-angular-speed.value + machine.jog.set-increment.raw increment + machine.jog.set-jog-continuous + machine.jog.set-linear-speed.value + +**Jog Mode** `actionNames` for `ActionButtons` +:: + + machine.jog-mode.continuous + machine.jog-mode.incremental + machine.jog-mode.toggle + +**Max Velocity** `actionNames` for `ActionButtons` +:: + + machine.max-velocity.set + machine.max-velocity.reset + +**Mode** `actionNames` for `ActionButtons` +:: + + machine.mode.auto + machine.mode.manual + machine.mode.toggle + +**Power** `actionNames` for `ActionButtons` +:: + + machine.power.off + machine.power.on + machine.power.toggle + +**UnHome** `actionNames` for `ActionButtons` +:: + + machine.unhome.all + machine.unhome.axis:axis letter + machine.unhome.joint:joint number + +------------------- +**Program Actions** +------------------- + +**Abort** `actionNames` for `ActionButtons` +:: + + program.abort + +**Block Delete** `actionNames` for `ActionButtons` +:: + + program.block-delete.off + program.block-delete.on + program.block-delete.toggle + +**Optional Skip** `actionNames` for `ActionButtons` +:: + + program. + +**Optional Stop** `actionNames` for `ActionButtons` +:: + + program.option-stop.off + program.optional-stop.on + program.optional-stop.toggle + +**Pause Program** `actionNames` for `ActionButtons` +:: + + program.pause + +**Resume Program** `actionNames` for `ActionButtons` +:: + + program.resume + +**Run Program** `actionNames` for `ActionButtons` + +Run has an optional argument `start line`, replace `n` with the line number. +:: + + program.run + program.run:n + +**Step Program** `actionNames` for `ActionButtons` +:: + + program.step + +------------------- +**Spindle Actions** +------------------- + +Spindle Actions have an optional argument `spindle`, if left off spindle 0 is +assumed. To specifiy a spindle replace `spindle` in the examples with the +spindle number. + + +**Brake** `actionNames` for `ActionButtons` +:: + + spindle.brake.off + spindle.brake.off:spindle + spindle.brake.on + spindle.brake.on:spindle + spindle.brake.toggle + spindle.brake.toggle:spindle + +**Faster** `actionNames` for `ActionButtons` + +Increase spindle speed by 100rpm +:: + + spindle.faster + spindle.faster:spindle + +**Forward** `actionNames` for `ActionButtons` + +Turn spindle on in the forward direction +:: + + spindle.forward + spindle.forward:speed + spindle.forward:speed,spindle + +**Off** `actionNames` for `ActionButtons` +:: + + spindle.off + spindle.off:spindle + +**Override** `actionNames` for `ActionButtons` + +Set spindle override percentage. Used with an ActionSlider you can omit the +speed. +:: + + spindle.override + spindle.override:speed + spindle.override:speed,spindle + +**Reverse** `actionNames` for `ActionButtons` +:: + + spindle.reverse + spindle.reverse:speed + spindle.reverse:speed,spindle + +**Slower** `actionNames` for `ActionButtons` + +Decrease spindle speed by 100rpm +:: + + spindle.slower + spindle.slower:spindle + +---------------- +**Tool Actions** +---------------- + +**Calibration** `actionNames` for `ActionButtons` +:: + + tool_actions.calibration + +**Halmeter** `actionNames` for `ActionButtons` +:: + + tool_actions.halmeter + +**Halscope** `actionNames` for `ActionButtons` +:: + + tool_actions.halscope + +**Halshow** `actionNames` for `ActionButtons` +:: + + tool_actions.halshow + +**Simulate Probe** `actionNames` for `ActionButtons` +:: + + tool_actions.simulate_probe + +**Status** `actionNames` for `ActionButtons` +:: + + tool_actions.status + diff --git a/_sources/components/action_sliders.rst.txt b/_sources/components/action_sliders.rst.txt new file mode 100644 index 000000000..4b4bf6559 --- /dev/null +++ b/_sources/components/action_sliders.rst.txt @@ -0,0 +1,13 @@ +============== +Action Sliders +============== + +**Feed Override** `actionNames` for `ActionSliders` +:: + + machine.feed-override.set + +**Maximum Velocity Override** `actionNames` for `ActionSliders` +:: + + machine.max-velocity.set diff --git a/_sources/components/backplot.rst.txt b/_sources/components/backplot.rst.txt new file mode 100644 index 000000000..ad7fa5232 --- /dev/null +++ b/_sources/components/backplot.rst.txt @@ -0,0 +1,101 @@ +============ +VTK Backplot +============ + +The VTK Backplot is a very fast plot renderer and is the recommended one to use. + +This example is using the :doc:`../tutorials/vcp_template` to create a blank +screen. + +.. image:: images/backplot-01.png + :align: center + +Add a `Frame` and select frameShape `Box` then drag another `Frame` inside the +first one and select frameShape `Box` and drag a `VTKBackPlot` into the first +frame and save your work. + +.. image:: images/backplot-02.png + :align: center + +Drag a couple of `Push Buttons` into the second frame. Right click inside the +first frame and select `Lay Out Vertically`. + +.. image:: images/backplot-03.png + :align: center + +Double click on the first button and change the name to `P`, next change the +objectName to `plotP`. In the Signal/Slot Editor tab left click on the green +plus symbol to create a new Signal/Slot. + +.. image:: images/backplot-04.png + :align: center + +Set the sender to `plotP`, set the Signal to `clicked()`, set the Receiver to +`vtkbackplot` and set the Slot to `setViewP()`. Now when we run the example +the first button will set the view to Isometric Projection. + +.. image:: images/backplot-05.png + :align: center + +Controlling the display of the boundries and axes information can be done in the +configuration ini file located in `username/linuxcnc/configs/configuration_name`. +Descriptions of the options for the ini file are in +:doc:`../configuration/ini_options`. + +Turning off everything except `machine boundry` in the ini file. + +.. image:: images/backplot-06.png + :align: center + +And the result. + +.. image:: images/backplot-07.png + :align: center + +In the same mannor as before we can drag more buttons into the frame and set the +views for `X, Y, Z and Path`. + +.. image:: images/backplot-08.png + :align: center + +Touch screens only have the left mouse button and the default action for the +left mouse button is to roll the disply. In order to pan the display we need to +add a button for Pan/Roll. Make sure to check off `checkable` so the function +will work. + +The big difference with this buttons signal is it must be `clicked(bool)` for +the `enable_panning` slot to show up. + +To change the text of the button to reflect the current status we need to do a +bit of python. Open the +`username/configuration_name/configuration_name/mainwindow.py` file. Add the +`connect` line to tie the clicked action to the function. Then add the +`toggleRollPan` function. + +:: + + class MyMainWindow(VCPMainWindow): + """Main window class for the VCP.""" + def __init__(self, *args, **kwargs): + super(MyMainWindow, self).__init__(*args, **kwargs) + + self.plotRPBtn.clicked.connect(self.toggleRollPan) + + def toggleRollPan(self): + if self.plotRPBtn.isChecked(): + self.plotRPBtn.setText('Pan') + else: + self.plotRPBtn.setText('Roll') + +Add a new signal with the sender plotRPBtn, signal clicked(bool), receiver +vtkbqackplot, slot enable_panning(bool). + +.. image:: images/backplot-09.png + :align: center + +When we run the example and click or press the `Roll` button it toggles to the +`Pan` function and using a touch screen you can now pan the backplot. + +.. image:: images/backplot-10.png + :align: center + diff --git a/_sources/components/containers.rst.txt b/_sources/components/containers.rst.txt new file mode 100644 index 000000000..194f80e76 --- /dev/null +++ b/_sources/components/containers.rst.txt @@ -0,0 +1,63 @@ +========== +Containers +========== + +QtPyVCP containers allow you to set rules for the objects inside of the +container. For example if you had a tool in spindle offset tab you could put a +container in the tab and set the rule to only enable it if a tool is loaded in +the spindle. + +VCP Widget +^^^^^^^^^^ + +The `VCP Widget` is a transparent container. You can specify the layout +in a container by dropping another widget into the container then right click in +the `VCP Widget` in an empty spot and select the layout you want. + + +VCP Frame +^^^^^^^^^ + +The `VCP Frame` container usually has a frame around it. This can be useful to help +you focus on a section and to differentiate controls from each other. + +Usage +^^^^^ + +In the following figure there are two QtPyVCP containers, on the right is a +`VCPFrame` and on the left a `VCPWidget`. + +.. image:: images/containers-01.png + :align: center + :scale: 40 % + +In order to add a layout to a container you first have to put something in the +container. Any widget will work, after you drop the widget in the container +right click in the container but not on the widget and select Layout, then pick +the layout you want. + +.. image:: images/containers-02.png + :align: center + :scale: 40 % + +To set a rule for the container double click on the container or right click on +the container and select `Edit Widget Rules`. + +.. image:: images/containers-03.png + :align: center + :scale: 40 % + +Rules that are available for containers are: + +* Enable +* None +* Style Class +* Style Sheet +* Visible + +.. image:: images/containers-04.png + :align: center + :scale: 60 % + + + diff --git a/_sources/components/index.rst.txt b/_sources/components/index.rst.txt new file mode 100644 index 000000000..579ab39ec --- /dev/null +++ b/_sources/components/index.rst.txt @@ -0,0 +1,17 @@ +========== +Components +========== + +QtPyVCP components available in Qt Designer + +.. toctree:: + :maxdepth: 1 + + action_buttons + action_sliders + backplot + containers + stacked_widget + status_items + subcallbutton + diff --git a/_sources/components/stacked_widget.rst.txt b/_sources/components/stacked_widget.rst.txt new file mode 100644 index 000000000..06b89cf8f --- /dev/null +++ b/_sources/components/stacked_widget.rst.txt @@ -0,0 +1,80 @@ +================== +VCP Stacked Widget +================== + +The `VCP Stacked Widget` is a container with multiple pages. The pages can be +accessed with buttons or changed by Python code as needed. + +The `VCP Stacked Widget` also has `Rules` you can set to control the following. + +* Enable +* Style Class +* Style Sheet +* Visible +* Current Index + +For more information about Rules see the :doc:`../tutorials/widget_rules` + +To open the `Rules Editor` double left click on the `VCP Stacked Widget`. + +.. image:: images/stacked-01.png + :align: center + +This example is using the :doc:`../tutorials/vcp_template` to create a blank +screen. + +Start by dragging a `VCPStackedWidget` and a `VCPFrame` into the mainwindow. +Change the frameShape to Box so you can see them better. + +Notice in the upper right corner of the stacked widget there are a left and +right arrow symbol. This is how you navigate through the pages while adding +widgets. + +.. image:: images/stacked-02.png + :align: center + +Changing pages on a stacked widget is done by setting the current index. This +can be done with a rule or in the mainwindow.py file. You can have navigation +buttons to select the page to show. + +Add a couple of push buttons and name them `page0` and `page1`. Remember the +object name must be unique so if you have `page_0` as the name of one of your +stacked widget pages you can't use that again. Next add a label to each page so +we can tell the pages change when we press the buttons. + +.. image:: images/stacked-03.png + :align: center + +In the mainwindow.py file we add the following highlighted lines of code. Be +sure to check indentation carefully. + +.. code-block:: python + :emphasize-lines: 11,12,16,17,19,20 + + from qtpyvcp.widgets.form_widgets.main_window import VCPMainWindow + + # Setup logging + from qtpyvcp.utilities import logger + LOG = logger.getLogger('qtpyvcp.' + __name__) + + class MyMainWindow(VCPMainWindow): + """Main window class for the VCP.""" + def __init__(self, *args, **kwargs): + super(MyMainWindow, self).__init__(*args, **kwargs) + self.page0.clicked.connect(self.setPage0) + self.page1.clicked.connect(self.setPage1) + + # add any custom methods here + + def setPage0(self): + self.stackedwidget.setCurrentIndex(0) + + def setPage1(self): + self.stackedwidget.setCurrentIndex(1) + +The `clicked.connect()` makes a connection between the button and the method. So +when you click a button the connected method runs the code to change pages. + +.. image:: images/stacked-04.png + :align: center + diff --git a/_sources/components/status_items.rst.txt b/_sources/components/status_items.rst.txt new file mode 100644 index 000000000..36074cbfa --- /dev/null +++ b/_sources/components/status_items.rst.txt @@ -0,0 +1,1376 @@ +============ +Status Items +============ + +Status Items can be used in :doc:`../tutorials/widget_rules` to control and +display status data for LinuxCNC. + + +---------- +**Tuples** +---------- + +If a status item returns a tuple you can get a single value from the tuple by +slicing. For example if the tuple is channel 0 ``ch[0]`` you can convert it to a +string and get the first entry like this ``str(ch[0][0])``. To get the third +item in the tuple you slice at 2 because counting starts at 0 not 1 so +``str(ch[0][2])`` returns the third item in the tuple. For axis items they are +in this order X, Y, Z, A, B, C, U, V, W. + +.. _status_list: + +--------------------- +**Status Items List** +--------------------- + +* :ref:`acceleration ` +* :ref:`active queue ` +* :ref:`actual position ` +* :ref:`adaptive feed enabled ` +* :ref:`analog inputs ` +* :ref:`all axes homed ` +* :ref:`angular units ` +* :ref:`analog outputs ` +* :ref:`axes configured ` +* :ref:`axis mask ` +* :ref:`block delete ` +* :ref:`o word call level ` +* :ref:`currently executing line ` +* :ref:`current velocity ` +* :ref:`cycle time ` +* :ref:`G4 delay left ` +* :ref:`digital inputs ` +* :ref:`distance to go ` +* :ref:`digital outputs ` +* :ref:`distance to go by axis ` +* :ref:`serial number ` +* :ref:`enabled ` +* :ref:`estop ` +* :ref:`task execution state ` +* :ref:`feed hold ` +* :ref:`feed override enabled ` +* :ref:`feedrate override ` +* :ref:`filename ` +* :ref:`flood ` +* :ref:`G5x index ` +* :ref:`G5x offset ` +* :ref:`G92 offset ` +* :ref:`gcodes ` +* :ref:`homed ` +* :ref:`id ` +* :ref:`in position ` +* :ref:`input timer ` +* :ref:`interpreter state ` +* :ref:`interpreter return code ` +* :ref:`joint n backlash ` +* :ref:`joint n enabled ` +* :ref:`joint n fault ` +* :ref:`joint n following error ` +* :ref:`joint n maximum following error ` +* :ref:`joint n homed ` +* :ref:`joint n homing ` +* :ref:`joint n in position ` +* :ref:`joint n input position ` +* :ref:`joint n type of axis ` +* :ref:`joint n maximum following error rapid ` +* :ref:`joint n maximum hard limit ` +* :ref:`joint n maximum soft limit setting ` +* :ref:`joint n maximum soft limit ` +* :ref:`joint n maximum following error feed ` +* :ref:`joint n minimum hard limit ` +* :ref:`joint n minimum soft limit ` +* :ref:`joint n minimum soft limit exceeded ` +* :ref:`joint n commanded output position ` +* :ref:`joint n override limits ` +* :ref:`joint n units ` +* :ref:`joint n velocity ` +* :ref:`joint actual positions ` +* :ref:`commanded joint positions ` +* :ref:`joints ` +* :ref:`kinematics type ` +* :ref:`limit masks ` +* :ref:`linear units ` +* :ref:`lube status ` +* :ref:`lube level ` +* :ref:`maximum acceleration ` +* :ref:`maximum velocity ` +* :ref:`m codes ` +* :ref:`mist status ` +* :ref:`motion line ` +* :ref:`motion mode` +* :ref:`motion type ` +* :ref:`machine power ` +* :ref:`optional stop ` +* :ref:`motion paused ` +* :ref:`pocket prepped ` +* :ref:`trajectory position ` +* :ref:`probe tripped ` +* :ref:`probe input value ` +* :ref:`probed position ` +* :ref:`probing status ` +* :ref:`program units ` +* :ref:`trajectory planner queue ` +* :ref:`trajectory planner queue full ` +* :ref:`queued mdi commands ` +* :ref:`rapid override scale ` +* :ref:`interperter read line ` +* :ref:`recent files ` +* :ref:`rotation XY ` +* :ref:`interpreter settings ` +* :ref:`spindle brake ` +* :ref:`spindle direction ` +* :ref:`spindle enabled ` +* :ref:`spindle homed ` +* :ref:`spindle orient fault ` +* :ref:`spindle n orient state ` +* :ref:`spindle speed override ` +* :ref:`spindle speed override enabled ` +* :ref:`spindle speed ` +* :ref:`spindles ` +* :ref:`command execution status ` +* :ref:`task mode ` +* :ref:`task paused ` +* :ref:`task state ` +* :ref:`tool in spindle ` +* :ref:`tool offset ` +* :ref:`tool table ` +* :ref:`velocity ` + + +.. _acceleration: + +acceleration + default acceleration, ini parameter [TRAJ]DEFAULT_ACCELERATION + + | syntax ``status:acceleration`` returns float + | syntax ``status:acceleration?string`` returns str + +:ref:`return to the status items list ` + +.. _active_queue: + +active_queue + number of motions blending + + | syntax ``status:active_queue`` returns int + | syntax ``status:active_queue?string`` returns str + +:ref:`return to the status items list ` + +.. _actual_position: + +actual_position + current trajectory position, (x y z a b c u v w) in machine units + + | syntax ``status:actual_position`` returns tuple of floats + | syntax ``status:actual_position?string`` returns tuple of str + +:ref:`return to the status items list ` + +.. _adaptive_feed_enabled: + +adaptive_feed_enabled + status of adaptive feedrate override + + | syntax ``status:adaptive_feed_enabled`` returns bool + | syntax ``status:adaptive_feed_enabled?string`` returns str + +:ref:`return to the status items list ` + +.. _ain: + +ain + current value of the analog input pins + + | syntax ``status:ain`` returns tuple of floats + | syntax ``status:ain?string`` returns str + +:ref:`return to the status items list ` + +.. _all_axes_homed: + +all_axes_homed + current status of all axes homed, if any axis is not homed it is false + + | syntax ``status:all_axes_homed`` returns bool + | syntax ``status:all_axes_homed?string`` returns str + +:ref:`return to the status items list ` + +.. _angular_units: + +angular_units + machine angular units per deg, ini parameter [TRAJ]ANGULAR_UNITS + + | syntax ``status:angular_units`` returns float + | syntax ``status:angular_units?string`` returns str + +:ref:`return to the status items list ` + +.. _aout: + +aout + current value of the analog output pins + + | syntax ``status:aout`` returns tuple of floats + | syntax ``status:aout?string`` returns str + +:ref:`return to the status items list ` + +.. _axes: + +axes + number of axes. derived from [TRAJ]COORDINATES ini parameter + + | syntax ``status:axes`` returns int + | syntax ``status:axes?string`` returns str + +:ref:`return to the status items list ` + +.. _axis_mask: + +axis_mask + axes as configured in the [TRAJ]COORDINATES INI parameter + + | the sum of the axes X=1, Y=2, Z=4, A=8, B=16, C=32, U=64, V=128, W=256 + | syntax ``status:axis_mask`` returns int bit mask + | syntax ``status:axis_mask?list`` returns list of axis numbers ``[0, 1, 2]`` + | syntax ``status:axis_mask?string`` returns string of axis letters ``XYZ`` + + +:ref:`return to the status items list ` + +.. _block_delete: + +block_delete + block delete curren status + + | syntax ``status:block_delete`` returns bool + | syntax ``status:block_delete?string`` returns str + +:ref:`return to the status items list ` + +.. _call_level: + +call_level + current nesting level of O-word procedures + + | syntax ``status:call_level`` returns int + | syntax ``status:call_level?string`` returns str + +:ref:`return to the status items list ` + +.. _current_line: + +current_line + currently executing line + + | syntax ``status:current_line`` returns int + | syntax ``status:current_line?string`` returns str + +:ref:`return to the status items list ` + +.. _current_vel: + +current_vel + current velocity in user units per second + + | syntax ``status:current_vel`` returns float + | syntax ``status:current_vel?string`` returns str + +:ref:`return to the status items list ` + +.. _cycle_time: + +cycle_time + thread period + + | syntax ``status:cycle_time`` returns float + | syntax ``status:cycle_time?string`` returns str + +:ref:`return to the status items list ` + +.. _delay_left: + +delay_left + remaining time on the G4 dwell command, seconds + + | syntax ``status:delay_left`` returns float + | syntax ``status:delay_left?string`` returns str + +:ref:`return to the status items list ` + +.. _din: + +din + current value of the digital input pins + + | syntax ``status:din`` returns tuple of integers + | syntax ``status:din?string`` returns str + +:ref:`return to the status items list ` + +.. _distance_to_go: + +distance_to_go + remaining distance of current move, as reported by trajectory planner + + | syntax ``status:distance_to_go`` returns float + | syntax ``status:distance_to_go?string`` returns str + +:ref:`return to the status items list ` + +.. _dout: + +dout + current value of the digital output pins + + | syntax ``status:dout`` returns tuple of integers + | syntax ``status:dout?string`` returns str + +:ref:`return to the status items list ` + +.. _dtg: + +dtg + remaining distance of current move for each axis, as reported by trajectory planner + + | syntax ``status:dtg`` returns tuple of floats + | syntax ``status:dtg?string`` returns str + +:ref:`return to the status items list ` + +.. _echo_serial_number: + +echo_serial_number + The serial number of the last completed command sent by a UI to task + + | syntax ``status:echo_serial_number`` returns int + | syntax ``status:echo_serial_number?string`` returns str + +:ref:`return to the status items list ` + +.. _enabled: + +enabled + trajectory planner enabled flag + + | syntax ``status:enabled`` returns bool + | syntax ``status:enabled?string`` returns str + +:ref:`return to the status items list ` + +.. _estop: + +estop + status of E Stop, 1 for enabled and 0 for not enabled + + | syntax ``status:estop`` returns int + | syntax ``status:estop?string`` returns str + +:ref:`return to the status items list ` + +.. _exec_state: + +exec_state + task execution state + + === =========================== + int str + === =========================== + 1 Error + 2 Done + 3 Waiting for Motion + 4 Waiting for Motion Queue + 5 Waiting for Pause + 6 Not used by LinuxCNC + 7 Waiting for Motion and IO + 8 Waiting for Delay + 9 Waiting for system CMD + 10 Waiting for spindle orient + === =========================== + + | syntax ``status:exec_state`` returns int + | syntax ``status:exec_state?string`` returns str + +:ref:`return to the status items list ` + +.. _feed_hold_enabled: + +feed_hold_enabled + status of feed hold + + | syntax ``status:feed_hold_enabled`` returns bool + | syntax ``status:feed_hold_enabled?string`` returns str + +:ref:`return to the status items list ` + +.. _feed_override_enabled: + +feed_override_enabled + status of feed override + + | syntax ``status:feed_override_enabled`` returns bool + | syntax ``status:feed_override_enabled?string`` returns str + +:ref:`return to the status items list ` + +.. _feedrate: + +feedrate + current feedrate override, 1.0 = 100% + + | syntax ``status:feedrate`` returns float + | syntax ``status:feedrate?string`` returns str + +:ref:`return to the status items list ` + +.. _file: + +file + currently loaded gcode filename with path + + | syntax ``status:file`` returns str + | for just the file name use this expression ``ch[0].split('/')[-1]`` + +:ref:`return to the status items list ` + +.. _flood: + +flood + current flood status 0 for off, 1 for on + + | syntax ``status:flood`` returns int + | syntax ``status:flood?string`` returns str + +:ref:`return to the status items list ` + +.. _g5x_index: + +g5x_index + currently active coordinate system + + === ====== + int string + === ====== + 0 G53 + 1 G54 + 2 G55 + 3 G56 + 4 G57 + 5 G58 + 6 G59 + 7 G59.1 + 8 G59.2 + 9 G59.3 + === ====== + + | syntax ``status:g5x_index`` returns int + | syntax ``status:g5x_index?string`` returns str + +:ref:`return to the status items list ` + +.. _g5x_offset: + +g5x_offset + offsets of the currently active coordinate system + + | syntax ``status:g5x_offset`` returns tuple of floats + | syntax ``status:g5x_offset?string`` returns str + +:ref:`return to the status items list ` + +.. _g92_offset: + +g92_offset + current g92 offsets + + | syntax ``status:g92_offset`` returns tuple of floats + | syntax ``status:g92_offset?string`` returns str + +:ref:`return to the status items list ` + +.. _gcodes: + +gcodes + active G-codes for each modal group + + | syntax ``status:gcodes`` returns tuple of integers + | syntax ``status:gcodes?string`` returns str + +:ref:`return to the status items list ` + +.. _homed: + +homed + currently homed joints, 0 = not homed, 1 = homed + + | syntax ``status:homed`` returns tuple of integers + | syntax ``status:homed?string`` returns str + +:ref:`return to the status items list ` + +.. _id: + +id + currently executing motion id + + | syntax ``status:id`` returns int + | syntax ``status:id?string`` returns str + +:ref:`return to the status items list ` + +.. _inpos: + +inpos + status machine in position + + | syntax ``status:inpos`` returns bool + | syntax ``status:inpos?string`` returns str + +:ref:`return to the status items list ` + +.. _input_timeout: + +input_timeout + flag for M66 timer in progress + + | syntax ``status:input_timeout`` returns bool + | syntax ``status:input_timeout?string`` returns str + +:ref:`return to the status items list ` + +.. _interp_state: + +interp_state + current state of RS274NGC interpreter + + === ======= + int str + === ======= + 1 Idle + 2 Reading + 3 Paused + 4 Waiting + === ======= + + | syntax ``status:interp_state`` returns int + | syntax ``status:interp_state?string`` returns str + +:ref:`return to the status items list ` + +.. _interpreter_errcode: + +interpreter_errcode + current RS274NGC interpreter return code + + === ============= + int str + === ============= + 0 Ok + 1 Exit + 2 Finished + 3 Endfile + 4 File not open + 5 Error + === ============= + + | syntax ``status:interpreter_errcode`` returns int + | syntax ``status:interpreter_errcode?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.backlash: + +joint.n.backlash + backlash in machine units, ini parameter [JOINT_n]BACKLASH (`n` is joint number) + + | syntax ``status:joint.n.backlash`` returns float + | syntax ``status:joint.n.backlash?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.enabled: + +joint.n.enabled + status of joint n enabled, 0 not enabled 1 enabled + + | syntax ``status:joint.n.enabled`` returns int + | syntax ``status:joint.n.enabled?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.fault: + +joint.n.fault + status of joint n fault, 0 not faulted 1 faulted + + | syntax ``status:joint.n.fault`` returns int + | syntax ``status:joint.n.fault?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.ferror_current: + +joint.n.ferror_current + current joint n following error + + | syntax ``status:joint.n.ferror_current`` returns float + | syntax ``status:joint.n.ferror_current?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.ferror_highmark: + +joint.n.ferror_highmark + joint n magnitude of maximum following error + + | syntax ``status:joint.n.ferror_highmark`` returns float + | syntax ``status:joint.n.ferror_highmark?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.homed: + +joint.n.homed + status of joint n homed, 0 not homed 1 homed + + | syntax ``status:joint.n.homed`` returns int + | syntax ``status:joint.n.homed?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.homing: + +joint.n.homing + status of joint n homing in progress, 0 not homing 1 homing + + | syntax ``status:joint.n.homing`` returns int + | syntax ``status:joint.n.homing?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.inpos: + +joint.n.inpos + status of joint n in position, 0 not in position 1 in position + + | syntax ``status:joint.n.inpos`` returns int + | syntax ``status:joint.n.inpos?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.input: + +joint.n.input + joint n current input position + + | syntax ``status:joint.n.input`` returns float + | syntax ``status:joint.n.input?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.jointType: + +joint.n.jointType + joint n type of axis, ini parameter [JOINT_n]TYPE + + | syntax ``status:joint.n.jointType`` returns int + | syntax ``status:joint.n.jointType?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.max_ferror: + +joint.n.max_ferror + joint n maximum following error rapid, ini parameter [JOINT_n]FERROR + + | syntax ``status:joint.n.max_ferror`` returns float + | syntax ``status:joint.n.max_ferror?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.max_hard_limit: + +joint.n.max_hard_limit + status of joint n maximum hard limit, 0 not exceeded 1 exceeded + + | syntax ``status:joint.n.max_hard_limit`` returns int + | syntax ``status:joint.n.max_hard_limit?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.max_position_limit: + +joint.n.max_position_limit + joint n maximum soft limit, ini parameter [JOINT_n]MAX_LIMIT + + | syntax ``status:joint.n.max_position_limit`` returns float + | syntax ``status:joint.n.max_position_limit?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.max_soft_limit: + +joint.n.max_soft_limit + status of joint n maximum soft limit, 0 not exceeded 1 exceeded + + | syntax ``status:joint.n.max_soft_limit`` returns int + | syntax ``status:joint.n.max_soft_limit?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.min_ferror: + +joint.n.min_ferror + maximum following error feed, ini parameter [JOINT_n]MIN_FERROR + + | syntax ``status:joint.n.min_ferror`` returns float + | syntax ``status:joint.n.min_ferror?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.min_hard_limit: + +joint.n.min_hard_limit + non-zero means min hard limit exceeded + + | syntax ``status:joint.n.min_hard_limit`` returns int + | syntax ``status:joint.n.min_hard_limit?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.min_position_limit: + +joint.n.min_position_limit + minimum soft limit ini parameter [JOINT_n]MIN_LIMIT + + | syntax ``status:joint.n.min_position_limit`` returns float + | syntax ``status:joint.n.min_position_limit?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.min_soft_limit: + +joint.n.min_soft_limit + non-zero means min_position_limit was exceeded + + | syntax ``status:joint.n.min_soft_limit`` returns int + | syntax ``status:joint.n.min_soft_limit?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.output: + +joint.n.output + commanded output position + + | syntax ``status:joint.n.output`` returns float + | syntax ``status:joint.n.output?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.override_limits: + +joint.n.override_limits + non-zero means limits are overridden + + | syntax ``status:joint.n.override_limits`` returns int + | syntax ``status:joint.n.override_limits?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.units: + +joint.n.units + joint units + + | syntax ``status:joint.n.units`` returns float + | syntax ``status:joint.n.units?string`` returns str + +:ref:`return to the status items list ` + +.. _joint.n.velocity: + +joint.n.velocity + current velocity + + | syntax ``status:joint.n.velocity`` returns float + | syntax ``status:joint.n.velocity?string`` returns str + +:ref:`return to the status items list ` + +.. _joint_actual_position: + +joint_actual_position + joint actual positions + + | syntax ``status:joint_actual_position`` returns tuple of floats + | syntax ``status:joint_actual_position?string`` returns str + +:ref:`return to the status items list ` + +.. _joint_position: + +joint_position + commanded joint positions + + | syntax ``status:joint_position`` returns tuple of floats + | syntax ``status:joint_position?string`` returns str + +:ref:`return to the status items list ` + +.. _joints: + +joints + number of joints, ini parameter [KINS]JOINTS + + | syntax ``status:joints`` returns int + | syntax ``status:joints?string`` returns str + +:ref:`return to the status items list ` + +.. _kinematics_type: + +kinematics_type + kinematics type + + | syntax ``status:kinematics_type`` returns int + | syntax ``status:kinematics_type?string`` returns str + +:ref:`return to the status items list ` + +.. _limit: + +limit + axis limit masks + + | syntax ``status:limit`` returns tuple of integers + | syntax ``status:limit?string`` returns str + +:ref:`return to the status items list ` + +.. _linear_units: + +linear_units + machine linear units, ini parameter [TRAJ]LINEAR_UNITS + + | syntax ``status:linear_units`` returns float + | syntax ``status:linear_units?string`` returns str + +:ref:`return to the status items list ` + +.. _lube: + +lube + lube status + + | syntax ``status:lube`` returns int + | syntax ``status:lube?string`` returns str + +:ref:`return to the status items list ` + +.. _lube_level: + +lube_level + status of iocontrol.0.lube_level + + | syntax ``status:lube_level`` returns int + | syntax ``status:lube_level?string`` returns str + +:ref:`return to the status items list ` + +.. _max_acceleration: + +max_acceleration + maximum acceleration, ini parameter [TRAJ]MAX_ACCELERATION + + | syntax ``status:max_acceleration`` returns float + | syntax ``status:max_acceleration?string`` returns str + +:ref:`return to the status items list ` + +.. _max_velocity: + +max_velocity + maximum velocity, ini parameter [TRAJ]MAX_VELOCITY + + | syntax ``status:max_velocity`` returns float + | syntax ``status:max_velocity?string`` returns str + +:ref:`return to the status items list ` + +.. _mcodes: + +mcodes + currently active M codes + + | syntax ``status:mcodes`` returns tuple of integers + | syntax ``status:mcodes?string`` returns str + +:ref:`return to the status items list ` + +.. _mist: + +mist + mist status + + | syntax ``status:mist`` returns int + | syntax ``status:mist?string`` returns str + +:ref:`return to the status items list ` + +.. _motion_line: + +motion_line + source line number motion is currently executing + + | syntax ``status:motion_line`` returns int + | syntax ``status:motion_line?string`` returns str + +:ref:`return to the status items list ` + +.. _motion_mode: + +motion_mode + mode of the motion controller + + === ====== + int string + === ====== + 0 N/A + 1 Free + 2 Coord + 3 Teleop + === ====== + + | syntax ``status:motion_mode`` returns int + | syntax ``status:motion_mode?string`` returns str + +:ref:`return to the status items list ` + +.. _motion_type: + +motion_type + motion type of move currently executing + + === ============ + int string + === ============ + 0 None + 1 Traverse + 2 Linear Feed + 3 Arc Feed + 4 Tool Change + 5 Probing + 6 Rotary Index + === ============ + + | syntax ``status:motion_type`` returns int + | syntax ``status:motion_type?string`` returns str + +:ref:`return to the status items list ` + +.. _on: + +on + status of machine power + + | syntax ``status:on`` returns bool + | syntax ``status:on?string`` returns str + +:ref:`return to the status items list ` + +.. _optional_stop: + +optional_stop + status of optional stop + + | syntax ``status:optional_stop`` returns int + | syntax ``status:optional_stop?string`` returns str + +:ref:`return to the status items list ` + +.. _paused: + +paused + motion paused + + | syntax ``pstatus:aused`` returns bool + | syntax ``status:paused?string`` returns str + +:ref:`return to the status items list ` + +.. _pocket_prepped: + +pocket_prepped + pocket prepped from last Tn commaned + + | syntax ``status:pocket_prepped`` returns int + | syntax ``status:pocket_prepped?string`` returns str + +:ref:`return to the status items list ` + +.. _position: + +position + trajectory position + + | syntax ``status:position`` returns tuple of floats + | syntax ``status:position?string`` returns str + +:ref:`return to the status items list ` + +.. _probe_tripped: + +probe_tripped + probe tripped + + | syntax ``status:probe_tripped`` returns bool + | syntax ``status:probe_tripped?string`` returns str + +:ref:`return to the status items list ` + +.. _probe_val: + +probe_val + value of the motion.probe-input pin + + | syntax ``status:probe_val`` returns int + | syntax ``status:probe_val?string`` returns str + +:ref:`return to the status items list ` + +.. _probed_position: + +probed_position + position where probe tripped + + | syntax ``status:probed_position`` returns tuple of floats + | syntax ``status:probed_position?string`` returns str + +:ref:`return to the status items list ` + +.. _probing: + +probing + probe operation is in progress + + | syntax ``status:probing`` returns bool + | syntax ``status:probing?string`` returns str + +:ref:`return to the status items list ` + +.. _program_units: + +program_units + program units + + === ===== ============ + int short long + === ===== ============ + 0 N/A N/A + 1 in Inches + 2 mm Millimeters + 3 cm Centimeters + === ===== ============ + + | syntax ``status:program_units`` returns int + | syntax ``status:rogram_units?string`` returns short str + | syntax ``status:rogram_units?string&format=long`` returns long str + +:ref:`return to the status items list ` + +.. _queue: + +queue + current size of the trajectory planner queue + + | syntax ``status:queue`` returns int + | syntax ``status:queue?string`` returns str + +:ref:`return to the status items list ` + +.. _queue_full: + +queue_full + status of the trajectory planner queue + + | syntax ``status:queue_full`` returns bool + | syntax ``status:queue_full?string`` returns str + +:ref:`return to the status items list ` + +.. _queued_mdi_commands: + +queued_mdi_commands + queued mdi commands + + | syntax ``status:queued_mdi_commands`` returns int + | syntax ``status:queued_mdi_commands?string`` returns str + +:ref:`return to the status items list ` + +.. _rapidrate: + +rapidrate + rapid override scale + + | syntax ``status:rapidrate`` returns float + | syntax ``status:rapidrate?string`` returns str + +:ref:`return to the status items list ` + +.. _read_line: + +read_line + current line the interperter is reading + + | syntax ``status:read_line`` returns int + | syntax ``status:read_line?string`` returns str + +:ref:`return to the status items list ` + +.. _recent_files: + +recent_files + recent files opened including file path + + | syntax ``status:recent_files`` returns list + | syntax ``status:recent_files?string`` returns str + +:ref:`return to the status items list ` + +.. _rotation_xy: + +rotation_xy + current XY rotation angle around Z axis + + | syntax ``status:rotation_xy`` returns float + | syntax ``status:rotation_xy?string`` returns str + +:ref:`return to the status items list ` + +.. _settings: + +settings + current interpreter settings + + returns a tuple of floats + + ===== ====== + index value + ===== ====== + 0 sequence number + 1 F word value (feed) + 2 S word value (speed) + 3 G64 P value (blend tolerance) + 4 G64 Q value (naive CAM tolerance) + ===== ====== + + | syntax ``status:settings`` returns tuple of floats + | syntax ``status:settings?string`` returns str + +:ref:`return to the status items list ` + +.. _spindle.n.brake: + +spindle.n.brake + status of spindle n brake + + | syntax ``status:spindle.n.brake`` returns int + | syntax ``status:spindle.n.brake?string`` returns str + +:ref:`return to the status items list ` + +.. _spindle.n.direction: + +spindle.n.direction + rotational direction of the spindle. forward=1, reverse=-1 + + | syntax ``status:spindle.n.direction`` returns int + | syntax ``status:spindle.n.direction?string`` returns str + +:ref:`return to the status items list ` + +.. _spindle.n.enabled: + +spindle.n.enabled + spindle enabled status + + | syntax ``status:spindle.n.enabled`` returns int + | syntax ``status:spindle.n.enabled?string`` returns str + +:ref:`return to the status items list ` + +.. _spindle.n.homed: + +spindle.n.homed + spindle n homed + + | syntax ``status:spindle.n.homed`` returns bool + | syntax ``status:spindle.n.homed?string`` returns str + +:ref:`return to the status items list ` + +.. _spindle.n.orient_fault: + +spindle.n.orient_fault + spindle n orient fault status + + | syntax ``status:spindle.n.orient_fault`` returns int + | syntax ``status:spindle.n.orient_fault?string`` returns str + +:ref:`return to the status items list ` + +.. _spindle.n.orient_state: + +spindle.n.orient_state + unknown + + | syntax ``status:spindle.n.orient_state`` returns int + | syntax ``status:spindle.n.orient_state?string`` returns str + +:ref:`return to the status items list ` + +.. _spindle.n.override: + +spindle.n.override + spindle n speed override scale + + | syntax ``status:spindle.n.override`` returns float + | syntax ``status:spindle.n.override?string`` returns str + +:ref:`return to the status items list ` + +.. _spindle.n.override_enabled: + +spindle.n.override_enabled + spindle n override enabled + + | syntax ``status:spindle.n.override_enabled`` returns bool + | syntax ``status:spindle.n.override_enabled?string`` returns str + +:ref:`return to the status items list ` + +.. _spindle.n.speed: + +spindle.n.speed + spindle n speed rpm, > 0 clockwise, < 0 counterclockwise + + | syntax ``status:spindle.n.speed`` returns float + | syntax ``status:spindle.n.speed?string`` returns str + +:ref:`return to the status items list ` + +.. _spindles: + +spindles + number of spindles, ini parameter [TRAJ]SPINDLES + + | syntax ``status:spindles`` returns int + | syntax ``status:spindles?string`` returns str + +:ref:`return to the status items list ` + +.. _state: + +state + current command execution status + + | syntax ``status:state`` returns int + | syntax ``status:state?string`` returns str + +:ref:`return to the status items list ` + +.. _task_mode: + +task_mode + current task mode + + === ====== + int string + === ====== + 0 N/A + 1 Manual + 2 Auto + 3 MDI + === ====== + + | syntax ``status:task_mode`` returns int + | syntax ``status:task_mode?string`` returns str + +:ref:`return to the status items list ` + +.. _task_paused: + +task_paused + task paused status + + | syntax ``status:task_paused`` returns int + | syntax ``status:task_paused?string`` returns str + +:ref:`return to the status items list ` + +.. _task_state: + +task_state + current task state + + === ====== + int string + === ====== + 0 N/A + 1 E-Stop + 2 Reset + 3 Off + 4 On + === ====== + + | syntax ``status:task_state`` returns int + | syntax ``status:task_state?string`` returns str + +:ref:`return to the status items list ` + +.. _tool_in_spindle: + +tool_in_spindle + current tool number + + | syntax ``status:tool_in_spindle`` returns int + | syntax ``status:tool_in_spindle?string`` returns str + +:ref:`return to the status items list ` + +.. _tool_offset: + +tool_offset + offset values of the current tool + + | syntax ``status:tool_offset`` returns tuple of floats + | syntax ``status:tool_offset?string`` returns str + +:ref:`return to the status items list ` + +:ref:`return to the status items list ` + +.. _tool_table: + +tool_table + list of tool entries + + | syntax ``status:tool_table`` returns tuple of tool_results + | syntax ``status:tool_table?string`` returns str + +:ref:`return to the status items list ` + +.. _velocity: + +velocity + This property is defined, but it does not have a useful interpretation + + | syntax ``status:velocity`` returns float + | syntax ``status:velocity?string`` returns str + +:ref:`return to the status items list ` + diff --git a/_sources/components/subcallbutton.rst.txt b/_sources/components/subcallbutton.rst.txt new file mode 100644 index 000000000..001dd80d9 --- /dev/null +++ b/_sources/components/subcallbutton.rst.txt @@ -0,0 +1,33 @@ +=============== +Sub Call Button +=============== + +The `SubCallButton` is used to call subroutines similar to NGCGUI subroutines. +The file format is almost the same. + +The way you get information into the subroutine is by having widgets with the +same name as the variable or using a default value for the variable. The file +must be executable and have the same name as the sub/endsub. + +You can use a line edit, a spin box or a double spin box to set the values for +the variable. + +For example if you had a spin box on the tool change page called `tool_number` +you could have one button to change the tool by getting the value from the +`tool_number` spin box. + +`tool_change.ngc` +:: + + ; tool change subroutine + o sub + + # = #1 (=1); set the default to 1 + + T# M6 G43 + + o endsub + +The subroutine must be on the path specificed in the ini file and must be +executable. Put the file name including the .ngc in the `FileName` variable of +the `SubCallButton`. diff --git a/_sources/configuration/index.rst.txt b/_sources/configuration/index.rst.txt new file mode 100644 index 000000000..a95640057 --- /dev/null +++ b/_sources/configuration/index.rst.txt @@ -0,0 +1,13 @@ +============= +Configuration +============= + +Configuration for QtPyVCP and VCP's + +.. toctree:: + :maxdepth: 1 + :titlesonly: + + ini_options + yml_config + diff --git a/_sources/configuration/ini_options.rst.txt b/_sources/configuration/ini_options.rst.txt new file mode 100644 index 000000000..f27b0cb60 --- /dev/null +++ b/_sources/configuration/ini_options.rst.txt @@ -0,0 +1,115 @@ +=========== +INI Options +=========== + +To get a list of current INI file options run + +.. code-block:: bash + + qtpyvcp -h + +Command line options are lower case and use the dash, INI file options are upper +case and use the underscore. Example: ``--hide-menu-bar`` >> ``HIDE_MENU_BAR``. + +Options available in the INI file are: + +.. code-block:: ini + + [DISPLAY] + # Name of the VCP to use, or a .ui or .yml file + VCP = name + + # The Qt theme to use, fusion, windows etc + THEME = theme + + # Path to QSS style sheet file + STYLESHEET = style.qss + + # Initial size of the window in pixels + SIZE = x + + # Initial position of the window in pixels + POSITION = x + + # Flag to start with window fullscreen + FULLSCREEN = bool + + # Flag to start with window maximized + MAXIMIZE = bool + + # Hides the menu bar, if present + HIDE_MENU_BAR = bool + + # Hides the status bar, if present + HIDE_STATUS_BAR = bool + + # Hides the cursor for touchscreen VCPs + HIDE_CURSOR = True + + # Whether to show dialog to confirm exit + CONFIRM_EXIT = bool + + # One of DEBUG, INFO, WARN, ERROR or CRITICAL + LOG_LEVEL = level + + # Specifies the log file + LOG_FILE = file + + # Specifies a machine specific YML config file + CONFIG_FILE = file + + # Specifies the preference file + PREF_FILE = file + + # Monitor and log system performance + PERFMON = bool + + # Qt Python binding to use, pyqt5 or pyside2 + QT_API = api + + # Additional args passed to the QtApplication. + COMMAND_LINE_ARGS = + + # Provide user defined G code Syntax file + GCODE_SYNTAX = file + + # VTK_BackPlot Options + [VTK] + # Boolean False to hide the machine boundry + MACHINE_BOUNDRY = bool + + # Boolean False to hide the machine boundry ticks + MACHINE_TICKS = bool + + # Boolean False to hide the machine labels + MACHINE_LABELS = bool + + # Boolean False to hide the program boundry + PROGRAM_BOUNDRY = bool + + # Boolean False to hide the program boundry ticks + PROGRAM_TICKS = bool + + # Boolean False to hide the program labels + PROGRAM_LABELS = bool + +Boolean values can be one of ``true``, ``on``, ``yes`` or ``1`` for **True**, +and one of ``false``, ``off``, ``no`` or ``0`` for **False**. + +File paths can be relative to the config dir, relative to the users home, or +absolute. Environment variables are expanded. + +.. code-block:: ini + + # File Paths: + # File paths can be relative to the config dir: + # LOG_FILE = qtpyvcp.log + + # Or relative to $HOME: (May not be compatible with other GUIs!) + # LOG_FILE = ~/qtpyvcp.log + + # Or at an absolute location: + # LOG_FILE = /home//qtpyvcp.log + + # Enviroment vars are also expanded: + # LOG_FILE = $CONFIG_DIR/qtpyvcp.log diff --git a/_sources/configuration/yml_config.rst.txt b/_sources/configuration/yml_config.rst.txt new file mode 100644 index 000000000..4a6ca430a --- /dev/null +++ b/_sources/configuration/yml_config.rst.txt @@ -0,0 +1,30 @@ +================= +YAML Config Files +================= + +At the heart of every QtPyVCP based VCP is a YAML configuration +file. This file is probably more important than any other, since +it defines how the .ui, .py and .qss files come together to make +a complete VCP. + +Multiple configuration files are used from various levels, and +when combined and merged determine the final configuration for +the VCP. + +At the lowest level is the ``default_config.yml`` file. This file +is not user editable and is always loaded. It defines the basic +things that must exist for the most basic VCP to function. + +At the next level is the VCP specific configuration file. This is +where individual VCPs are defined, and is generally only edited by +a VCP developers. It includes basic info such as the VCP name and author, +as well as what .ui, .py and .qss files to use when loading the VCP. +It is also where VCP specific menus, dialogs etc. are defined. + +At the highest level is the machine specific configuration file. This +file is usually located in the config dir along with the INI file. It +is not required but can be used to tweak configurations settings for a +particular machine. + +More information about YAML files can be found at +`Wikipedia `_ diff --git a/_sources/designer/plugins/clock.rst.txt b/_sources/designer/plugins/clock.rst.txt new file mode 100644 index 000000000..f34ce1895 --- /dev/null +++ b/_sources/designer/plugins/clock.rst.txt @@ -0,0 +1,52 @@ +============ +Clock plugin +============ + +This plugin provides the Date and Time + +This plugin is not loaded by default, so to use it you will first +need to add it to your VCPs YAML config file. + +YAML configuration: + +.. code-block:: yaml + + data_plugins: + clock: + provider: qtpyvcp.plugins.clock:Clock + +------------------------ +*Available datachannels* +------------------------ + +* :ref:`time