meerk40t 0.9.3001__py2.py3-none-any.whl → 0.9.7020__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. meerk40t/__init__.py +1 -1
  2. meerk40t/balormk/balor_params.py +167 -167
  3. meerk40t/balormk/clone_loader.py +457 -457
  4. meerk40t/balormk/controller.py +1566 -1512
  5. meerk40t/balormk/cylindermod.py +64 -0
  6. meerk40t/balormk/device.py +966 -1959
  7. meerk40t/balormk/driver.py +778 -591
  8. meerk40t/balormk/galvo_commands.py +1194 -0
  9. meerk40t/balormk/gui/balorconfig.py +237 -111
  10. meerk40t/balormk/gui/balorcontroller.py +191 -184
  11. meerk40t/balormk/gui/baloroperationproperties.py +116 -115
  12. meerk40t/balormk/gui/corscene.py +845 -0
  13. meerk40t/balormk/gui/gui.py +179 -147
  14. meerk40t/balormk/livelightjob.py +466 -382
  15. meerk40t/balormk/mock_connection.py +131 -109
  16. meerk40t/balormk/plugin.py +133 -135
  17. meerk40t/balormk/usb_connection.py +306 -301
  18. meerk40t/camera/__init__.py +1 -1
  19. meerk40t/camera/camera.py +514 -397
  20. meerk40t/camera/gui/camerapanel.py +1241 -1095
  21. meerk40t/camera/gui/gui.py +58 -58
  22. meerk40t/camera/plugin.py +441 -399
  23. meerk40t/ch341/__init__.py +27 -27
  24. meerk40t/ch341/ch341device.py +628 -628
  25. meerk40t/ch341/libusb.py +595 -589
  26. meerk40t/ch341/mock.py +171 -171
  27. meerk40t/ch341/windriver.py +157 -157
  28. meerk40t/constants.py +13 -0
  29. meerk40t/core/__init__.py +1 -1
  30. meerk40t/core/bindalias.py +550 -539
  31. meerk40t/core/core.py +47 -47
  32. meerk40t/core/cutcode/cubiccut.py +73 -73
  33. meerk40t/core/cutcode/cutcode.py +315 -312
  34. meerk40t/core/cutcode/cutgroup.py +141 -137
  35. meerk40t/core/cutcode/cutobject.py +192 -185
  36. meerk40t/core/cutcode/dwellcut.py +37 -37
  37. meerk40t/core/cutcode/gotocut.py +29 -29
  38. meerk40t/core/cutcode/homecut.py +29 -29
  39. meerk40t/core/cutcode/inputcut.py +34 -34
  40. meerk40t/core/cutcode/linecut.py +33 -33
  41. meerk40t/core/cutcode/outputcut.py +34 -34
  42. meerk40t/core/cutcode/plotcut.py +335 -335
  43. meerk40t/core/cutcode/quadcut.py +61 -61
  44. meerk40t/core/cutcode/rastercut.py +168 -148
  45. meerk40t/core/cutcode/waitcut.py +34 -34
  46. meerk40t/core/cutplan.py +1843 -1316
  47. meerk40t/core/drivers.py +330 -329
  48. meerk40t/core/elements/align.py +801 -669
  49. meerk40t/core/elements/branches.py +1858 -1507
  50. meerk40t/core/elements/clipboard.py +229 -219
  51. meerk40t/core/elements/element_treeops.py +4595 -2837
  52. meerk40t/core/elements/element_types.py +125 -105
  53. meerk40t/core/elements/elements.py +4315 -3617
  54. meerk40t/core/elements/files.py +117 -64
  55. meerk40t/core/elements/geometry.py +473 -224
  56. meerk40t/core/elements/grid.py +467 -316
  57. meerk40t/core/elements/materials.py +158 -94
  58. meerk40t/core/elements/notes.py +50 -38
  59. meerk40t/core/elements/offset_clpr.py +934 -912
  60. meerk40t/core/elements/offset_mk.py +963 -955
  61. meerk40t/core/elements/penbox.py +339 -267
  62. meerk40t/core/elements/placements.py +300 -83
  63. meerk40t/core/elements/render.py +785 -687
  64. meerk40t/core/elements/shapes.py +2618 -2092
  65. meerk40t/core/elements/testcases.py +105 -0
  66. meerk40t/core/elements/trace.py +651 -563
  67. meerk40t/core/elements/tree_commands.py +415 -409
  68. meerk40t/core/elements/undo_redo.py +116 -58
  69. meerk40t/core/elements/wordlist.py +319 -200
  70. meerk40t/core/exceptions.py +9 -9
  71. meerk40t/core/laserjob.py +220 -220
  72. meerk40t/core/logging.py +63 -63
  73. meerk40t/core/node/blobnode.py +83 -86
  74. meerk40t/core/node/bootstrap.py +105 -103
  75. meerk40t/core/node/branch_elems.py +40 -31
  76. meerk40t/core/node/branch_ops.py +45 -38
  77. meerk40t/core/node/branch_regmark.py +48 -41
  78. meerk40t/core/node/cutnode.py +29 -32
  79. meerk40t/core/node/effect_hatch.py +375 -257
  80. meerk40t/core/node/effect_warp.py +398 -0
  81. meerk40t/core/node/effect_wobble.py +441 -309
  82. meerk40t/core/node/elem_ellipse.py +404 -309
  83. meerk40t/core/node/elem_image.py +1082 -801
  84. meerk40t/core/node/elem_line.py +358 -292
  85. meerk40t/core/node/elem_path.py +259 -201
  86. meerk40t/core/node/elem_point.py +129 -102
  87. meerk40t/core/node/elem_polyline.py +310 -246
  88. meerk40t/core/node/elem_rect.py +376 -286
  89. meerk40t/core/node/elem_text.py +445 -418
  90. meerk40t/core/node/filenode.py +59 -40
  91. meerk40t/core/node/groupnode.py +138 -74
  92. meerk40t/core/node/image_processed.py +777 -766
  93. meerk40t/core/node/image_raster.py +156 -113
  94. meerk40t/core/node/layernode.py +31 -31
  95. meerk40t/core/node/mixins.py +135 -107
  96. meerk40t/core/node/node.py +1427 -1304
  97. meerk40t/core/node/nutils.py +117 -114
  98. meerk40t/core/node/op_cut.py +463 -335
  99. meerk40t/core/node/op_dots.py +296 -251
  100. meerk40t/core/node/op_engrave.py +414 -311
  101. meerk40t/core/node/op_image.py +755 -369
  102. meerk40t/core/node/op_raster.py +787 -522
  103. meerk40t/core/node/place_current.py +37 -40
  104. meerk40t/core/node/place_point.py +329 -126
  105. meerk40t/core/node/refnode.py +58 -47
  106. meerk40t/core/node/rootnode.py +225 -219
  107. meerk40t/core/node/util_console.py +48 -48
  108. meerk40t/core/node/util_goto.py +84 -65
  109. meerk40t/core/node/util_home.py +61 -61
  110. meerk40t/core/node/util_input.py +102 -102
  111. meerk40t/core/node/util_output.py +102 -102
  112. meerk40t/core/node/util_wait.py +65 -65
  113. meerk40t/core/parameters.py +709 -707
  114. meerk40t/core/planner.py +875 -785
  115. meerk40t/core/plotplanner.py +656 -652
  116. meerk40t/core/space.py +120 -113
  117. meerk40t/core/spoolers.py +706 -705
  118. meerk40t/core/svg_io.py +1836 -1549
  119. meerk40t/core/treeop.py +534 -445
  120. meerk40t/core/undos.py +278 -124
  121. meerk40t/core/units.py +784 -680
  122. meerk40t/core/view.py +393 -322
  123. meerk40t/core/webhelp.py +62 -62
  124. meerk40t/core/wordlist.py +513 -504
  125. meerk40t/cylinder/cylinder.py +247 -0
  126. meerk40t/cylinder/gui/cylindersettings.py +41 -0
  127. meerk40t/cylinder/gui/gui.py +24 -0
  128. meerk40t/device/__init__.py +1 -1
  129. meerk40t/device/basedevice.py +322 -123
  130. meerk40t/device/devicechoices.py +50 -0
  131. meerk40t/device/dummydevice.py +163 -128
  132. meerk40t/device/gui/defaultactions.py +618 -602
  133. meerk40t/device/gui/effectspanel.py +114 -0
  134. meerk40t/device/gui/formatterpanel.py +253 -290
  135. meerk40t/device/gui/warningpanel.py +337 -260
  136. meerk40t/device/mixins.py +13 -13
  137. meerk40t/dxf/__init__.py +1 -1
  138. meerk40t/dxf/dxf_io.py +766 -554
  139. meerk40t/dxf/plugin.py +47 -35
  140. meerk40t/external_plugins.py +79 -79
  141. meerk40t/external_plugins_build.py +28 -28
  142. meerk40t/extra/cag.py +112 -116
  143. meerk40t/extra/coolant.py +403 -0
  144. meerk40t/extra/encode_detect.py +204 -0
  145. meerk40t/extra/ezd.py +1165 -1165
  146. meerk40t/extra/hershey.py +834 -340
  147. meerk40t/extra/imageactions.py +322 -316
  148. meerk40t/extra/inkscape.py +628 -622
  149. meerk40t/extra/lbrn.py +424 -424
  150. meerk40t/extra/outerworld.py +283 -0
  151. meerk40t/extra/param_functions.py +1542 -1556
  152. meerk40t/extra/potrace.py +257 -253
  153. meerk40t/extra/serial_exchange.py +118 -0
  154. meerk40t/extra/updater.py +602 -453
  155. meerk40t/extra/vectrace.py +147 -146
  156. meerk40t/extra/winsleep.py +83 -83
  157. meerk40t/extra/xcs_reader.py +597 -0
  158. meerk40t/fill/fills.py +781 -335
  159. meerk40t/fill/patternfill.py +1061 -1061
  160. meerk40t/fill/patterns.py +614 -567
  161. meerk40t/grbl/control.py +87 -87
  162. meerk40t/grbl/controller.py +990 -903
  163. meerk40t/grbl/device.py +1084 -768
  164. meerk40t/grbl/driver.py +989 -771
  165. meerk40t/grbl/emulator.py +532 -497
  166. meerk40t/grbl/gcodejob.py +783 -767
  167. meerk40t/grbl/gui/grblconfiguration.py +373 -298
  168. meerk40t/grbl/gui/grblcontroller.py +485 -271
  169. meerk40t/grbl/gui/grblhardwareconfig.py +269 -153
  170. meerk40t/grbl/gui/grbloperationconfig.py +105 -0
  171. meerk40t/grbl/gui/gui.py +147 -116
  172. meerk40t/grbl/interpreter.py +44 -44
  173. meerk40t/grbl/loader.py +22 -22
  174. meerk40t/grbl/mock_connection.py +56 -56
  175. meerk40t/grbl/plugin.py +294 -264
  176. meerk40t/grbl/serial_connection.py +93 -88
  177. meerk40t/grbl/tcp_connection.py +81 -79
  178. meerk40t/grbl/ws_connection.py +112 -0
  179. meerk40t/gui/__init__.py +1 -1
  180. meerk40t/gui/about.py +2042 -296
  181. meerk40t/gui/alignment.py +1644 -1608
  182. meerk40t/gui/autoexec.py +199 -0
  183. meerk40t/gui/basicops.py +791 -670
  184. meerk40t/gui/bufferview.py +77 -71
  185. meerk40t/gui/busy.py +232 -133
  186. meerk40t/gui/choicepropertypanel.py +1662 -1469
  187. meerk40t/gui/consolepanel.py +706 -542
  188. meerk40t/gui/devicepanel.py +687 -581
  189. meerk40t/gui/dialogoptions.py +110 -107
  190. meerk40t/gui/executejob.py +316 -306
  191. meerk40t/gui/fonts.py +90 -90
  192. meerk40t/gui/functionwrapper.py +252 -0
  193. meerk40t/gui/gui_mixins.py +729 -0
  194. meerk40t/gui/guicolors.py +205 -182
  195. meerk40t/gui/help_assets/help_assets.py +218 -201
  196. meerk40t/gui/helper.py +154 -0
  197. meerk40t/gui/hersheymanager.py +1440 -846
  198. meerk40t/gui/icons.py +3422 -2747
  199. meerk40t/gui/imagesplitter.py +555 -508
  200. meerk40t/gui/keymap.py +354 -344
  201. meerk40t/gui/laserpanel.py +897 -806
  202. meerk40t/gui/laserrender.py +1470 -1232
  203. meerk40t/gui/lasertoolpanel.py +805 -793
  204. meerk40t/gui/magnetoptions.py +436 -0
  205. meerk40t/gui/materialmanager.py +2944 -0
  206. meerk40t/gui/materialtest.py +1722 -1694
  207. meerk40t/gui/mkdebug.py +646 -359
  208. meerk40t/gui/mwindow.py +163 -140
  209. meerk40t/gui/navigationpanels.py +2605 -2467
  210. meerk40t/gui/notes.py +143 -142
  211. meerk40t/gui/opassignment.py +414 -410
  212. meerk40t/gui/operation_info.py +310 -299
  213. meerk40t/gui/plugin.py +500 -328
  214. meerk40t/gui/position.py +714 -669
  215. meerk40t/gui/preferences.py +901 -650
  216. meerk40t/gui/propertypanels/attributes.py +1461 -1131
  217. meerk40t/gui/propertypanels/blobproperty.py +117 -114
  218. meerk40t/gui/propertypanels/consoleproperty.py +83 -80
  219. meerk40t/gui/propertypanels/gotoproperty.py +77 -0
  220. meerk40t/gui/propertypanels/groupproperties.py +223 -217
  221. meerk40t/gui/propertypanels/hatchproperty.py +489 -469
  222. meerk40t/gui/propertypanels/imageproperty.py +2244 -1384
  223. meerk40t/gui/propertypanels/inputproperty.py +59 -58
  224. meerk40t/gui/propertypanels/opbranchproperties.py +82 -80
  225. meerk40t/gui/propertypanels/operationpropertymain.py +1890 -1638
  226. meerk40t/gui/propertypanels/outputproperty.py +59 -58
  227. meerk40t/gui/propertypanels/pathproperty.py +389 -380
  228. meerk40t/gui/propertypanels/placementproperty.py +1214 -383
  229. meerk40t/gui/propertypanels/pointproperty.py +140 -136
  230. meerk40t/gui/propertypanels/propertywindow.py +313 -181
  231. meerk40t/gui/propertypanels/rasterwizardpanels.py +996 -912
  232. meerk40t/gui/propertypanels/regbranchproperties.py +76 -0
  233. meerk40t/gui/propertypanels/textproperty.py +770 -755
  234. meerk40t/gui/propertypanels/waitproperty.py +56 -55
  235. meerk40t/gui/propertypanels/warpproperty.py +121 -0
  236. meerk40t/gui/propertypanels/wobbleproperty.py +255 -204
  237. meerk40t/gui/ribbon.py +2471 -2210
  238. meerk40t/gui/scene/scene.py +1100 -1051
  239. meerk40t/gui/scene/sceneconst.py +22 -22
  240. meerk40t/gui/scene/scenepanel.py +439 -349
  241. meerk40t/gui/scene/scenespacewidget.py +365 -365
  242. meerk40t/gui/scene/widget.py +518 -505
  243. meerk40t/gui/scenewidgets/affinemover.py +215 -215
  244. meerk40t/gui/scenewidgets/attractionwidget.py +315 -309
  245. meerk40t/gui/scenewidgets/bedwidget.py +120 -97
  246. meerk40t/gui/scenewidgets/elementswidget.py +137 -107
  247. meerk40t/gui/scenewidgets/gridwidget.py +785 -745
  248. meerk40t/gui/scenewidgets/guidewidget.py +765 -765
  249. meerk40t/gui/scenewidgets/laserpathwidget.py +66 -66
  250. meerk40t/gui/scenewidgets/machineoriginwidget.py +86 -86
  251. meerk40t/gui/scenewidgets/nodeselector.py +28 -28
  252. meerk40t/gui/scenewidgets/rectselectwidget.py +592 -346
  253. meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
  254. meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
  255. meerk40t/gui/scenewidgets/selectionwidget.py +2958 -2756
  256. meerk40t/gui/simpleui.py +362 -333
  257. meerk40t/gui/simulation.py +2451 -2094
  258. meerk40t/gui/snapoptions.py +208 -203
  259. meerk40t/gui/spoolerpanel.py +1227 -1180
  260. meerk40t/gui/statusbarwidgets/defaultoperations.py +480 -353
  261. meerk40t/gui/statusbarwidgets/infowidget.py +520 -483
  262. meerk40t/gui/statusbarwidgets/opassignwidget.py +356 -355
  263. meerk40t/gui/statusbarwidgets/selectionwidget.py +172 -171
  264. meerk40t/gui/statusbarwidgets/shapepropwidget.py +754 -236
  265. meerk40t/gui/statusbarwidgets/statusbar.py +272 -260
  266. meerk40t/gui/statusbarwidgets/statusbarwidget.py +268 -270
  267. meerk40t/gui/statusbarwidgets/strokewidget.py +267 -251
  268. meerk40t/gui/themes.py +200 -78
  269. meerk40t/gui/tips.py +590 -0
  270. meerk40t/gui/toolwidgets/circlebrush.py +35 -35
  271. meerk40t/gui/toolwidgets/toolcircle.py +248 -242
  272. meerk40t/gui/toolwidgets/toolcontainer.py +82 -77
  273. meerk40t/gui/toolwidgets/tooldraw.py +97 -90
  274. meerk40t/gui/toolwidgets/toolellipse.py +219 -212
  275. meerk40t/gui/toolwidgets/toolimagecut.py +25 -132
  276. meerk40t/gui/toolwidgets/toolline.py +39 -144
  277. meerk40t/gui/toolwidgets/toollinetext.py +79 -236
  278. meerk40t/gui/toolwidgets/toollinetext_inline.py +296 -0
  279. meerk40t/gui/toolwidgets/toolmeasure.py +163 -216
  280. meerk40t/gui/toolwidgets/toolnodeedit.py +2088 -2074
  281. meerk40t/gui/toolwidgets/toolnodemove.py +92 -94
  282. meerk40t/gui/toolwidgets/toolparameter.py +754 -668
  283. meerk40t/gui/toolwidgets/toolplacement.py +108 -108
  284. meerk40t/gui/toolwidgets/toolpoint.py +68 -59
  285. meerk40t/gui/toolwidgets/toolpointlistbuilder.py +294 -0
  286. meerk40t/gui/toolwidgets/toolpointmove.py +183 -0
  287. meerk40t/gui/toolwidgets/toolpolygon.py +288 -403
  288. meerk40t/gui/toolwidgets/toolpolyline.py +38 -196
  289. meerk40t/gui/toolwidgets/toolrect.py +211 -207
  290. meerk40t/gui/toolwidgets/toolrelocate.py +72 -72
  291. meerk40t/gui/toolwidgets/toolribbon.py +598 -113
  292. meerk40t/gui/toolwidgets/tooltabedit.py +546 -0
  293. meerk40t/gui/toolwidgets/tooltext.py +98 -89
  294. meerk40t/gui/toolwidgets/toolvector.py +213 -204
  295. meerk40t/gui/toolwidgets/toolwidget.py +39 -39
  296. meerk40t/gui/usbconnect.py +98 -91
  297. meerk40t/gui/utilitywidgets/buttonwidget.py +18 -18
  298. meerk40t/gui/utilitywidgets/checkboxwidget.py +90 -90
  299. meerk40t/gui/utilitywidgets/controlwidget.py +14 -14
  300. meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +343 -340
  301. meerk40t/gui/utilitywidgets/debugwidgets.py +148 -0
  302. meerk40t/gui/utilitywidgets/handlewidget.py +27 -27
  303. meerk40t/gui/utilitywidgets/harmonograph.py +450 -447
  304. meerk40t/gui/utilitywidgets/openclosewidget.py +40 -40
  305. meerk40t/gui/utilitywidgets/rotationwidget.py +54 -54
  306. meerk40t/gui/utilitywidgets/scalewidget.py +75 -75
  307. meerk40t/gui/utilitywidgets/seekbarwidget.py +183 -183
  308. meerk40t/gui/utilitywidgets/togglewidget.py +142 -142
  309. meerk40t/gui/utilitywidgets/toolbarwidget.py +8 -8
  310. meerk40t/gui/wordlisteditor.py +985 -931
  311. meerk40t/gui/wxmeerk40t.py +1447 -1169
  312. meerk40t/gui/wxmmain.py +5644 -4112
  313. meerk40t/gui/wxmribbon.py +1591 -1076
  314. meerk40t/gui/wxmscene.py +1631 -1453
  315. meerk40t/gui/wxmtree.py +2416 -2089
  316. meerk40t/gui/wxutils.py +1769 -1099
  317. meerk40t/gui/zmatrix.py +102 -102
  318. meerk40t/image/__init__.py +1 -1
  319. meerk40t/image/dither.py +429 -0
  320. meerk40t/image/imagetools.py +2793 -2269
  321. meerk40t/internal_plugins.py +150 -130
  322. meerk40t/kernel/__init__.py +63 -12
  323. meerk40t/kernel/channel.py +259 -212
  324. meerk40t/kernel/context.py +538 -538
  325. meerk40t/kernel/exceptions.py +41 -41
  326. meerk40t/kernel/functions.py +463 -414
  327. meerk40t/kernel/jobs.py +100 -100
  328. meerk40t/kernel/kernel.py +3828 -3571
  329. meerk40t/kernel/lifecycles.py +71 -71
  330. meerk40t/kernel/module.py +49 -49
  331. meerk40t/kernel/service.py +147 -147
  332. meerk40t/kernel/settings.py +383 -343
  333. meerk40t/lihuiyu/controller.py +883 -876
  334. meerk40t/lihuiyu/device.py +1181 -1069
  335. meerk40t/lihuiyu/driver.py +1466 -1372
  336. meerk40t/lihuiyu/gui/gui.py +127 -106
  337. meerk40t/lihuiyu/gui/lhyaccelgui.py +377 -363
  338. meerk40t/lihuiyu/gui/lhycontrollergui.py +741 -651
  339. meerk40t/lihuiyu/gui/lhydrivergui.py +470 -446
  340. meerk40t/lihuiyu/gui/lhyoperationproperties.py +238 -237
  341. meerk40t/lihuiyu/gui/tcpcontroller.py +226 -190
  342. meerk40t/lihuiyu/interpreter.py +53 -53
  343. meerk40t/lihuiyu/laserspeed.py +450 -450
  344. meerk40t/lihuiyu/loader.py +90 -90
  345. meerk40t/lihuiyu/parser.py +404 -404
  346. meerk40t/lihuiyu/plugin.py +101 -102
  347. meerk40t/lihuiyu/tcp_connection.py +111 -109
  348. meerk40t/main.py +231 -165
  349. meerk40t/moshi/builder.py +788 -781
  350. meerk40t/moshi/controller.py +505 -499
  351. meerk40t/moshi/device.py +495 -442
  352. meerk40t/moshi/driver.py +862 -696
  353. meerk40t/moshi/gui/gui.py +78 -76
  354. meerk40t/moshi/gui/moshicontrollergui.py +538 -522
  355. meerk40t/moshi/gui/moshidrivergui.py +87 -75
  356. meerk40t/moshi/plugin.py +43 -43
  357. meerk40t/network/console_server.py +140 -57
  358. meerk40t/network/kernelserver.py +10 -9
  359. meerk40t/network/tcp_server.py +142 -140
  360. meerk40t/network/udp_server.py +103 -77
  361. meerk40t/network/web_server.py +404 -0
  362. meerk40t/newly/controller.py +1158 -1144
  363. meerk40t/newly/device.py +874 -732
  364. meerk40t/newly/driver.py +540 -412
  365. meerk40t/newly/gui/gui.py +219 -188
  366. meerk40t/newly/gui/newlyconfig.py +116 -101
  367. meerk40t/newly/gui/newlycontroller.py +193 -186
  368. meerk40t/newly/gui/operationproperties.py +51 -51
  369. meerk40t/newly/mock_connection.py +82 -82
  370. meerk40t/newly/newly_params.py +56 -56
  371. meerk40t/newly/plugin.py +1214 -1246
  372. meerk40t/newly/usb_connection.py +322 -322
  373. meerk40t/rotary/gui/gui.py +52 -46
  374. meerk40t/rotary/gui/rotarysettings.py +240 -232
  375. meerk40t/rotary/rotary.py +202 -98
  376. meerk40t/ruida/control.py +291 -91
  377. meerk40t/ruida/controller.py +138 -1088
  378. meerk40t/ruida/device.py +676 -231
  379. meerk40t/ruida/driver.py +534 -472
  380. meerk40t/ruida/emulator.py +1494 -1491
  381. meerk40t/ruida/exceptions.py +4 -4
  382. meerk40t/ruida/gui/gui.py +71 -76
  383. meerk40t/ruida/gui/ruidaconfig.py +239 -72
  384. meerk40t/ruida/gui/ruidacontroller.py +187 -184
  385. meerk40t/ruida/gui/ruidaoperationproperties.py +48 -47
  386. meerk40t/ruida/loader.py +54 -52
  387. meerk40t/ruida/mock_connection.py +57 -109
  388. meerk40t/ruida/plugin.py +124 -87
  389. meerk40t/ruida/rdjob.py +2084 -945
  390. meerk40t/ruida/serial_connection.py +116 -0
  391. meerk40t/ruida/tcp_connection.py +146 -0
  392. meerk40t/ruida/udp_connection.py +73 -0
  393. meerk40t/svgelements.py +9671 -9669
  394. meerk40t/tools/driver_to_path.py +584 -579
  395. meerk40t/tools/geomstr.py +5583 -4680
  396. meerk40t/tools/jhfparser.py +357 -292
  397. meerk40t/tools/kerftest.py +904 -890
  398. meerk40t/tools/livinghinges.py +1168 -1033
  399. meerk40t/tools/pathtools.py +987 -949
  400. meerk40t/tools/pmatrix.py +234 -0
  401. meerk40t/tools/pointfinder.py +942 -942
  402. meerk40t/tools/polybool.py +941 -940
  403. meerk40t/tools/rasterplotter.py +1660 -547
  404. meerk40t/tools/shxparser.py +1047 -901
  405. meerk40t/tools/ttfparser.py +726 -446
  406. meerk40t/tools/zinglplotter.py +595 -593
  407. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/LICENSE +21 -21
  408. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/METADATA +150 -139
  409. meerk40t-0.9.7020.dist-info/RECORD +446 -0
  410. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/WHEEL +1 -1
  411. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/top_level.txt +0 -1
  412. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/zip-safe +1 -1
  413. meerk40t/balormk/elementlightjob.py +0 -159
  414. meerk40t-0.9.3001.dist-info/RECORD +0 -437
  415. test/bootstrap.py +0 -63
  416. test/test_cli.py +0 -12
  417. test/test_core_cutcode.py +0 -418
  418. test/test_core_elements.py +0 -144
  419. test/test_core_plotplanner.py +0 -397
  420. test/test_core_viewports.py +0 -312
  421. test/test_drivers_grbl.py +0 -108
  422. test/test_drivers_lihuiyu.py +0 -443
  423. test/test_drivers_newly.py +0 -113
  424. test/test_element_degenerate_points.py +0 -43
  425. test/test_elements_classify.py +0 -97
  426. test/test_elements_penbox.py +0 -22
  427. test/test_file_svg.py +0 -176
  428. test/test_fill.py +0 -155
  429. test/test_geomstr.py +0 -1523
  430. test/test_geomstr_nodes.py +0 -18
  431. test/test_imagetools_actualize.py +0 -306
  432. test/test_imagetools_wizard.py +0 -258
  433. test/test_kernel.py +0 -200
  434. test/test_laser_speeds.py +0 -3303
  435. test/test_length.py +0 -57
  436. test/test_lifecycle.py +0 -66
  437. test/test_operations.py +0 -251
  438. test/test_operations_hatch.py +0 -57
  439. test/test_ruida.py +0 -19
  440. test/test_spooler.py +0 -22
  441. test/test_tools_rasterplotter.py +0 -29
  442. test/test_wobble.py +0 -133
  443. test/test_zingl.py +0 -124
  444. {test → meerk40t/cylinder}/__init__.py +0 -0
  445. /meerk40t/{core/element_commands.py → cylinder/gui/__init__.py} +0 -0
  446. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/entry_points.txt +0 -0
meerk40t/gui/wxutils.py CHANGED
@@ -1,1099 +1,1769 @@
1
- """
2
- Mixin functions for wxMeerk40t
3
- """
4
- import platform
5
- from typing import List
6
-
7
- import wx
8
- import wx.lib.mixins.listctrl as listmix
9
- from wx.lib.scrolledpanel import ScrolledPanel as SP
10
-
11
- from meerk40t.core.units import ACCEPTED_UNITS, Angle, Length
12
-
13
- _ = wx.GetTranslation
14
-
15
-
16
- ##############
17
- # DYNAMIC CHOICE
18
- # NODE MENU
19
- ##############
20
-
21
-
22
- def create_menu_for_choices(gui, choices: List[dict]) -> wx.Menu:
23
- """
24
- Creates a menu for a given choices table.
25
-
26
- Processes submenus, references, radio_state as needed.
27
- """
28
- menu = wx.Menu()
29
- submenus = {}
30
- choice = dict()
31
-
32
- def get(key, default=None):
33
- try:
34
- return choice[key]
35
- except KeyError:
36
- return default
37
-
38
- def execute(choice):
39
- func = choice["action"]
40
- func_kwargs = choice["kwargs"]
41
- func_args = choice["kwargs"]
42
-
43
- def specific(event=None):
44
- func(*func_args, **func_kwargs)
45
-
46
- return specific
47
-
48
- def set_bool(choice, value):
49
- obj = choice["object"]
50
- param = choice["attr"]
51
-
52
- def check(event=None):
53
- setattr(obj, param, value)
54
-
55
- return check
56
-
57
- for c in choices:
58
- choice = c
59
- submenu_name = get("submenu")
60
- submenu = None
61
- if submenu_name in submenus:
62
- submenu = submenus[submenu_name]
63
- else:
64
- if get("separate_before", default=False):
65
- menu.AppendSeparator()
66
- if submenu_name is not None:
67
- submenu = wx.Menu()
68
- menu.AppendSubMenu(submenu, submenu_name)
69
- submenus[submenu_name] = submenu
70
-
71
- menu_context = submenu if submenu is not None else menu
72
- t = get("type")
73
- if t == bool:
74
- item = menu_context.Append(
75
- wx.ID_ANY, get("label"), get("tip"), wx.ITEM_CHECK
76
- )
77
- obj = get("object")
78
- param = get("attr")
79
- check = bool(getattr(obj, param, False))
80
- item.Check(check)
81
- gui.Bind(
82
- wx.EVT_MENU,
83
- set_bool(choice, not check),
84
- item,
85
- )
86
- elif t == "action":
87
- item = menu_context.Append(
88
- wx.ID_ANY, get("label"), get("tip"), wx.ITEM_NORMAL
89
- )
90
- gui.Bind(
91
- wx.EVT_MENU,
92
- execute(choice),
93
- item,
94
- )
95
- if not submenu and get("separate_after", default=False):
96
- menu.AppendSeparator()
97
- return menu
98
-
99
-
100
- def create_choices_for_node(node, elements) -> List[dict]:
101
- """
102
- Converts a node tree operation menu to a choices dictionary to display the menu items in a choice panel.
103
-
104
- @param node:
105
- @param elements:
106
- @return:
107
- """
108
- choices = []
109
- from meerk40t.core.treeop import get_tree_operation_for_node
110
-
111
- tree_operations_for_node = get_tree_operation_for_node(elements)
112
- for func in tree_operations_for_node(node):
113
- choice = {}
114
- choices.append(choice)
115
- choice["action"] = func
116
- choice["type"] = "action"
117
- choice["submenu"] = func.submenu
118
- choice["kwargs"] = dict()
119
- choice["args"] = tuple()
120
- choice["separate_before"] = func.separate_before
121
- choice["separate_after"] = func.separate_after
122
- choice["label"] = func.name
123
- choice["real_name"] = func.real_name
124
- choice["tip"] = func.help
125
- choice["radio"] = func.radio
126
- choice["reference"] = func.reference
127
- choice["user_prompt"] = func.user_prompt
128
- choice["calcs"] = func.calcs
129
- choice["values"] = func.values
130
- return choices
131
-
132
-
133
- def create_menu_for_node_TEST(gui, node, elements) -> wx.Menu:
134
- """
135
- Test code towards unifying choices and tree nodes into choices that parse to menus.
136
-
137
- This is unused experimental code. Testing the potential inter-relationships between choices for the choice panels
138
- and dynamic node menus.
139
-
140
- @param gui:
141
- @param node:
142
- @param elements:
143
- @return:
144
- """
145
- choices = create_choices_for_node(node, elements)
146
- return create_menu_for_choices(gui, choices)
147
-
148
-
149
- ##############
150
- # DYNAMIC NODE MENU
151
- ##############
152
-
153
-
154
- def create_menu_for_node(gui, node, elements, optional_2nd_node=None) -> wx.Menu:
155
- """
156
- Create menu for a particular node. Does not invoke the menu.
157
-
158
- Processes submenus, references, radio_state as needed.
159
- """
160
- menu = wx.Menu()
161
- submenus = {}
162
- radio_check_not_needed = []
163
- from meerk40t.core.treeop import get_tree_operation_for_node
164
-
165
- tree_operations_for_node = get_tree_operation_for_node(elements)
166
-
167
- def menu_functions(f, node):
168
- func_dict = dict(f.func_dict)
169
-
170
- def specific(event=None):
171
- prompts = f.user_prompt
172
- for prompt in prompts:
173
- response = elements.kernel.prompt(prompt["type"], prompt["prompt"])
174
- if response is None:
175
- return
176
- func_dict[prompt["attr"]] = response
177
- f(node, **func_dict)
178
-
179
- return specific
180
-
181
- # Check specifically for the optional first (use case: reference nodes)
182
- if optional_2nd_node is not None:
183
- mc1 = menu.MenuItemCount
184
- last_was_separator = False
185
-
186
- for func in tree_operations_for_node(optional_2nd_node):
187
- submenu_name = func.submenu
188
- submenu = None
189
- if submenu_name in submenus:
190
- submenu = submenus[submenu_name]
191
- else:
192
- if submenu_name is not None:
193
- last_was_separator = False
194
- submenu = wx.Menu()
195
- menu.AppendSubMenu(submenu, submenu_name, func.help)
196
- submenus[submenu_name] = submenu
197
-
198
- menu_context = submenu if submenu is not None else menu
199
- if func.separate_before:
200
- last_was_separator = True
201
- menu_context.AppendSeparator()
202
- if func.reference is not None:
203
- menu_context.AppendSubMenu(
204
- create_menu_for_node(
205
- gui,
206
- func.reference(optional_2nd_node),
207
- elements,
208
- optional_2nd_node,
209
- ),
210
- func.real_name,
211
- )
212
- continue
213
- if func.radio_state is not None:
214
- last_was_separator = False
215
- item = menu_context.Append(
216
- wx.ID_ANY, func.real_name, func.help, wx.ITEM_RADIO
217
- )
218
- check = func.radio_state
219
- item.Check(check)
220
- if check and menu_context not in radio_check_not_needed:
221
- radio_check_not_needed.append(menu_context)
222
- if func.enabled:
223
- gui.Bind(
224
- wx.EVT_MENU,
225
- menu_functions(func, optional_2nd_node),
226
- item,
227
- )
228
- else:
229
- item.Enable(False)
230
- else:
231
- last_was_separator = False
232
- if hasattr(func, "check_state") and func.check_state is not None:
233
- check = func.check_state
234
- kind = wx.ITEM_CHECK
235
- else:
236
- kind = wx.ITEM_NORMAL
237
- check = None
238
- item = menu_context.Append(wx.ID_ANY, func.real_name, func.help, kind)
239
- if check is not None:
240
- item.Check(check)
241
- if func.enabled:
242
- gui.Bind(
243
- wx.EVT_MENU,
244
- menu_functions(func, node),
245
- item,
246
- )
247
- else:
248
- item.Enable(False)
249
- if menu_context not in radio_check_not_needed:
250
- radio_check_not_needed.append(menu_context)
251
- if not submenu and func.separate_after:
252
- last_was_separator = True
253
- menu.AppendSeparator()
254
- mc2 = menu.MenuItemCount
255
- if not last_was_separator and mc2 - mc1 > 0:
256
- menu.AppendSeparator()
257
-
258
- for func in tree_operations_for_node(node):
259
- submenu_name = func.submenu
260
- submenu = None
261
- if submenu_name in submenus:
262
- submenu = submenus[submenu_name]
263
- else:
264
- if submenu_name is not None:
265
- submenu = wx.Menu()
266
- menu.AppendSubMenu(submenu, submenu_name, func.help)
267
- submenus[submenu_name] = submenu
268
-
269
- menu_context = submenu if submenu is not None else menu
270
- if func.separate_before:
271
- menu_context.AppendSeparator()
272
- if func.reference is not None:
273
- menu_context.AppendSubMenu(
274
- create_menu_for_node(gui, func.reference(node), elements),
275
- func.real_name,
276
- )
277
- continue
278
- if func.radio_state is not None:
279
- item = menu_context.Append(
280
- wx.ID_ANY, func.real_name, func.help, wx.ITEM_RADIO
281
- )
282
- check = func.radio_state
283
- item.Check(check)
284
- if check and menu_context not in radio_check_not_needed:
285
- radio_check_not_needed.append(menu_context)
286
- if func.enabled:
287
- gui.Bind(
288
- wx.EVT_MENU,
289
- menu_functions(func, node),
290
- item,
291
- )
292
- else:
293
- item.Enable(False)
294
- else:
295
- if hasattr(func, "check_state") and func.check_state is not None:
296
- check = func.check_state
297
- kind = wx.ITEM_CHECK
298
- else:
299
- kind = wx.ITEM_NORMAL
300
- check = None
301
- item = menu_context.Append(wx.ID_ANY, func.real_name, func.help, kind)
302
- if check is not None:
303
- item.Check(check)
304
- if func.enabled:
305
- gui.Bind(
306
- wx.EVT_MENU,
307
- menu_functions(func, node),
308
- item,
309
- )
310
- else:
311
- item.Enable(False)
312
-
313
- if menu_context not in radio_check_not_needed:
314
- radio_check_not_needed.append(menu_context)
315
- if not submenu and func.separate_after:
316
- menu.AppendSeparator()
317
-
318
- for submenu in submenus.values():
319
- if submenu not in radio_check_not_needed:
320
- item = submenu.Append(
321
- wx.ID_ANY,
322
- _("Other value..."),
323
- _("Value set using properties"),
324
- wx.ITEM_RADIO,
325
- )
326
- item.Check(True)
327
- return menu
328
-
329
-
330
- def create_menu(gui, node, elements):
331
- """
332
- Create menu items. This is used for both the scene and the tree to create menu items.
333
-
334
- @param gui: Gui used to create menu items.
335
- @param node: The Node clicked on for the generated menu.
336
- @param elements: elements service for use with node creation
337
- @return:
338
- """
339
- if node is None:
340
- return
341
- # Is it a reference object?
342
- optional_node = None
343
- if hasattr(node, "node"):
344
- optional_node = node
345
- node = node.node
346
-
347
- menu = create_menu_for_node(gui, node, elements, optional_node)
348
- if menu.MenuItemCount != 0:
349
- gui.PopupMenu(menu)
350
- menu.Destroy()
351
-
352
-
353
- ##############
354
- # GUI CONTROL OVERRIDES
355
- ##############
356
-
357
-
358
- class TextCtrl(wx.TextCtrl):
359
- """
360
- Just to add some of the more common things we need, i.e. smaller default size...
361
-
362
- Allow text boxes of specific types so that we can have consistent options for dealing with them.
363
- """
364
-
365
- def __init__(
366
- self,
367
- parent,
368
- id=wx.ID_ANY,
369
- value="",
370
- pos=wx.DefaultPosition,
371
- size=wx.DefaultSize,
372
- style=0,
373
- validator=wx.DefaultValidator,
374
- name="",
375
- check="",
376
- limited=False,
377
- nonzero=False,
378
- ):
379
- super().__init__(
380
- parent,
381
- id=id,
382
- value=value,
383
- pos=pos,
384
- size=size,
385
- style=style,
386
- validator=validator,
387
- name=name,
388
- )
389
- self.parent = parent
390
- self.extend_default_units_if_empty = True
391
- self._check = check
392
- self._style = style
393
- self._nonzero = nonzero
394
- if self._nonzero is None:
395
- self._nonzero = False
396
- # For the sake of readability we allow multiple occurrences of
397
- # the same character in the string even if it's unnecessary...
398
- floatstr = "+-.eE0123456789"
399
- unitstr = "".join(ACCEPTED_UNITS)
400
- angle_units = (
401
- "deg",
402
- "rad",
403
- "grad",
404
- "turn",
405
- r"%",
406
- )
407
- anglestr = "".join(angle_units)
408
- self.charpattern = ""
409
- if self._check == "length":
410
- self.charpattern = floatstr + unitstr
411
- elif self._check == "percent":
412
- self.charpattern = floatstr + r"%"
413
- elif self._check == "float":
414
- self.charpattern = floatstr
415
- elif self._check == "angle":
416
- self.charpattern = floatstr + anglestr
417
- elif self._check == "int":
418
- self.charpattern = r"-+0123456789"
419
- self.lower_limit = None
420
- self.upper_limit = None
421
- self.lower_limit_err = None
422
- self.upper_limit_err = None
423
- self.lower_limit_warn = None
424
- self.upper_limit_warn = None
425
- self._default_color_background = None
426
- self._error_color_background = wx.RED
427
- self._warn_color_background = wx.YELLOW
428
- self._modify_color_background = None
429
-
430
- self._default_color_foreground = None
431
- self._error_color_foreground = None
432
- self._warn_color_foreground = wx.BLACK
433
- self._modify_color_foreground = None
434
- self._warn_status = "modified"
435
-
436
- self._last_valid_value = None
437
- self._event_generated = None
438
- self._action_routine = None
439
-
440
- # You can set this to False, if you don't want logic to interfere with text input
441
- self.execute_action_on_change = True
442
-
443
- if self._check is not None and self._check != "":
444
- self.Bind(wx.EVT_KEY_DOWN, self.on_char)
445
- self.Bind(wx.EVT_KEY_UP, self.on_check)
446
- self.Bind(wx.EVT_SET_FOCUS, self.on_enter_field)
447
- self.Bind(wx.EVT_KILL_FOCUS, self.on_leave_field)
448
- if self._style & wx.TE_PROCESS_ENTER != 0:
449
- self.Bind(wx.EVT_TEXT_ENTER, self.on_enter)
450
- _MIN_WIDTH, _MAX_WIDTH = self.validate_widths()
451
- self.SetMinSize(dip_size(self, _MIN_WIDTH, -1))
452
- if limited:
453
- self.SetMaxSize(dip_size(self, _MAX_WIDTH, -1))
454
-
455
- def validate_widths(self):
456
- minw = 35
457
- maxw = 100
458
- minpattern = "0000"
459
- maxpattern = "999999999.99mm"
460
- if self._check == "length":
461
- minpattern = "0000"
462
- maxpattern = "999999999.99mm"
463
- elif self._check == "percent":
464
- minpattern = "0000"
465
- maxpattern = "99.99%"
466
- elif self._check == "float":
467
- minpattern = "0000"
468
- maxpattern = "99999.99"
469
- elif self._check == "angle":
470
- minpattern = "0000"
471
- maxpattern = "9999.99deg"
472
- elif self._check == "int":
473
- minpattern = "0000"
474
- maxpattern = "-999999"
475
- # Let's be a bit more specific: what is the minimum size of the textcontrol fonts
476
- # to hold these patterns
477
- tfont = self.GetFont()
478
- xsize = 15
479
- imgBit = wx.Bitmap(xsize, xsize)
480
- dc = wx.MemoryDC(imgBit)
481
- dc.SelectObject(imgBit)
482
- dc.SetFont(tfont)
483
- f_width, f_height, f_descent, f_external_leading = dc.GetFullTextExtent(
484
- minpattern
485
- )
486
- minw = f_width + 5
487
- f_width, f_height, f_descent, f_external_leading = dc.GetFullTextExtent(
488
- maxpattern
489
- )
490
- maxw = f_width + 10
491
- # Now release dc
492
- dc.SelectObject(wx.NullBitmap)
493
- return minw, maxw
494
-
495
- def SetActionRoutine(self, action_routine):
496
- """
497
- This routine will be called after a lost_focus / text_enter event,
498
- it's a simple way of dealing with all the
499
- ctrl.bind(wx.EVT_KILL_FOCUS / wx.EVT_TEXT_ENTER) things
500
- Yes, you can still have them, but you should call
501
- ctrl.prevalidate()
502
- then to ensure the logic to avoid invalid content is been called.
503
- If you need to programmatically distinguish between a lost focus
504
- and text_enter event, then consult
505
- ctrl.event_generated()
506
- this will give back wx.EVT_KILL_FOCUS or wx.EVT_TEXT_ENTER
507
- """
508
- self._action_routine = action_routine
509
-
510
- def event_generated(self):
511
- """
512
- This routine will give back wx.EVT_KILL_FOCUS or wx.EVT_TEXT_ENTER
513
- if called during an execution of the validator routine, see above,
514
- or None in any other case
515
- """
516
- return self._event_generated
517
-
518
- def get_warn_status(self, txt):
519
- status = ""
520
- try:
521
- value = None
522
- if self._check == "float":
523
- value = float(txt)
524
- elif self._check == "percent":
525
- if txt.endswith("%"):
526
- value = float(txt[:-1]) / 100.0
527
- else:
528
- value = float(txt)
529
- elif self._check == "int":
530
- value = int(txt)
531
- elif self._check == "empty":
532
- if len(txt) == 0:
533
- status = "error"
534
- elif self._check == "length":
535
- value = Length(txt)
536
- elif self._check == "angle":
537
- value = Angle(txt)
538
- # we passed so far, thus the values are syntactically correct
539
- # Now check for content compliance
540
- if value is not None:
541
- if self.lower_limit is not None and value < self.lower_limit:
542
- value = self.lower_limit
543
- self.SetValue(str(value))
544
- status = "default"
545
- if self.upper_limit is not None and value > self.upper_limit:
546
- value = self.upper_limit
547
- self.SetValue(str(value))
548
- status = "default"
549
- if self.lower_limit_warn is not None and value < self.lower_limit_warn:
550
- status = "warning"
551
- if self.upper_limit_warn is not None and value > self.upper_limit_warn:
552
- status = "warning"
553
- if self.lower_limit_err is not None and value < self.lower_limit_err:
554
- status = "error"
555
- if self.upper_limit_err is not None and value > self.upper_limit_err:
556
- status = "error"
557
- if self._nonzero and value == 0:
558
- status = "error"
559
- except ValueError:
560
- status = "error"
561
- return status
562
-
563
- def SetValue(self, newvalue):
564
- identical = False
565
- current = super().GetValue()
566
- if self._check == "float":
567
- try:
568
- v1 = float(current)
569
- v2 = float(newvalue)
570
- if v1 == v2:
571
- identical = True
572
- except ValueError:
573
- pass
574
- if identical:
575
- # print (f"...ignored {current}={v1}, {newvalue}={v2}")
576
- return
577
- # print(f"SetValue called: {current} != {newvalue}")
578
- self._last_valid_value = newvalue
579
- status = self.get_warn_status(newvalue)
580
- self.warn_status = status
581
- cursor = self.GetInsertionPoint()
582
- super().SetValue(newvalue)
583
- cursor = min(len(newvalue), cursor)
584
- self.SetInsertionPoint(cursor)
585
-
586
- def set_error_level(self, err_min, err_max):
587
- self.lower_limit_err = err_min
588
- self.upper_limit_err = err_max
589
-
590
- def set_warn_level(self, warn_min, warn_max):
591
- self.lower_limit_warn = warn_min
592
- self.upper_limit_warn = warn_max
593
-
594
- def set_range(self, range_min, range_max):
595
- self.lower_limit = range_min
596
- self.upper_limit = range_max
597
-
598
- def prevalidate(self, origin=None):
599
- # Check whether the field is okay, if not then put it to the last value
600
- txt = super().GetValue()
601
- # print (f"prevalidate called from: {origin}, check={self._check}, content:{txt}")
602
- if self.warn_status == "error" and self._last_valid_value is not None:
603
- # ChangeValue is not creating any events...
604
- self.ChangeValue(self._last_valid_value)
605
- self.warn_status = ""
606
- elif (
607
- txt != "" and self._check == "length" and self.extend_default_units_if_empty
608
- ):
609
- # Do we have non-existing units provided? --> Change content
610
- purenumber = True
611
- unitstr = "".join(ACCEPTED_UNITS)
612
- for c in unitstr:
613
- if c in txt:
614
- purenumber = False
615
- break
616
- if purenumber and hasattr(self.parent, "context"):
617
- context = self.parent.context
618
- root = context.root
619
- root.setting(str, "units_name", "mm")
620
- units = root.units_name
621
- if units in ("inch", "inches"):
622
- units = "in"
623
- txt = txt.strip() + units
624
- self.ChangeValue(txt)
625
-
626
- def on_enter_field(self, event):
627
- self._last_valid_value = super().GetValue()
628
- event.Skip()
629
-
630
- def on_leave_field(self, event):
631
- # Needs to be passed on
632
- event.Skip()
633
- self.prevalidate("leave")
634
- if self._action_routine is not None:
635
- self._event_generated = wx.EVT_KILL_FOCUS
636
- self._action_routine()
637
- self._event_generated = None
638
- self.SelectNone()
639
- # We assume it's been dealt with, so we recolor...
640
- self.SetModified(False)
641
- self.warn_status = self._warn_status
642
-
643
- def on_enter(self, event):
644
- # Let others deal with it after me
645
- event.Skip()
646
- self.prevalidate("enter")
647
- if self._action_routine is not None:
648
- self._event_generated = wx.EVT_TEXT_ENTER
649
- self._action_routine()
650
- self._event_generated = None
651
- self.SelectNone()
652
- # We assume it's been dealt with, so we recolor...
653
- self.SetModified(False)
654
- self.warn_status = self._warn_status
655
-
656
- @property
657
- def warn_status(self):
658
- return self._warn_status
659
-
660
- @warn_status.setter
661
- def warn_status(self, value):
662
- self._warn_status = value
663
- background = self._default_color_background
664
- foreground = self._default_color_foreground
665
- if value == "modified":
666
- # Is it modified?
667
- if self.IsModified():
668
- background = self._modify_color_background
669
- foreground = self._modify_color_foreground
670
- elif value == "warning":
671
- background = self._warn_color_background
672
- foreground = self._warn_color_foreground
673
- elif value == "error":
674
- background = self._error_color_background
675
- foreground = self._error_color_foreground
676
- self.SetBackgroundColour(background)
677
- self.SetForegroundColour(foreground)
678
- self.Refresh()
679
-
680
- def on_char(self, event):
681
- proceed = True
682
- # The French azerty keyboard generates numbers by pressing Shift + some key
683
- # Under Linux this is not properly translated by GetUnicodeKey and
684
- # is hence leading to a 'wrong' character being recognised (the original key).
685
- # So we can't rely on a proper representation if the Shift-Key
686
- # is held down, sigh.
687
- if self.charpattern != "" and not event.ShiftDown():
688
- keyc = event.GetUnicodeKey()
689
- special = False
690
- if event.RawControlDown() or event.ControlDown() or event.AltDown():
691
- # GetUnicodeKey ignores all special keys, so we need to acknowledge that
692
- special = True
693
- if keyc == 127: # delete
694
- special = True
695
- if keyc != wx.WXK_NONE and not special:
696
- # a 'real' character?
697
- if keyc >= ord(" "):
698
- char = chr(keyc).lower()
699
- if char not in self.charpattern:
700
- proceed = False
701
- # print(f"Ignored: {keyc} - {char}")
702
- if proceed:
703
- event.DoAllowNextEvent()
704
- event.Skip()
705
-
706
- def on_check(self, event):
707
- event.Skip()
708
- txt = super().GetValue()
709
- status = self.get_warn_status(txt)
710
- if status == "":
711
- status = "modified"
712
- self.warn_status = status
713
- # Is it a valid value?
714
- lenokay = True
715
- if len(txt) == 0 and self._check in (
716
- "float",
717
- "length",
718
- "angle",
719
- "int",
720
- "percent",
721
- ):
722
- lenokay = False
723
- if (
724
- self.execute_action_on_change
725
- and status == "modified"
726
- and hasattr(self.parent, "context")
727
- and lenokay
728
- ):
729
- if getattr(self.parent.context.root, "process_while_typing", False):
730
- if self._action_routine is not None:
731
- self._event_generated = wx.EVT_TEXT
732
- self._action_routine()
733
- self._event_generated = None
734
-
735
- @property
736
- def is_changed(self):
737
- return self.GetValue() != self._last_valid_value
738
-
739
- @property
740
- def Value(self):
741
- return self.GetValue()
742
-
743
- def GetValue(self):
744
- result = super().GetValue()
745
- if (
746
- result != ""
747
- and self._check == "length"
748
- and self.extend_default_units_if_empty
749
- ):
750
- purenumber = True
751
- unitstr = "".join(ACCEPTED_UNITS)
752
- for c in unitstr:
753
- if c in result:
754
- purenumber = False
755
- break
756
- if purenumber and hasattr(self.parent, "context"):
757
- context = self.parent.context
758
- root = context.root
759
- root.setting(str, "units_name", "mm")
760
- units = root.units_name
761
- if units in ("inch", "inches"):
762
- units = "in"
763
- result = result.strip()
764
- if result.endswith("."):
765
- result += "0"
766
- result += units
767
- return result
768
-
769
-
770
- class CheckBox(wx.CheckBox):
771
- """
772
- This checkbox replaces wx.Checkbox and creates a series of mouse over tool tips to permit Linux tooltips that
773
- otherwise do not show.
774
- """
775
-
776
- def __init__(
777
- self,
778
- *args,
779
- **kwargs,
780
- ):
781
- self._tool_tip = None
782
- super().__init__(*args, **kwargs)
783
- if platform.system() == "Linux":
784
-
785
- def on_mouse_over_check(ctrl):
786
- def mouse(event=None):
787
- ctrl.SetToolTip(self._tool_tip)
788
-
789
- return mouse
790
-
791
- self.Bind(wx.EVT_MOTION, on_mouse_over_check(super()))
792
-
793
- def SetToolTip(self, tooltip):
794
- self._tool_tip = tooltip
795
- super().SetToolTip(self._tool_tip)
796
-
797
-
798
- class StaticBoxSizer(wx.StaticBoxSizer):
799
- def __init__(
800
- self,
801
- parent,
802
- id=wx.ID_ANY,
803
- label="",
804
- orientation=wx.HORIZONTAL,
805
- *args,
806
- **kwargs,
807
- ):
808
- self.sbox = wx.StaticBox(parent, id, label=label)
809
- self.sbox.SetMinSize(dip_size(self, 50, 50))
810
- super().__init__(self.sbox, orientation)
811
-
812
- def Show(self, show=True):
813
- self.sbox.Show(show)
814
-
815
- def SetLabel(self, label):
816
- self.sbox.SetLabel(label)
817
-
818
- def Refresh(self, *args):
819
- self.sbox.Refresh(*args)
820
-
821
-
822
- class ScrolledPanel(SP):
823
- """
824
- We sometimes delete things fast enough that they call _SetupAfter when dead and crash.
825
- """
826
-
827
- def _SetupAfter(self, scrollToTop):
828
- try:
829
- self.SetVirtualSize(self.GetBestVirtualSize())
830
- if scrollToTop:
831
- self.Scroll(0, 0)
832
- except RuntimeError:
833
- pass
834
-
835
-
836
- class EditableListCtrl(wx.ListCtrl, listmix.TextEditMixin):
837
- """TextEditMixin allows any column to be edited."""
838
-
839
- # ----------------------------------------------------------------------
840
- def __init__(
841
- self, parent, ID=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0
842
- ):
843
- """Constructor"""
844
- wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
845
- listmix.TextEditMixin.__init__(self)
846
-
847
-
848
- class HoverButton(wx.Button):
849
- """
850
- Provide a button with Hover-Color changing ability.
851
- """
852
-
853
- def __init__(self, parent, ID, label):
854
- super().__init__(parent, ID, label)
855
- self._focus_color = None
856
- self._disable_color = None
857
- self._foreground_color = self.GetForegroundColour()
858
- self._background_color = self.GetBackgroundColour()
859
- self.Bind(wx.EVT_ENTER_WINDOW, self.on_enter)
860
- self.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave)
861
- # self.Bind(wx.EVT_MOUSE_EVENTS, self.on_mouse)
862
-
863
- def SetFocusColour(self, color):
864
- self._focus_color = wx.Colour(color)
865
-
866
- def SetDisabledBackgroundColour(self, color):
867
- self._disable_color = wx.Colour(color)
868
-
869
- def SetForegroundColour(self, color):
870
- self._foreground_color = wx.Colour(color)
871
- super().SetForegroundColour(color)
872
-
873
- def SetBackgroundColour(self, color):
874
- self._background_color = wx.Colour(color)
875
- super().SetBackgroundColour(color)
876
-
877
- def GetFocusColour(self, color):
878
- return self._focus_color
879
-
880
- def Enable(self, value):
881
- if value:
882
- super().SetBackgroundColour(self._background_color)
883
- else:
884
- if self._disable_color is None:
885
- r, g, b, a = self._background_color.Get()
886
- color = wx.Colour(
887
- min(255, int(1.5 * r)),
888
- min(255, int(1.5 * g)),
889
- min(255, int(1.5 * b)),
890
- )
891
- else:
892
- color = self._disable_color
893
- super().SetBackgroundColour(color)
894
- super().Enable(value)
895
- self.Refresh()
896
-
897
- def on_enter(self, event):
898
- if self._focus_color is not None:
899
- super().SetForegroundColour(self._focus_color)
900
- self.Refresh()
901
- event.Skip()
902
-
903
- def on_leave(self, event):
904
- super().SetForegroundColour(self._foreground_color)
905
- self.Refresh()
906
- event.Skip()
907
-
908
- # def on_mouse(self, event):
909
- # if event.Leaving():
910
- # self.on_leave(event)
911
- # event.Skip()
912
-
913
-
914
- ##############
915
- # GUI KEYSTROKE FUNCTIONS
916
- ##############
917
-
918
- WX_METAKEYS = [
919
- wx.WXK_START,
920
- wx.WXK_WINDOWS_LEFT,
921
- wx.WXK_WINDOWS_RIGHT,
922
- ]
923
-
924
- WX_MODIFIERS = {
925
- wx.WXK_CONTROL: "ctrl",
926
- wx.WXK_RAW_CONTROL: "macctl",
927
- wx.WXK_ALT: "alt",
928
- wx.WXK_SHIFT: "shift",
929
- wx.WXK_START: "start",
930
- wx.WXK_WINDOWS_LEFT: "win-left",
931
- wx.WXK_WINDOWS_RIGHT: "win-right",
932
- }
933
-
934
- WX_SPECIALKEYS = {
935
- wx.WXK_F1: "f1",
936
- wx.WXK_F2: "f2",
937
- wx.WXK_F3: "f3",
938
- wx.WXK_F4: "f4",
939
- wx.WXK_F5: "f5",
940
- wx.WXK_F6: "f6",
941
- wx.WXK_F7: "f7",
942
- wx.WXK_F8: "f8",
943
- wx.WXK_F9: "f9",
944
- wx.WXK_F10: "f10",
945
- wx.WXK_F11: "f11",
946
- wx.WXK_F12: "f12",
947
- wx.WXK_F13: "f13",
948
- wx.WXK_F14: "f14",
949
- wx.WXK_F15: "f15",
950
- wx.WXK_F16: "f16",
951
- wx.WXK_F17: "f17",
952
- wx.WXK_F18: "f18",
953
- wx.WXK_F19: "f19",
954
- wx.WXK_F20: "f20",
955
- wx.WXK_F21: "f21",
956
- wx.WXK_F22: "f22",
957
- wx.WXK_F23: "f23",
958
- wx.WXK_F24: "f24",
959
- wx.WXK_ADD: "+",
960
- wx.WXK_END: "end",
961
- wx.WXK_NUMPAD0: "numpad0",
962
- wx.WXK_NUMPAD1: "numpad1",
963
- wx.WXK_NUMPAD2: "numpad2",
964
- wx.WXK_NUMPAD3: "numpad3",
965
- wx.WXK_NUMPAD4: "numpad4",
966
- wx.WXK_NUMPAD5: "numpad5",
967
- wx.WXK_NUMPAD6: "numpad6",
968
- wx.WXK_NUMPAD7: "numpad7",
969
- wx.WXK_NUMPAD8: "numpad8",
970
- wx.WXK_NUMPAD9: "numpad9",
971
- wx.WXK_NUMPAD_ADD: "numpad_add",
972
- wx.WXK_NUMPAD_SUBTRACT: "numpad_subtract",
973
- wx.WXK_NUMPAD_MULTIPLY: "numpad_multiply",
974
- wx.WXK_NUMPAD_DIVIDE: "numpad_divide",
975
- wx.WXK_NUMPAD_DECIMAL: "numpad.",
976
- wx.WXK_NUMPAD_ENTER: "numpad_enter",
977
- wx.WXK_NUMPAD_RIGHT: "numpad_right",
978
- wx.WXK_NUMPAD_LEFT: "numpad_left",
979
- wx.WXK_NUMPAD_UP: "numpad_up",
980
- wx.WXK_NUMPAD_DOWN: "numpad_down",
981
- wx.WXK_NUMPAD_DELETE: "numpad_delete",
982
- wx.WXK_NUMPAD_INSERT: "numpad_insert",
983
- wx.WXK_NUMPAD_PAGEUP: "numpad_pgup",
984
- wx.WXK_NUMPAD_PAGEDOWN: "numpad_pgdn",
985
- wx.WXK_NUMPAD_HOME: "numpad_home",
986
- wx.WXK_NUMPAD_END: "numpad_end",
987
- wx.WXK_NUMLOCK: "num_lock",
988
- wx.WXK_SCROLL: "scroll_lock",
989
- wx.WXK_CAPITAL: "caps_lock",
990
- wx.WXK_HOME: "home",
991
- wx.WXK_DOWN: "down",
992
- wx.WXK_UP: "up",
993
- wx.WXK_RIGHT: "right",
994
- wx.WXK_LEFT: "left",
995
- wx.WXK_ESCAPE: "escape",
996
- wx.WXK_BACK: "back",
997
- wx.WXK_PAUSE: "pause",
998
- wx.WXK_PAGEDOWN: "pagedown",
999
- wx.WXK_PAGEUP: "pageup",
1000
- wx.WXK_PRINT: "print",
1001
- wx.WXK_RETURN: "return",
1002
- wx.WXK_SPACE: "space",
1003
- wx.WXK_TAB: "tab",
1004
- wx.WXK_DELETE: "delete",
1005
- wx.WXK_INSERT: "insert",
1006
- wx.WXK_SPECIAL1: "special1",
1007
- wx.WXK_SPECIAL2: "special2",
1008
- wx.WXK_SPECIAL3: "special3",
1009
- wx.WXK_SPECIAL4: "special4",
1010
- wx.WXK_SPECIAL5: "special5",
1011
- wx.WXK_SPECIAL6: "special6",
1012
- wx.WXK_SPECIAL7: "special7",
1013
- wx.WXK_SPECIAL8: "special8",
1014
- wx.WXK_SPECIAL9: "special9",
1015
- wx.WXK_SPECIAL10: "special10",
1016
- wx.WXK_SPECIAL11: "special11",
1017
- wx.WXK_SPECIAL12: "special12",
1018
- wx.WXK_SPECIAL13: "special13",
1019
- wx.WXK_SPECIAL14: "special14",
1020
- wx.WXK_SPECIAL15: "special15",
1021
- wx.WXK_SPECIAL16: "special16",
1022
- wx.WXK_SPECIAL17: "special17",
1023
- wx.WXK_SPECIAL18: "special18",
1024
- wx.WXK_SPECIAL19: "special19",
1025
- wx.WXK_SPECIAL20: "special20",
1026
- wx.WXK_CLEAR: "clear",
1027
- wx.WXK_WINDOWS_MENU: "menu",
1028
- }
1029
-
1030
-
1031
- def is_navigation_key(keyvalue):
1032
- if keyvalue is None:
1033
- return False
1034
- if "right" in keyvalue:
1035
- return True
1036
- if "left" in keyvalue:
1037
- return True
1038
- if "up" in keyvalue and "pgup" not in keyvalue and "pageup" not in keyvalue:
1039
- return True
1040
- if "down" in keyvalue and "pagedown" not in keyvalue:
1041
- return True
1042
- if "tab" in keyvalue:
1043
- return True
1044
- if "return" in keyvalue:
1045
- return True
1046
- return False
1047
-
1048
-
1049
- def get_key_name(event, return_modifier=False):
1050
- keyvalue = ""
1051
- # https://wxpython.org/Phoenix/docs/html/wx.KeyEvent.html
1052
- key = event.GetUnicodeKey()
1053
- if key == wx.WXK_NONE:
1054
- key = event.GetKeyCode()
1055
- if event.RawControlDown() and not event.ControlDown():
1056
- keyvalue += "macctl+" # Deliberately not macctrl+
1057
- elif event.ControlDown():
1058
- keyvalue += "ctrl+"
1059
- if event.AltDown() or key == wx.WXK_ALT:
1060
- keyvalue += "alt+"
1061
- if event.ShiftDown():
1062
- keyvalue += "shift+"
1063
- if event.MetaDown() or key in WX_METAKEYS:
1064
- keyvalue += "meta+"
1065
- # if return_modifier and keyvalue: print("key", key, keyvalue)
1066
- if key in WX_MODIFIERS:
1067
- return keyvalue if return_modifier else None
1068
- if key in WX_SPECIALKEYS:
1069
- keyvalue += WX_SPECIALKEYS[key]
1070
- else:
1071
- keyvalue += chr(key)
1072
- # print("key", key, keyvalue)
1073
- return keyvalue.lower()
1074
-
1075
-
1076
- def disable_window(window):
1077
- for m in window.Children:
1078
- if hasattr(m, "Disable"):
1079
- m.Disable()
1080
- if hasattr(m, "Children"):
1081
- disable_window(m)
1082
-
1083
-
1084
- def set_ctrl_value(ctrl, value):
1085
- # Let's try to save the caret position
1086
- cursor = ctrl.GetInsertionPoint()
1087
- if ctrl.GetValue() != value:
1088
- ctrl.SetValue(value)
1089
- ctrl.SetInsertionPoint(min(len(value), cursor))
1090
-
1091
-
1092
- def dip_size(frame, x, y):
1093
- # wx.Window.FromDIP was introduced with wxPython 4.1, so not all distros may have this
1094
- wxsize = wx.Size(x, y)
1095
- try:
1096
- dipsize = frame.FromDIP(wxsize)
1097
- return dipsize
1098
- except AttributeError:
1099
- return wxsize
1
+ """
2
+ Mixin functions for wxMeerk40t
3
+ """
4
+ import platform
5
+ from typing import List
6
+
7
+ import wx
8
+ import wx.lib.mixins.listctrl as listmix
9
+ from wx.lib.scrolledpanel import ScrolledPanel as SP
10
+
11
+ from meerk40t.svgelements import Matrix
12
+ from meerk40t.core.units import ACCEPTED_ANGLE_UNITS, ACCEPTED_UNITS, Angle, Length
13
+
14
+ _ = wx.GetTranslation
15
+
16
+
17
+ ##############
18
+ # DYNAMIC CHOICE
19
+ # NODE MENU
20
+ ##############
21
+
22
+
23
+ def get_matrix_scale(matrix):
24
+ # We usually use the value_scale_x to establish a pixel size
25
+ # by counteracting the scene matrix, linewidth = 1 / matrix.value_scale_x()
26
+ # For a rotated scene this crashes, so we need to take
27
+ # that into consideration, so let's look at the
28
+ # distance from (1, 0) to (0, 0) and call this our scale
29
+ from math import sqrt
30
+
31
+ x0, y0 = matrix.point_in_matrix_space((0, 0))
32
+ x1, y1 = matrix.point_in_matrix_space((1, 0))
33
+ res = sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
34
+ if res < 1e-8:
35
+ res = 1
36
+ return res
37
+
38
+ def get_matrix_full_scale(matrix):
39
+ # We usually use the value_scale_x to establish a pixel size
40
+ # by counteracting the scene matrix, linewidth = 1 / matrix.value_scale_x()
41
+ # For a rotated scene this crashes, so we need to take
42
+ # that into consideration, so let's look at the
43
+ # distance from (1, 0) to (0, 0) and call this our scale
44
+ from math import sqrt
45
+
46
+ x0, y0 = matrix.point_in_matrix_space((0, 0))
47
+ x1, y1 = matrix.point_in_matrix_space((1, 0))
48
+ resx = sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
49
+ if resx < 1e-8:
50
+ resx = 1
51
+ x1, y1 = matrix.point_in_matrix_space((0, 1))
52
+ resy = sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
53
+ if resy < 1e-8:
54
+ resy = 1
55
+ return resx, resy
56
+
57
+ def get_gc_scale(gc):
58
+ gcmat = gc.GetTransform()
59
+ mat_param = gcmat.Get()
60
+ testmatrix = Matrix(
61
+ mat_param[0],
62
+ mat_param[1],
63
+ mat_param[2],
64
+ mat_param[3],
65
+ mat_param[4],
66
+ mat_param[5],
67
+ )
68
+ return get_matrix_scale(testmatrix)
69
+
70
+ def get_gc_full_scale(gc):
71
+ gcmat = gc.GetTransform()
72
+ mat_param = gcmat.Get()
73
+ testmatrix = Matrix(
74
+ mat_param[0],
75
+ mat_param[1],
76
+ mat_param[2],
77
+ mat_param[3],
78
+ mat_param[4],
79
+ mat_param[5],
80
+ )
81
+ return get_matrix_full_scale(testmatrix)
82
+
83
+ def create_menu_for_choices(gui, choices: List[dict]) -> wx.Menu:
84
+ """
85
+ Creates a menu for a given choices table.
86
+
87
+ Processes submenus, references, radio_state as needed.
88
+ """
89
+ menu = wx.Menu()
90
+ submenus = {}
91
+ choice = dict()
92
+
93
+ def get(key, default=None):
94
+ try:
95
+ return choice[key]
96
+ except KeyError:
97
+ return default
98
+
99
+ def execute(choice):
100
+ func = choice["action"]
101
+ func_kwargs = choice["kwargs"]
102
+ func_args = choice["kwargs"]
103
+
104
+ def specific(event=None):
105
+ func(*func_args, **func_kwargs)
106
+
107
+ return specific
108
+
109
+ def set_bool(choice, value):
110
+ obj = choice["object"]
111
+ param = choice["attr"]
112
+
113
+ def check(event=None):
114
+ setattr(obj, param, value)
115
+
116
+ return check
117
+
118
+ for c in choices:
119
+ choice = c
120
+ submenu_name = get("submenu")
121
+ submenu = None
122
+ if submenu_name and submenu_name in submenus:
123
+ submenu = submenus[submenu_name]
124
+ else:
125
+ if get("separate_before", default=False):
126
+ menu.AppendSeparator()
127
+ c["separate_before"] = False
128
+ if submenu_name:
129
+ submenu = wx.Menu()
130
+ menu.AppendSubMenu(submenu, submenu_name)
131
+ submenus[submenu_name] = submenu
132
+
133
+ menu_context = submenu if submenu is not None else menu
134
+ if get("separate_before", default=False):
135
+ menu.AppendSeparator()
136
+ c["separate_before"] = False
137
+ t = get("type")
138
+ if t == bool:
139
+ item = menu_context.Append(
140
+ wx.ID_ANY, get("label"), get("tip"), wx.ITEM_CHECK
141
+ )
142
+ obj = get("object")
143
+ param = get("attr")
144
+ check = bool(getattr(obj, param, False))
145
+ item.Check(check)
146
+ gui.Bind(
147
+ wx.EVT_MENU,
148
+ set_bool(choice, not check),
149
+ item,
150
+ )
151
+ elif t == "action":
152
+ item = menu_context.Append(
153
+ wx.ID_ANY, get("label"), get("tip"), wx.ITEM_NORMAL
154
+ )
155
+ gui.Bind(
156
+ wx.EVT_MENU,
157
+ execute(choice),
158
+ item,
159
+ )
160
+ if not submenu and get("separate_after", default=False):
161
+ menu.AppendSeparator()
162
+ return menu
163
+
164
+
165
+ def create_choices_for_node(node, elements) -> List[dict]:
166
+ """
167
+ Converts a node tree operation menu to a choices dictionary to display the menu items in a choice panel.
168
+
169
+ @param node:
170
+ @param elements:
171
+ @return:
172
+ """
173
+ choices = []
174
+ from meerk40t.core.treeop import get_tree_operation_for_node
175
+
176
+ tree_operations_for_node = get_tree_operation_for_node(elements)
177
+ for func in tree_operations_for_node(node):
178
+ choice = {}
179
+ choices.append(choice)
180
+ choice["action"] = func
181
+ choice["type"] = "action"
182
+ choice["submenu"] = func.submenu
183
+ choice["kwargs"] = dict()
184
+ choice["args"] = tuple()
185
+ choice["separate_before"] = func.separate_before
186
+ choice["separate_after"] = func.separate_after
187
+ choice["label"] = func.name
188
+ choice["real_name"] = func.real_name
189
+ choice["tip"] = func.help
190
+ choice["radio"] = func.radio
191
+ choice["reference"] = func.reference
192
+ choice["user_prompt"] = func.user_prompt
193
+ choice["calcs"] = func.calcs
194
+ choice["values"] = func.values
195
+ return choices
196
+
197
+
198
+ def create_menu_for_node_TEST(gui, node, elements) -> wx.Menu:
199
+ """
200
+ Test code towards unifying choices and tree nodes into choices that parse to menus.
201
+
202
+ This is unused experimental code. Testing the potential interrelationships between choices for the choice panels
203
+ and dynamic node menus.
204
+
205
+ @param gui:
206
+ @param node:
207
+ @param elements:
208
+ @return:
209
+ """
210
+ choices = create_choices_for_node(node, elements)
211
+ return create_menu_for_choices(gui, choices)
212
+
213
+
214
+ ##############
215
+ # DYNAMIC NODE MENU
216
+ ##############
217
+
218
+
219
+ def create_menu_for_node(gui, node, elements, optional_2nd_node=None) -> wx.Menu:
220
+ """
221
+ Create menu for a particular node. Does not invoke the menu.
222
+
223
+ Processes submenus, references, radio_state as needed.
224
+ """
225
+ menu = wx.Menu()
226
+ submenus = {}
227
+ radio_check_not_needed = []
228
+ from meerk40t.core.treeop import get_tree_operation_for_node
229
+
230
+ tree_operations_for_node = get_tree_operation_for_node(elements)
231
+
232
+ def menu_functions(f, node):
233
+ func_dict = dict(f.func_dict)
234
+
235
+ def specific(event=None):
236
+ prompts = f.user_prompt
237
+ if len(prompts) > 0:
238
+ with wx.Dialog(
239
+ None,
240
+ wx.ID_ANY,
241
+ _("Parameters"),
242
+ style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER,
243
+ ) as dlg:
244
+ gui.context.themes.set_window_colors(dlg)
245
+
246
+ sizer = wx.BoxSizer(wx.VERTICAL)
247
+ fields = []
248
+ for prompt in prompts:
249
+ label = wxStaticText(dlg, wx.ID_ANY, prompt["prompt"])
250
+ sizer.Add(label, 0, wx.EXPAND, 0)
251
+ dtype = prompt["type"]
252
+ if dtype == bool:
253
+ control = wxCheckBox(dlg, wx.ID_ANY)
254
+ else:
255
+ control = TextCtrl(dlg, wx.ID_ANY)
256
+ control.SetMaxSize(dip_size(dlg, 75, -1))
257
+ fields.append(control)
258
+ sizer.Add(control, 0, wx.EXPAND, 0)
259
+ sizer.AddSpacer(23)
260
+ b_sizer = wx.BoxSizer(wx.HORIZONTAL)
261
+ button_OK = wxButton(dlg, wx.ID_OK, _("OK"))
262
+ button_CANCEL = wxButton(dlg, wx.ID_CANCEL, _("Cancel"))
263
+ # dlg.SetAffirmativeId(button_OK.GetId())
264
+ # dlg.SetEscapeId(button_CANCEL.GetId())
265
+ b_sizer.Add(button_OK, 0, wx.EXPAND, 0)
266
+ b_sizer.Add(button_CANCEL, 0, wx.EXPAND, 0)
267
+ sizer.Add(b_sizer, 0, wx.EXPAND, 0)
268
+ sizer.Fit(dlg)
269
+ dlg.SetSizer(sizer)
270
+ dlg.Layout()
271
+
272
+ response = dlg.ShowModal()
273
+ if response != wx.ID_OK:
274
+ return
275
+ for prompt, control in zip(prompts, fields):
276
+ dtype = prompt["type"]
277
+ try:
278
+ value = dtype(control.GetValue())
279
+ except ValueError:
280
+ return
281
+ func_dict[prompt["attr"]] = value
282
+ # for prompt in prompts:
283
+ # response = elements.kernel.prompt(prompt["type"], prompt["prompt"])
284
+ # if response is None:
285
+ # return
286
+ # func_dict[prompt["attr"]] = response
287
+ f(node, **func_dict)
288
+
289
+ return specific
290
+
291
+ # Check specifically for the optional first (use case: reference nodes)
292
+ if optional_2nd_node is not None:
293
+ mc1 = menu.MenuItemCount
294
+ last_was_separator = False
295
+
296
+ for func in tree_operations_for_node(optional_2nd_node):
297
+ submenu_name = func.submenu
298
+ submenu = None
299
+ if submenu_name and submenu_name in submenus:
300
+ submenu = submenus[submenu_name]
301
+ else:
302
+ if submenu_name:
303
+ last_was_separator = False
304
+ subs = submenu_name.split("|")
305
+ common = ""
306
+ parent_menu = menu
307
+ for sname in subs:
308
+ if sname == "":
309
+ continue
310
+ if common:
311
+ common += "|"
312
+ common += sname
313
+ if common in submenus:
314
+ submenu = submenus[common]
315
+ parent_menu = submenu
316
+ else:
317
+ submenu = wx.Menu()
318
+ if func.separate_before:
319
+ last_was_separator = True
320
+ parent_menu.AppendSeparator()
321
+ func.separate_before = False
322
+
323
+ parent_menu.AppendSubMenu(submenu, sname, func.help)
324
+ submenus[common] = submenu
325
+ parent_menu = submenu
326
+
327
+ menu_context = submenu if submenu is not None else menu
328
+ if func.separate_before:
329
+ menu_context.AppendSeparator()
330
+ if func.reference is not None:
331
+ menu_context.AppendSubMenu(
332
+ create_menu_for_node(
333
+ gui,
334
+ func.reference(optional_2nd_node),
335
+ elements,
336
+ optional_2nd_node,
337
+ ),
338
+ func.real_name,
339
+ )
340
+ continue
341
+ if func.radio_state is not None:
342
+ last_was_separator = False
343
+ item = menu_context.Append(
344
+ wx.ID_ANY, func.real_name, func.help, wx.ITEM_RADIO
345
+ )
346
+ check = func.radio_state
347
+ item.Check(check)
348
+ if check and menu_context not in radio_check_not_needed:
349
+ radio_check_not_needed.append(menu_context)
350
+ if func.enabled:
351
+ gui.Bind(
352
+ wx.EVT_MENU,
353
+ menu_functions(func, optional_2nd_node),
354
+ item,
355
+ )
356
+ else:
357
+ item.Enable(False)
358
+ else:
359
+ last_was_separator = False
360
+ if hasattr(func, "check_state") and func.check_state is not None:
361
+ check = func.check_state
362
+ kind = wx.ITEM_CHECK
363
+ else:
364
+ kind = wx.ITEM_NORMAL
365
+ check = None
366
+ item = menu_context.Append(wx.ID_ANY, func.real_name, func.help, kind)
367
+ if check is not None:
368
+ item.Check(check)
369
+ if func.enabled:
370
+ gui.Bind(
371
+ wx.EVT_MENU,
372
+ menu_functions(func, node),
373
+ item,
374
+ )
375
+ else:
376
+ item.Enable(False)
377
+ if menu_context not in radio_check_not_needed:
378
+ radio_check_not_needed.append(menu_context)
379
+ if not submenu and func.separate_after:
380
+ last_was_separator = True
381
+ menu.AppendSeparator()
382
+ mc2 = menu.MenuItemCount
383
+ if not last_was_separator and mc2 - mc1 > 0:
384
+ menu.AppendSeparator()
385
+
386
+ for func in tree_operations_for_node(node):
387
+ submenu_name = func.submenu
388
+ submenu = None
389
+ if submenu_name and submenu_name in submenus:
390
+ submenu = submenus[submenu_name]
391
+ else:
392
+ if submenu_name:
393
+ subs = submenu_name.split("|")
394
+ common = ""
395
+ parent_menu = menu
396
+ for sname in subs:
397
+ if sname == "":
398
+ continue
399
+ if common:
400
+ common += "|"
401
+ common += sname
402
+ if common in submenus:
403
+ submenu = submenus[common]
404
+ parent_menu = submenu
405
+ else:
406
+ submenu = wx.Menu()
407
+ if func.separate_before:
408
+ parent_menu.AppendSeparator()
409
+ func.separate_before = False
410
+ parent_menu.AppendSubMenu(submenu, sname, func.help)
411
+ submenus[common] = submenu
412
+ parent_menu = submenu
413
+
414
+ menu_context = submenu if submenu is not None else menu
415
+ if func.separate_before:
416
+ menu_context.AppendSeparator()
417
+ func.separate_before = False
418
+ if func.reference is not None:
419
+ menu_context.AppendSubMenu(
420
+ create_menu_for_node(gui, func.reference(node), elements),
421
+ func.real_name,
422
+ )
423
+ continue
424
+ if func.radio_state is not None:
425
+ item = menu_context.Append(
426
+ wx.ID_ANY, func.real_name, func.help, wx.ITEM_RADIO
427
+ )
428
+ check = func.radio_state
429
+ item.Check(check)
430
+ if check and menu_context not in radio_check_not_needed:
431
+ radio_check_not_needed.append(menu_context)
432
+ if func.enabled:
433
+ gui.Bind(
434
+ wx.EVT_MENU,
435
+ menu_functions(func, node),
436
+ item,
437
+ )
438
+ else:
439
+ item.Enable(False)
440
+ else:
441
+ if hasattr(func, "check_state") and func.check_state is not None:
442
+ check = func.check_state
443
+ kind = wx.ITEM_CHECK
444
+ else:
445
+ kind = wx.ITEM_NORMAL
446
+ check = None
447
+ item = menu_context.Append(wx.ID_ANY, func.real_name, func.help, kind)
448
+ if check is not None:
449
+ item.Check(check)
450
+ if func.enabled:
451
+ gui.Bind(
452
+ wx.EVT_MENU,
453
+ menu_functions(func, node),
454
+ item,
455
+ )
456
+ else:
457
+ item.Enable(False)
458
+
459
+ if menu_context not in radio_check_not_needed:
460
+ radio_check_not_needed.append(menu_context)
461
+ if not submenu and func.separate_after:
462
+ menu.AppendSeparator()
463
+ for submenu in submenus.values():
464
+ plain = True
465
+ for item in submenu.GetMenuItems():
466
+ if not (item.IsSeparator() or item.IsSubMenu() or not item.IsEnabled()):
467
+ plain = False
468
+ break
469
+ if plain and submenu not in radio_check_not_needed:
470
+ radio_check_not_needed.append(submenu)
471
+
472
+ if submenu not in radio_check_not_needed:
473
+ item = submenu.Append(
474
+ wx.ID_ANY,
475
+ _("Other value..."),
476
+ _("Value set using properties"),
477
+ wx.ITEM_RADIO,
478
+ )
479
+ item.Check(True)
480
+ return menu
481
+
482
+
483
+ def create_menu(gui, node, elements):
484
+ """
485
+ Create menu items. This is used for both the scene and the tree to create menu items.
486
+
487
+ @param gui: Gui used to create menu items.
488
+ @param node: The Node clicked on for the generated menu.
489
+ @param elements: elements service for use with node creation
490
+ @return:
491
+ """
492
+ if node is None:
493
+ return
494
+ # Is it a reference object?
495
+ optional_node = None
496
+ if hasattr(node, "node"):
497
+ optional_node = node
498
+ node = node.node
499
+
500
+ menu = create_menu_for_node(gui, node, elements, optional_node)
501
+ if menu.MenuItemCount != 0:
502
+ gui.PopupMenu(menu)
503
+ menu.Destroy()
504
+
505
+
506
+ ##############
507
+ # GUI CONTROL OVERRIDES
508
+ ##############
509
+ def set_color_according_to_theme(control, background, foreground):
510
+ win = control
511
+ while win is not None:
512
+ if hasattr(win, "context") and hasattr(win.context, "themes"):
513
+ if background:
514
+ col = win.context.themes.get(background)
515
+ if col:
516
+ control.SetBackgroundColour(col)
517
+ if foreground:
518
+ col = win.context.themes.get(foreground)
519
+ if col:
520
+ control.SetForegroundColour(col)
521
+ break
522
+ win = win.GetParent()
523
+
524
+
525
+ class TextCtrl(wx.TextCtrl):
526
+ """
527
+ Just to add some of the more common things we need, i.e. smaller default size...
528
+
529
+ Allow text boxes of specific types so that we can have consistent options for dealing with them.
530
+ """
531
+
532
+ def __init__(
533
+ self,
534
+ parent,
535
+ id=wx.ID_ANY,
536
+ value="",
537
+ pos=wx.DefaultPosition,
538
+ size=wx.DefaultSize,
539
+ style=0,
540
+ validator=wx.DefaultValidator,
541
+ name="",
542
+ check="",
543
+ limited=False,
544
+ nonzero=False,
545
+ ):
546
+ if value is None:
547
+ value = ""
548
+ super().__init__(
549
+ parent,
550
+ id=id,
551
+ value=value,
552
+ pos=pos,
553
+ size=size,
554
+ style=style,
555
+ validator=validator,
556
+ name=name,
557
+ )
558
+ self.parent = parent
559
+ self.extend_default_units_if_empty = True
560
+ self._check = check
561
+ self._style = style
562
+ self._nonzero = nonzero
563
+ if self._nonzero is None:
564
+ self._nonzero = False
565
+ # For the sake of readability we allow multiple occurrences of
566
+ # the same character in the string even if it's unnecessary...
567
+ floatstr = "+-.eE0123456789"
568
+ unitstr = "".join(ACCEPTED_UNITS)
569
+ anglestr = "".join(ACCEPTED_ANGLE_UNITS)
570
+ self.charpattern = ""
571
+ if self._check == "length":
572
+ self.charpattern = floatstr + unitstr
573
+ elif self._check == "percent":
574
+ self.charpattern = floatstr + r"%"
575
+ elif self._check == "float":
576
+ self.charpattern = floatstr
577
+ elif self._check == "angle":
578
+ self.charpattern = floatstr + anglestr
579
+ elif self._check == "int":
580
+ self.charpattern = r"-+0123456789"
581
+ self.lower_limit = None
582
+ self.upper_limit = None
583
+ self.lower_limit_err = None
584
+ self.upper_limit_err = None
585
+ self.lower_limit_warn = None
586
+ self.upper_limit_warn = None
587
+ self._default_color_background = self.GetBackgroundColour()
588
+ self._error_color_background = wx.RED
589
+ self._warn_color_background = wx.YELLOW
590
+ self._modify_color_background = self._default_color_background
591
+
592
+ self._default_color_foreground = self.GetForegroundColour()
593
+ self._error_color_foreground = self._default_color_foreground
594
+ self._warn_color_foreground = wx.BLACK
595
+ self._modify_color_foreground = self._default_color_foreground
596
+ self._warn_status = "modified"
597
+
598
+ self._last_valid_value = None
599
+ self._event_generated = None
600
+ self._action_routine = None
601
+ self._default_values = None
602
+
603
+ # You can set this to False, if you don't want logic to interfere with text input
604
+ self.execute_action_on_change = True
605
+
606
+ if self._check is not None and self._check != "":
607
+ self.Bind(wx.EVT_KEY_DOWN, self.on_char)
608
+ self.Bind(wx.EVT_KEY_UP, self.on_check)
609
+ self.Bind(wx.EVT_SET_FOCUS, self.on_enter_field)
610
+ self.Bind(wx.EVT_KILL_FOCUS, self.on_leave_field)
611
+ self.Bind(wx.EVT_RIGHT_DOWN, self.on_right_click)
612
+ if self._style & wx.TE_PROCESS_ENTER != 0:
613
+ self.Bind(wx.EVT_TEXT_ENTER, self.on_enter)
614
+ _MIN_WIDTH, _MAX_WIDTH = self.validate_widths()
615
+ self.SetMinSize(dip_size(self, _MIN_WIDTH, -1))
616
+ if limited:
617
+ self.SetMaxSize(dip_size(self, _MAX_WIDTH, -1))
618
+ set_color_according_to_theme(self, "text_bg", "text_fg")
619
+
620
+ def validate_widths(self):
621
+ minpattern = "0000"
622
+ maxpattern = "999999999.99mm"
623
+ if self._check == "length":
624
+ minpattern = "0000"
625
+ maxpattern = "999999999.99mm"
626
+ elif self._check == "percent":
627
+ minpattern = "0000"
628
+ maxpattern = "99.99%"
629
+ elif self._check == "float":
630
+ minpattern = "0000"
631
+ maxpattern = "99999.99"
632
+ elif self._check == "angle":
633
+ minpattern = "0000"
634
+ maxpattern = "9999.99deg"
635
+ elif self._check == "int":
636
+ minpattern = "0000"
637
+ maxpattern = "-999999"
638
+ # Let's be a bit more specific: what is the minimum size of the textcontrol fonts
639
+ # to hold these patterns
640
+ tfont = self.GetFont()
641
+ xsize = 15
642
+ imgBit = wx.Bitmap(xsize, xsize)
643
+ dc = wx.MemoryDC(imgBit)
644
+ dc.SelectObject(imgBit)
645
+ dc.SetFont(tfont)
646
+ f_width, f_height, f_descent, f_external_leading = dc.GetFullTextExtent(
647
+ minpattern
648
+ )
649
+ minw = f_width + 5
650
+ f_width, f_height, f_descent, f_external_leading = dc.GetFullTextExtent(
651
+ maxpattern
652
+ )
653
+ maxw = f_width + 10
654
+ # Now release dc
655
+ dc.SelectObject(wx.NullBitmap)
656
+ return minw, maxw
657
+
658
+ def SetActionRoutine(self, action_routine):
659
+ """
660
+ This routine will be called after a lost_focus / text_enter event,
661
+ it's a simple way of dealing with all the
662
+ ctrl.bind(wx.EVT_KILL_FOCUS / wx.EVT_TEXT_ENTER) things
663
+ Yes, you can still have them, but you should call
664
+ ctrl.prevalidate()
665
+ then to ensure the logic to avoid invalid content is been called.
666
+ If you need to programmatically distinguish between a lost focus
667
+ and text_enter event, then consult
668
+ ctrl.event_generated()
669
+ this will give back wx.EVT_KILL_FOCUS or wx.EVT_TEXT_ENTER
670
+ """
671
+ self._action_routine = action_routine
672
+
673
+ def event_generated(self):
674
+ """
675
+ This routine will give back wx.EVT_KILL_FOCUS or wx.EVT_TEXT_ENTER
676
+ if called during an execution of the validator routine, see above,
677
+ or None in any other case
678
+ """
679
+ return self._event_generated
680
+
681
+ def set_default_values(self, def_values):
682
+ self._default_values = def_values
683
+
684
+ def get_warn_status(self, txt):
685
+ status = ""
686
+ try:
687
+ value = None
688
+ if self._check == "float":
689
+ value = float(txt)
690
+ elif self._check == "percent":
691
+ if txt.endswith("%"):
692
+ value = float(txt[:-1]) / 100.0
693
+ else:
694
+ value = float(txt)
695
+ elif self._check == "int":
696
+ value = int(txt)
697
+ elif self._check == "empty":
698
+ if len(txt) == 0:
699
+ status = "error"
700
+ elif self._check == "length":
701
+ value = Length(txt)
702
+ elif self._check == "angle":
703
+ value = Angle(txt)
704
+ # we passed so far, thus the values are syntactically correct
705
+ # Now check for content compliance
706
+ if value is not None:
707
+ if self.lower_limit is not None and value < self.lower_limit:
708
+ value = self.lower_limit
709
+ self.SetValue(str(value))
710
+ status = "default"
711
+ if self.upper_limit is not None and value > self.upper_limit:
712
+ value = self.upper_limit
713
+ self.SetValue(str(value))
714
+ status = "default"
715
+ if self.lower_limit_warn is not None and value < self.lower_limit_warn:
716
+ status = "warning"
717
+ if self.upper_limit_warn is not None and value > self.upper_limit_warn:
718
+ status = "warning"
719
+ if self.lower_limit_err is not None and value < self.lower_limit_err:
720
+ status = "error"
721
+ if self.upper_limit_err is not None and value > self.upper_limit_err:
722
+ status = "error"
723
+ if self._nonzero and value == 0:
724
+ status = "error"
725
+ except ValueError:
726
+ status = "error"
727
+ return status
728
+
729
+ def SetValue(self, newvalue):
730
+ identical = False
731
+ current = super().GetValue()
732
+ if self._check == "float":
733
+ try:
734
+ v1 = float(current)
735
+ v2 = float(newvalue)
736
+ if v1 == v2:
737
+ identical = True
738
+ except ValueError:
739
+ pass
740
+ if identical:
741
+ # print (f"...ignored {current}={v1}, {newvalue}={v2}")
742
+ return
743
+ # print(f"SetValue called: {current} != {newvalue}")
744
+ self._last_valid_value = newvalue
745
+ status = self.get_warn_status(newvalue)
746
+ self.warn_status = status
747
+ cursor = self.GetInsertionPoint()
748
+ super().SetValue(newvalue)
749
+ cursor = min(len(newvalue), cursor)
750
+ self.SetInsertionPoint(cursor)
751
+
752
+ def set_error_level(self, err_min, err_max):
753
+ self.lower_limit_err = err_min
754
+ self.upper_limit_err = err_max
755
+
756
+ def set_warn_level(self, warn_min, warn_max):
757
+ self.lower_limit_warn = warn_min
758
+ self.upper_limit_warn = warn_max
759
+
760
+ def set_range(self, range_min, range_max):
761
+ self.lower_limit = range_min
762
+ self.upper_limit = range_max
763
+
764
+ def prevalidate(self, origin=None):
765
+ # Check whether the field is okay, if not then put it to the last value
766
+ txt = super().GetValue()
767
+ # print (f"prevalidate called from: {origin}, check={self._check}, content:{txt}")
768
+ if self.warn_status == "error" and self._last_valid_value is not None:
769
+ # ChangeValue is not creating any events...
770
+ self.ChangeValue(self._last_valid_value)
771
+ self.warn_status = ""
772
+ elif (
773
+ txt != "" and self._check == "length" and self.extend_default_units_if_empty
774
+ ):
775
+ # Do we have non-existing units provided? --> Change content
776
+ purenumber = True
777
+ unitstr = "".join(ACCEPTED_UNITS)
778
+ for c in unitstr:
779
+ if c in txt:
780
+ purenumber = False
781
+ break
782
+ if purenumber and hasattr(self.parent, "context"):
783
+ context = self.parent.context
784
+ root = context.root
785
+ root.setting(str, "units_name", "mm")
786
+ units = root.units_name
787
+ if units in ("inch", "inches"):
788
+ units = "in"
789
+ txt = txt.strip() + units
790
+ self.ChangeValue(txt)
791
+ elif (
792
+ txt != "" and self._check == "angle" and self.extend_default_units_if_empty
793
+ ):
794
+ # Do we have non-existing units provided? --> Change content
795
+ purenumber = True
796
+ unitstr = "".join(ACCEPTED_ANGLE_UNITS)
797
+ for c in unitstr:
798
+ if c in txt:
799
+ purenumber = False
800
+ break
801
+ if purenumber and hasattr(self.parent, "context"):
802
+ context = self.parent.context
803
+ root = context.root
804
+ root.setting(str, "angle_units", "deg")
805
+ units = root.angle_units
806
+ txt = txt.strip() + units
807
+ self.ChangeValue(txt)
808
+
809
+ def on_enter_field(self, event):
810
+ self._last_valid_value = super().GetValue()
811
+ event.Skip()
812
+
813
+ def on_leave_field(self, event):
814
+ # Needs to be passed on
815
+ event.Skip()
816
+ self.prevalidate("leave")
817
+ if self._action_routine is not None:
818
+ self._event_generated = wx.EVT_KILL_FOCUS
819
+ try:
820
+ self._action_routine()
821
+ finally:
822
+ self._event_generated = None
823
+ self.SelectNone()
824
+ # We assume it's been dealt with, so we recolor...
825
+ self.SetModified(False)
826
+ self.warn_status = self._warn_status
827
+
828
+ def on_enter(self, event):
829
+ # Let others deal with it after me
830
+ event.Skip()
831
+ self.prevalidate("enter")
832
+ if self._action_routine is not None:
833
+ self._event_generated = wx.EVT_TEXT_ENTER
834
+ try:
835
+ self._action_routine()
836
+ finally:
837
+ self._event_generated = None
838
+ self.SelectNone()
839
+ # We assume it's been dealt with, so we recolor...
840
+ self.SetModified(False)
841
+ self.warn_status = self._warn_status
842
+
843
+ def on_right_click(self, event):
844
+ def set_menu_value(to_be_set):
845
+ def handler(event):
846
+ self.SetValue(to_be_set)
847
+ self.prevalidate("enter")
848
+ if self._action_routine is not None:
849
+ self._event_generated = wx.EVT_TEXT_ENTER
850
+ try:
851
+ self._action_routine()
852
+ finally:
853
+ self._event_generated = None
854
+ return handler
855
+
856
+ if not self._default_values:
857
+ event.Skip()
858
+ return
859
+ menu = wx.Menu()
860
+ has_info = isinstance(self._default_values[0], (list, tuple))
861
+ item : wx.MenuItem = menu.Append(wx.ID_ANY, _("Default values..."), "")
862
+ item.Enable(False)
863
+ for info in self._default_values:
864
+ item = menu.Append(wx.ID_ANY, info[0] if has_info else info, info[1] if has_info else "")
865
+ self.Bind(wx.EVT_MENU, set_menu_value(info[0] if has_info else info), id=item.GetId())
866
+ self.PopupMenu(menu)
867
+ menu.Destroy()
868
+
869
+
870
+ @property
871
+ def warn_status(self):
872
+ return self._warn_status
873
+
874
+ @warn_status.setter
875
+ def warn_status(self, value):
876
+ self._warn_status = value
877
+ background = self._default_color_background
878
+ foreground = self._default_color_foreground
879
+ if value == "modified":
880
+ # Is it modified?
881
+ if self.IsModified():
882
+ background = self._modify_color_background
883
+ foreground = self._modify_color_foreground
884
+ elif value == "warning":
885
+ background = self._warn_color_background
886
+ foreground = self._warn_color_foreground
887
+ elif value == "error":
888
+ background = self._error_color_background
889
+ foreground = self._error_color_foreground
890
+ self.SetBackgroundColour(background)
891
+ self.SetForegroundColour(foreground)
892
+ self.Refresh()
893
+
894
+ def on_char(self, event):
895
+ proceed = True
896
+ # The French azerty keyboard generates numbers by pressing Shift + some key
897
+ # Under Linux this is not properly translated by GetUnicodeKey and
898
+ # is hence leading to a 'wrong' character being recognised (the original key).
899
+ # So we can't rely on a proper representation if the Shift-Key
900
+ # is held down, sigh.
901
+ if self.charpattern != "" and not event.ShiftDown():
902
+ keyc = event.GetUnicodeKey()
903
+ special = False
904
+ if event.RawControlDown() or event.ControlDown() or event.AltDown():
905
+ # GetUnicodeKey ignores all special keys, so we need to acknowledge that
906
+ special = True
907
+ if keyc == 127: # delete
908
+ special = True
909
+ if keyc != wx.WXK_NONE and not special:
910
+ # a 'real' character?
911
+ if keyc >= ord(" "):
912
+ char = chr(keyc).lower()
913
+ if char not in self.charpattern:
914
+ proceed = False
915
+ # print(f"Ignored: {keyc} - {char}")
916
+ if proceed:
917
+ event.DoAllowNextEvent()
918
+ event.Skip()
919
+
920
+ def on_check(self, event):
921
+ event.Skip()
922
+ txt = super().GetValue()
923
+ status = self.get_warn_status(txt)
924
+ if status == "":
925
+ status = "modified"
926
+ self.warn_status = status
927
+ # Is it a valid value?
928
+ lenokay = True
929
+ if len(txt) == 0 and self._check in (
930
+ "float",
931
+ "length",
932
+ "angle",
933
+ "int",
934
+ "percent",
935
+ ):
936
+ lenokay = False
937
+ if (
938
+ self.execute_action_on_change
939
+ and status == "modified"
940
+ and hasattr(self.parent, "context")
941
+ and lenokay
942
+ ):
943
+ if getattr(self.parent.context.root, "process_while_typing", False):
944
+ if self._action_routine is not None:
945
+ self._event_generated = wx.EVT_TEXT
946
+ try:
947
+ self._action_routine()
948
+ finally:
949
+ self._event_generated = None
950
+
951
+ @property
952
+ def is_changed(self):
953
+ return self.GetValue() != self._last_valid_value
954
+
955
+ @property
956
+ def Value(self):
957
+ return self.GetValue()
958
+
959
+ def GetValue(self):
960
+ result = super().GetValue()
961
+ if (
962
+ result != ""
963
+ and self._check == "length"
964
+ and self.extend_default_units_if_empty
965
+ ):
966
+ purenumber = True
967
+ unitstr = "".join(ACCEPTED_UNITS)
968
+ for c in unitstr:
969
+ if c in result:
970
+ purenumber = False
971
+ break
972
+ if purenumber and hasattr(self.parent, "context"):
973
+ context = self.parent.context
974
+ root = context.root
975
+ root.setting(str, "units_name", "mm")
976
+ units = root.units_name
977
+ if units in ("inch", "inches"):
978
+ units = "in"
979
+ result = result.strip()
980
+ if result.endswith("."):
981
+ result += "0"
982
+ result += units
983
+ return result
984
+
985
+
986
+ class wxCheckBox(wx.CheckBox):
987
+ """
988
+ This class wraps around wx.CheckBox and creates a series of mouse over tool tips to permit Linux tooltips that
989
+ otherwise do not show.
990
+ """
991
+
992
+ def __init__(
993
+ self,
994
+ *args,
995
+ **kwargs,
996
+ ):
997
+ self._tool_tip = None
998
+ super().__init__(*args, **kwargs)
999
+ if platform.system() == "Linux":
1000
+
1001
+ def on_mouse_over_check(ctrl):
1002
+ def mouse(event=None):
1003
+ ctrl.SetToolTip(self._tool_tip)
1004
+ event.Skip()
1005
+
1006
+ return mouse
1007
+
1008
+ self.Bind(wx.EVT_MOTION, on_mouse_over_check(super()))
1009
+ set_color_according_to_theme(self, "text_bg", "text_fg")
1010
+
1011
+ def SetToolTip(self, tooltip):
1012
+ self._tool_tip = tooltip
1013
+ super().SetToolTip(self._tool_tip)
1014
+
1015
+ class wxComboBox(wx.ComboBox):
1016
+ """
1017
+ This class wraps around wx.ComboBox and creates a series of mouse over tool tips to permit Linux tooltips that
1018
+ otherwise do not show.
1019
+ """
1020
+
1021
+ def __init__(
1022
+ self,
1023
+ *args,
1024
+ **kwargs,
1025
+ ):
1026
+ self._tool_tip = None
1027
+ super().__init__(*args, **kwargs)
1028
+ if platform.system() == "Linux":
1029
+
1030
+ def on_mouse_over_check(ctrl):
1031
+ def mouse(event=None):
1032
+ ctrl.SetToolTip(self._tool_tip)
1033
+ event.Skip()
1034
+
1035
+ return mouse
1036
+
1037
+ self.Bind(wx.EVT_MOTION, on_mouse_over_check(super()))
1038
+ set_color_according_to_theme(self, "text_bg", "text_fg")
1039
+
1040
+ def SetToolTip(self, tooltip):
1041
+ self._tool_tip = tooltip
1042
+ super().SetToolTip(self._tool_tip)
1043
+
1044
+
1045
+ class wxTreeCtrl(wx.TreeCtrl):
1046
+ """
1047
+ This class wraps around wx.TreeCtrl and creates a series of mouse over tool tips to permit Linux tooltips that
1048
+ otherwise do not show.
1049
+ """
1050
+
1051
+ def __init__(
1052
+ self,
1053
+ *args,
1054
+ **kwargs,
1055
+ ):
1056
+ self._tool_tip = None
1057
+ super().__init__(*args, **kwargs)
1058
+ if platform.system() == "Linux":
1059
+
1060
+ def on_mouse_over_check(ctrl):
1061
+ def mouse(event=None):
1062
+ ctrl.SetToolTip(self._tool_tip)
1063
+ event.Skip()
1064
+
1065
+ return mouse
1066
+
1067
+ self.Bind(wx.EVT_MOTION, on_mouse_over_check(super()))
1068
+ set_color_according_to_theme(self, "list_bg", "list_fg")
1069
+
1070
+ def SetToolTip(self, tooltip):
1071
+ self._tool_tip = tooltip
1072
+ super().SetToolTip(self._tool_tip)
1073
+
1074
+
1075
+ class wxBitmapButton(wx.BitmapButton):
1076
+ """
1077
+ This class wraps around wx.Button and creates a series of mouse over tool tips to permit Linux tooltips that
1078
+ otherwise do not show.
1079
+ """
1080
+
1081
+ def __init__(
1082
+ self,
1083
+ *args,
1084
+ **kwargs,
1085
+ ):
1086
+ self._tool_tip = None
1087
+ super().__init__(*args, **kwargs)
1088
+ if platform.system() == "Linux":
1089
+
1090
+ def on_mouse_over_check(ctrl):
1091
+ def mouse(event=None):
1092
+ ctrl.SetToolTip(self._tool_tip)
1093
+ event.Skip()
1094
+
1095
+ return mouse
1096
+
1097
+ self.Bind(wx.EVT_MOTION, on_mouse_over_check(super()))
1098
+ set_color_according_to_theme(self, "button_bg", "button_fg")
1099
+
1100
+ def SetToolTip(self, tooltip):
1101
+ self._tool_tip = tooltip
1102
+ super().SetToolTip(self._tool_tip)
1103
+
1104
+
1105
+ class wxButton(wx.Button):
1106
+ """
1107
+ This class wraps around wx.Button and creates a series of mouse over tool tips to permit Linux tooltips that
1108
+ otherwise do not show.
1109
+ """
1110
+
1111
+ def __init__(
1112
+ self,
1113
+ *args,
1114
+ **kwargs,
1115
+ ):
1116
+ self._tool_tip = None
1117
+ super().__init__(*args, **kwargs)
1118
+ if platform.system() == "Linux":
1119
+
1120
+ def on_mouse_over_check(ctrl):
1121
+ def mouse(event=None):
1122
+ ctrl.SetToolTip(self._tool_tip)
1123
+ event.Skip()
1124
+
1125
+ return mouse
1126
+
1127
+ self.Bind(wx.EVT_MOTION, on_mouse_over_check(super()))
1128
+ set_color_according_to_theme(self, "button_bg", "button_fg")
1129
+
1130
+ def SetToolTip(self, tooltip):
1131
+ self._tool_tip = tooltip
1132
+ super().SetToolTip(self._tool_tip)
1133
+
1134
+
1135
+ class wxToggleButton(wx.ToggleButton):
1136
+ """
1137
+ This class wraps around wx.ToggleButton and creates a series of mouse over tool tips to permit Linux tooltips that
1138
+ otherwise do not show.
1139
+ """
1140
+
1141
+ def __init__(
1142
+ self,
1143
+ *args,
1144
+ **kwargs,
1145
+ ):
1146
+ self._tool_tip = None
1147
+ super().__init__(*args, **kwargs)
1148
+ if platform.system() == "Linux":
1149
+
1150
+ def on_mouse_over_check(ctrl):
1151
+ def mouse(event=None):
1152
+ ctrl.SetToolTip(self._tool_tip)
1153
+ event.Skip()
1154
+
1155
+ return mouse
1156
+
1157
+ self.Bind(wx.EVT_MOTION, on_mouse_over_check(super()))
1158
+ set_color_according_to_theme(self, "button_bg", "button_fg")
1159
+ self.bitmap_toggled = None
1160
+ self.bitmap_untoggled = None
1161
+
1162
+ def update_button(self, value):
1163
+ # We just act as a man in the middle
1164
+ if value is None:
1165
+ value = self.GetValue()
1166
+ if value:
1167
+ if self.bitmap_toggled is not None:
1168
+ self.SetBitmap(self.bitmap_toggled)
1169
+ else:
1170
+ if self.bitmap_untoggled is not None:
1171
+ self.SetBitmap(self.bitmap_untoggled)
1172
+
1173
+ def SetValue(self, value):
1174
+ super().SetValue(value)
1175
+ self.update_button(value)
1176
+
1177
+ def SetToolTip(self, tooltip):
1178
+ self._tool_tip = tooltip
1179
+ super().SetToolTip(self._tool_tip)
1180
+
1181
+
1182
+ class wxStaticBitmap(wx.StaticBitmap):
1183
+ """
1184
+ This class wraps around wx.StaticBitmap and creates a series of mouse over tool tips to permit Linux tooltips that
1185
+ otherwise do not show.
1186
+ """
1187
+
1188
+ def __init__(
1189
+ self,
1190
+ *args,
1191
+ **kwargs,
1192
+ ):
1193
+ self._tool_tip = None
1194
+ super().__init__(*args, **kwargs)
1195
+ if platform.system() == "Linux":
1196
+
1197
+ def on_mouse_over_check(ctrl):
1198
+ def mouse(event=None):
1199
+ ctrl.SetToolTip(self._tool_tip)
1200
+ event.Skip()
1201
+
1202
+ return mouse
1203
+
1204
+ self.Bind(wx.EVT_MOTION, on_mouse_over_check(super()))
1205
+ set_color_according_to_theme(self, "button_bg", "button_fg")
1206
+
1207
+ def SetToolTip(self, tooltip):
1208
+ self._tool_tip = tooltip
1209
+ super().SetToolTip(self._tool_tip)
1210
+
1211
+
1212
+ class StaticBoxSizer(wx.StaticBoxSizer):
1213
+ def __init__(
1214
+ self,
1215
+ parent,
1216
+ id=wx.ID_ANY,
1217
+ label="",
1218
+ orientation=wx.HORIZONTAL,
1219
+ *args,
1220
+ **kwargs,
1221
+ ):
1222
+ if label is None:
1223
+ label = ""
1224
+ self.sbox = wx.StaticBox(parent, id, label=label)
1225
+ self.sbox.SetMinSize(dip_size(self.sbox, 50, 50))
1226
+ super().__init__(self.sbox, orientation)
1227
+ self.parent = parent
1228
+
1229
+ @property
1230
+ def Id(self):
1231
+ return self.sbox.Id
1232
+
1233
+ def GetId(self):
1234
+ return self.Id
1235
+
1236
+ def Show(self, show=True):
1237
+ self.sbox.Show(show)
1238
+
1239
+ def SetLabel(self, label):
1240
+ self.sbox.SetLabel(label)
1241
+
1242
+ def Refresh(self, *args):
1243
+ self.sbox.Refresh(*args)
1244
+
1245
+ def Enable(self, enable:bool=True):
1246
+ """Enable or disable the StaticBoxSizer and its children.
1247
+
1248
+ Enables or disables all children of the sizer recursively.
1249
+ """
1250
+ def enem(wind, flag):
1251
+ for c in wind.GetChildren():
1252
+ enem(c, flag)
1253
+ if hasattr(wind, "Enable"):
1254
+ wind.Enable(flag)
1255
+
1256
+ enem(self.sbox, enable)
1257
+
1258
+ class ScrolledPanel(SP):
1259
+ """
1260
+ We sometimes delete things fast enough that they call _SetupAfter when dead and crash.
1261
+ """
1262
+
1263
+ def _SetupAfter(self, scrollToTop):
1264
+ try:
1265
+ self.SetVirtualSize(self.GetBestVirtualSize())
1266
+ if scrollToTop:
1267
+ self.Scroll(0, 0)
1268
+ except RuntimeError:
1269
+ pass
1270
+
1271
+ class wxListCtrl(wx.ListCtrl):
1272
+ """
1273
+ wxListCtrl will extend a regular ListCtrl by saving / restoring column widths
1274
+ """
1275
+ def __init__(
1276
+ self, parent, ID=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, context=None, list_name=None
1277
+ ):
1278
+ wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
1279
+ self.context = context
1280
+ self.list_name = list_name
1281
+ # The resize event is never triggered, so tap into the parent...
1282
+ # parent.Bind(wx.EVT_SIZE, self.proxy_resize_event, self)
1283
+ parent.Bind(wx.EVT_SIZE, self.proxy_resize_event, parent)
1284
+ parent.Bind(wx.EVT_LIST_COL_END_DRAG, self.proxy_col_resized, self)
1285
+ set_color_according_to_theme(self, "list_bg", "list_fg")
1286
+
1287
+ def save_column_widths(self):
1288
+ if self.context is None or self.list_name is None:
1289
+ return
1290
+ try:
1291
+ sizes = list()
1292
+ for col in range(self.GetColumnCount()):
1293
+ sizes.append(self.GetColumnWidth(col))
1294
+ self.context.setting(tuple, self.list_name, None)
1295
+ setattr(self.context, self.list_name, sizes)
1296
+ except RuntimeError:
1297
+ # Could happen if the control is already destroyed
1298
+ return
1299
+
1300
+ def load_column_widths(self):
1301
+ if self.context is None or self.list_name is None:
1302
+ return
1303
+ sizes = self.context.setting(tuple, self.list_name, None)
1304
+ if sizes is None:
1305
+ return
1306
+ # print(f"Found for {self.list_name}: {sizes}")
1307
+ available = self.GetColumnCount()
1308
+ for idx, width in enumerate(sizes):
1309
+ if idx >= available:
1310
+ break
1311
+ self.SetColumnWidth(idx, width)
1312
+
1313
+ def resize_columns(self):
1314
+ self.load_column_widths()
1315
+ # we could at least try to make use of the available space
1316
+ dummy = self.adjust_last_column()
1317
+
1318
+ def proxy_col_resized(self, event):
1319
+ # We are not touching the event object to allow other routines to tap into it
1320
+ event.Skip()
1321
+ # print (f"col resized called from {self.GetId()} - {self.list_name}")
1322
+ dummy = self.adjust_last_column()
1323
+ self.save_column_widths()
1324
+
1325
+ def adjust_last_column(self, size_to_use=None):
1326
+ # gap is the amount of pixels to be reserved to allow for a vertical scrollbar
1327
+ gap = 30
1328
+ size = size_to_use
1329
+ if size is None:
1330
+ size = self.GetSize()
1331
+ list_width = size[0]
1332
+ total = gap
1333
+ last = 0
1334
+ for col in range(self.GetColumnCount()):
1335
+ try:
1336
+ last = self.GetColumnWidth(col)
1337
+ total += last
1338
+ except Exception as e:
1339
+ # print(f"Strange, crashed for column {col} of {self.GetColumnCount()}: {e}")
1340
+ return False
1341
+ # print(f"{self.list_name}, cols={self.GetColumnCount()}, available={list_width}, used={total}")
1342
+ if total < list_width:
1343
+ col = self.GetColumnCount() - 1
1344
+ if col < 0 :
1345
+ return False
1346
+ # print(f"Will adjust last column from {last} to {last + (list_width - total)}")
1347
+ try:
1348
+ self.SetColumnWidth(col, last + (list_width - total))
1349
+ except Exception as e:
1350
+ # print(f"Something strange happened while resizing the last columns for {self.list_name}: {e}")
1351
+ return False
1352
+ return True
1353
+ return False
1354
+
1355
+ def proxy_resize_event(self, event):
1356
+ # We are not touching the event object to allow other routines to tap into it
1357
+ event.Skip()
1358
+ # print (f"Resize called from {self.GetId()} - {self.list_name}: {event.Size}")
1359
+ if self.adjust_last_column(event.Size):
1360
+ self.save_column_widths()
1361
+
1362
+
1363
+ class EditableListCtrl(wxListCtrl, listmix.TextEditMixin):
1364
+ """TextEditMixin allows any column to be edited."""
1365
+
1366
+ # ----------------------------------------------------------------------
1367
+ def __init__(
1368
+ self, parent, ID=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, context=None, list_name=None,
1369
+ ):
1370
+ """Constructor"""
1371
+ wxListCtrl.__init__(self, parent=parent, ID=ID, pos=pos, size=size, style=style, context=context, list_name=list_name)
1372
+ listmix.TextEditMixin.__init__(self)
1373
+ set_color_according_to_theme(self, "list_bg", "list_fg")
1374
+
1375
+
1376
+ class HoverButton(wxButton):
1377
+ """
1378
+ Provide a button with Hover-Color changing ability.
1379
+ """
1380
+
1381
+ def __init__(self, parent, ID, label):
1382
+ super().__init__(parent, ID, label)
1383
+ self._focus_color = None
1384
+ self._disable_color = None
1385
+ self._foreground_color = self.GetForegroundColour()
1386
+ self._background_color = self.GetBackgroundColour()
1387
+ self.Bind(wx.EVT_ENTER_WINDOW, self.on_enter)
1388
+ self.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave)
1389
+ # self.Bind(wx.EVT_MOUSE_EVENTS, self.on_mouse)
1390
+ set_color_according_to_theme(self, "list_bg", "list_fg")
1391
+
1392
+ def SetFocusColour(self, color):
1393
+ self._focus_color = wx.Colour(color)
1394
+
1395
+ def SetDisabledBackgroundColour(self, color):
1396
+ self._disable_color = wx.Colour(color)
1397
+
1398
+ def SetForegroundColour(self, color):
1399
+ self._foreground_color = wx.Colour(color)
1400
+ super().SetForegroundColour(color)
1401
+
1402
+ def SetBackgroundColour(self, color):
1403
+ self._background_color = wx.Colour(color)
1404
+ super().SetBackgroundColour(color)
1405
+
1406
+ def GetFocusColour(self, color):
1407
+ return self._focus_color
1408
+
1409
+ def Enable(self, value):
1410
+ if value:
1411
+ super().SetBackgroundColour(self._background_color)
1412
+ else:
1413
+ if self._disable_color is None:
1414
+ r, g, b, a = self._background_color.Get()
1415
+ color = wx.Colour(
1416
+ min(255, int(1.5 * r)),
1417
+ min(255, int(1.5 * g)),
1418
+ min(255, int(1.5 * b)),
1419
+ )
1420
+ else:
1421
+ color = self._disable_color
1422
+ super().SetBackgroundColour(color)
1423
+ super().Enable(value)
1424
+ self.Refresh()
1425
+
1426
+ def on_enter(self, event):
1427
+ if self._focus_color is not None:
1428
+ super().SetForegroundColour(self._focus_color)
1429
+ self.Refresh()
1430
+ event.Skip()
1431
+
1432
+ def on_leave(self, event):
1433
+ super().SetForegroundColour(self._foreground_color)
1434
+ self.Refresh()
1435
+ event.Skip()
1436
+
1437
+ # def on_mouse(self, event):
1438
+ # if event.Leaving():
1439
+ # self.on_leave(event)
1440
+ # event.Skip()
1441
+
1442
+
1443
+ class wxRadioBox(StaticBoxSizer):
1444
+ """
1445
+ This class recreates the functionality of a wx.RadioBox, as this class does not recognize / honor parent color values, so a manual darkmode logic fails
1446
+ """
1447
+
1448
+ def __init__(
1449
+ self,
1450
+ parent=None,
1451
+ id=None,
1452
+ label=None,
1453
+ choices=None,
1454
+ majorDimension = 0,
1455
+ style=0,
1456
+ *args,
1457
+ **kwargs,
1458
+ ):
1459
+ self.parent = parent
1460
+ self.choices = choices
1461
+ self._children = []
1462
+ self._labels = []
1463
+ self._tool_tip = None
1464
+ self._help = None
1465
+ super().__init__(parent=parent, id=wx.ID_ANY, label=label, orientation=wx.VERTICAL)
1466
+ if majorDimension == 0 or style==wx.RA_SPECIFY_ROWS:
1467
+ majorDimension = 1000
1468
+ container = None
1469
+ for idx, c in enumerate(self.choices):
1470
+ if idx % majorDimension == 0:
1471
+ container = wx.BoxSizer(wx.HORIZONTAL)
1472
+ self.Add(container, 0, wx.EXPAND, 0)
1473
+ st = 0
1474
+ if idx == 0:
1475
+ st = wx.RB_GROUP
1476
+
1477
+ radio_option = wx.RadioButton(parent, wx.ID_ANY, label=c, style=st)
1478
+ container.Add(radio_option, 1, wx.ALIGN_CENTER_VERTICAL, 0)
1479
+ self._children.append(radio_option)
1480
+
1481
+ if platform.system() == "Linux":
1482
+
1483
+ def on_mouse_over_check(ctrl):
1484
+ def mouse(event=None):
1485
+ ctrl.SetToolTip(self._tool_tip)
1486
+ event.Skip()
1487
+
1488
+ return mouse
1489
+ for ctrl in self._children:
1490
+ ctrl.Bind(wx.EVT_MOTION, on_mouse_over_check(ctrl))
1491
+
1492
+ for ctrl in self._children:
1493
+ ctrl.Bind(wx.EVT_RADIOBUTTON, self.on_radio)
1494
+
1495
+ for ctrl in self._children + self._labels:
1496
+ set_color_according_to_theme(ctrl, "text_bg", "text_fg")
1497
+
1498
+ @property
1499
+ def Children(self):
1500
+ return self._children
1501
+
1502
+ def GetParent(self):
1503
+ return self.parent
1504
+
1505
+ def SetToolTip(self, tooltip):
1506
+ self._tool_tip = tooltip
1507
+ for ctrl in self._children:
1508
+ ctrl.SetToolTip(self._tool_tip)
1509
+
1510
+ def Select(self, n):
1511
+ self.SetSelection(n)
1512
+
1513
+ def SetSelection(self, n):
1514
+ for idx, ctrl in enumerate(self._children):
1515
+ ctrl.SetValue(idx == n)
1516
+
1517
+ def GetSelection(self):
1518
+ for idx, ctrl in enumerate(self._children):
1519
+ if ctrl.GetValue():
1520
+ return idx
1521
+ return -1
1522
+
1523
+ def GetStringSelection(self):
1524
+ idx = self.GetSelection()
1525
+ return None if idx < 0 else self.choices[idx]
1526
+
1527
+ def Disable(self):
1528
+ self.Enable(False)
1529
+
1530
+ def EnableItem(self, n, flag):
1531
+ if 0 <= n < len(self._children):
1532
+ self._children[n].Enable(flag)
1533
+
1534
+ def Enable(self, flag):
1535
+ for ctrl in self._children:
1536
+ ctrl.Enable(flag)
1537
+
1538
+ def Hide(self):
1539
+ self.Show(False)
1540
+
1541
+ def Show(self, flag):
1542
+ for ctrl in self._children + self._labels:
1543
+ ctrl.Show(flag)
1544
+
1545
+ def Bind(self, event_type, routine):
1546
+ self.parent.Bind(event_type, routine, self)
1547
+
1548
+ def on_radio(self, orgevent):
1549
+ #
1550
+ event = orgevent.Clone()
1551
+ event.SetEventType(wx.wxEVT_RADIOBOX)
1552
+ event.SetId(self.Id)
1553
+ event.SetEventObject(self)
1554
+ event.Int = self.GetSelection()
1555
+ wx.PostEvent(self.parent, event)
1556
+
1557
+ def SetForegroundColour(self, wc):
1558
+ for ctrl in self._children + self._labels:
1559
+ ctrl.SetForegroundColour(wc)
1560
+
1561
+ def SetBackgroundColour(self, wc):
1562
+ for ctrl in self._children + self._labels:
1563
+ ctrl.SetBackgroundColour(wc)
1564
+
1565
+ def SetHelpText(self, help):
1566
+ self._help = help
1567
+
1568
+ def GetHelpText(self):
1569
+ return self._help
1570
+ class wxStaticText(wx.StaticText):
1571
+ def __init__(self, *args, **kwargs):
1572
+ super().__init__(*args, **kwargs)
1573
+ set_color_according_to_theme(self, "label_bg", "label_fg")
1574
+
1575
+ class wxListBox(wx.ListBox):
1576
+ def __init__(self, *args, **kwargs):
1577
+ super().__init__(*args, **kwargs)
1578
+ set_color_according_to_theme(self, "list_bg", "list_fg")
1579
+
1580
+ ##############
1581
+ # GUI KEYSTROKE FUNCTIONS
1582
+ ##############
1583
+
1584
+ WX_METAKEYS = [
1585
+ wx.WXK_START,
1586
+ wx.WXK_WINDOWS_LEFT,
1587
+ wx.WXK_WINDOWS_RIGHT,
1588
+ ]
1589
+
1590
+ WX_MODIFIERS = {
1591
+ wx.WXK_CONTROL: "ctrl",
1592
+ wx.WXK_RAW_CONTROL: "macctl",
1593
+ wx.WXK_ALT: "alt",
1594
+ wx.WXK_SHIFT: "shift",
1595
+ wx.WXK_START: "start",
1596
+ wx.WXK_WINDOWS_LEFT: "win-left",
1597
+ wx.WXK_WINDOWS_RIGHT: "win-right",
1598
+ }
1599
+
1600
+ WX_SPECIALKEYS = {
1601
+ wx.WXK_F1: "f1",
1602
+ wx.WXK_F2: "f2",
1603
+ wx.WXK_F3: "f3",
1604
+ wx.WXK_F4: "f4",
1605
+ wx.WXK_F5: "f5",
1606
+ wx.WXK_F6: "f6",
1607
+ wx.WXK_F7: "f7",
1608
+ wx.WXK_F8: "f8",
1609
+ wx.WXK_F9: "f9",
1610
+ wx.WXK_F10: "f10",
1611
+ wx.WXK_F11: "f11",
1612
+ wx.WXK_F12: "f12",
1613
+ wx.WXK_F13: "f13",
1614
+ wx.WXK_F14: "f14",
1615
+ wx.WXK_F15: "f15",
1616
+ wx.WXK_F16: "f16",
1617
+ wx.WXK_F17: "f17",
1618
+ wx.WXK_F18: "f18",
1619
+ wx.WXK_F19: "f19",
1620
+ wx.WXK_F20: "f20",
1621
+ wx.WXK_F21: "f21",
1622
+ wx.WXK_F22: "f22",
1623
+ wx.WXK_F23: "f23",
1624
+ wx.WXK_F24: "f24",
1625
+ wx.WXK_ADD: "+",
1626
+ wx.WXK_END: "end",
1627
+ wx.WXK_NUMPAD0: "numpad0",
1628
+ wx.WXK_NUMPAD1: "numpad1",
1629
+ wx.WXK_NUMPAD2: "numpad2",
1630
+ wx.WXK_NUMPAD3: "numpad3",
1631
+ wx.WXK_NUMPAD4: "numpad4",
1632
+ wx.WXK_NUMPAD5: "numpad5",
1633
+ wx.WXK_NUMPAD6: "numpad6",
1634
+ wx.WXK_NUMPAD7: "numpad7",
1635
+ wx.WXK_NUMPAD8: "numpad8",
1636
+ wx.WXK_NUMPAD9: "numpad9",
1637
+ wx.WXK_NUMPAD_ADD: "numpad_add",
1638
+ wx.WXK_NUMPAD_SUBTRACT: "numpad_subtract",
1639
+ wx.WXK_NUMPAD_MULTIPLY: "numpad_multiply",
1640
+ wx.WXK_NUMPAD_DIVIDE: "numpad_divide",
1641
+ wx.WXK_NUMPAD_DECIMAL: "numpad.",
1642
+ wx.WXK_NUMPAD_ENTER: "numpad_enter",
1643
+ wx.WXK_NUMPAD_RIGHT: "numpad_right",
1644
+ wx.WXK_NUMPAD_LEFT: "numpad_left",
1645
+ wx.WXK_NUMPAD_UP: "numpad_up",
1646
+ wx.WXK_NUMPAD_DOWN: "numpad_down",
1647
+ wx.WXK_NUMPAD_DELETE: "numpad_delete",
1648
+ wx.WXK_NUMPAD_INSERT: "numpad_insert",
1649
+ wx.WXK_NUMPAD_PAGEUP: "numpad_pgup",
1650
+ wx.WXK_NUMPAD_PAGEDOWN: "numpad_pgdn",
1651
+ wx.WXK_NUMPAD_HOME: "numpad_home",
1652
+ wx.WXK_NUMPAD_END: "numpad_end",
1653
+ wx.WXK_NUMLOCK: "num_lock",
1654
+ wx.WXK_SCROLL: "scroll_lock",
1655
+ wx.WXK_CAPITAL: "caps_lock",
1656
+ wx.WXK_HOME: "home",
1657
+ wx.WXK_DOWN: "down",
1658
+ wx.WXK_UP: "up",
1659
+ wx.WXK_RIGHT: "right",
1660
+ wx.WXK_LEFT: "left",
1661
+ wx.WXK_ESCAPE: "escape",
1662
+ wx.WXK_BACK: "back",
1663
+ wx.WXK_PAUSE: "pause",
1664
+ wx.WXK_PAGEDOWN: "pagedown",
1665
+ wx.WXK_PAGEUP: "pageup",
1666
+ wx.WXK_PRINT: "print",
1667
+ wx.WXK_RETURN: "return",
1668
+ wx.WXK_SPACE: "space",
1669
+ wx.WXK_TAB: "tab",
1670
+ wx.WXK_DELETE: "delete",
1671
+ wx.WXK_INSERT: "insert",
1672
+ wx.WXK_SPECIAL1: "special1",
1673
+ wx.WXK_SPECIAL2: "special2",
1674
+ wx.WXK_SPECIAL3: "special3",
1675
+ wx.WXK_SPECIAL4: "special4",
1676
+ wx.WXK_SPECIAL5: "special5",
1677
+ wx.WXK_SPECIAL6: "special6",
1678
+ wx.WXK_SPECIAL7: "special7",
1679
+ wx.WXK_SPECIAL8: "special8",
1680
+ wx.WXK_SPECIAL9: "special9",
1681
+ wx.WXK_SPECIAL10: "special10",
1682
+ wx.WXK_SPECIAL11: "special11",
1683
+ wx.WXK_SPECIAL12: "special12",
1684
+ wx.WXK_SPECIAL13: "special13",
1685
+ wx.WXK_SPECIAL14: "special14",
1686
+ wx.WXK_SPECIAL15: "special15",
1687
+ wx.WXK_SPECIAL16: "special16",
1688
+ wx.WXK_SPECIAL17: "special17",
1689
+ wx.WXK_SPECIAL18: "special18",
1690
+ wx.WXK_SPECIAL19: "special19",
1691
+ wx.WXK_SPECIAL20: "special20",
1692
+ wx.WXK_CLEAR: "clear",
1693
+ wx.WXK_WINDOWS_MENU: "menu",
1694
+ }
1695
+
1696
+
1697
+ def is_navigation_key(keyvalue):
1698
+ if keyvalue is None:
1699
+ return False
1700
+ if "right" in keyvalue:
1701
+ return True
1702
+ if "left" in keyvalue:
1703
+ return True
1704
+ if "up" in keyvalue and "pgup" not in keyvalue and "pageup" not in keyvalue:
1705
+ return True
1706
+ if "down" in keyvalue and "pagedown" not in keyvalue:
1707
+ return True
1708
+ if "tab" in keyvalue:
1709
+ return True
1710
+ if "return" in keyvalue:
1711
+ return True
1712
+ return False
1713
+
1714
+
1715
+ def get_key_name(event, return_modifier=False):
1716
+ keyvalue = ""
1717
+ # https://wxpython.org/Phoenix/docs/html/wx.KeyEvent.html
1718
+ key = event.GetUnicodeKey()
1719
+ if key == wx.WXK_NONE:
1720
+ key = event.GetKeyCode()
1721
+ if event.RawControlDown() and not event.ControlDown():
1722
+ keyvalue += "macctl+" # Deliberately not macctrl+
1723
+ elif event.ControlDown():
1724
+ keyvalue += "ctrl+"
1725
+ if event.AltDown() or key == wx.WXK_ALT:
1726
+ keyvalue += "alt+"
1727
+ if event.ShiftDown():
1728
+ keyvalue += "shift+"
1729
+ if event.MetaDown() or key in WX_METAKEYS:
1730
+ keyvalue += "meta+"
1731
+ # if return_modifier and keyvalue: print("key", key, keyvalue)
1732
+ if key in WX_MODIFIERS:
1733
+ return keyvalue if return_modifier else None
1734
+ if key in WX_SPECIALKEYS:
1735
+ keyvalue += WX_SPECIALKEYS[key]
1736
+ else:
1737
+ keyvalue += chr(key)
1738
+ # print("key", key, keyvalue)
1739
+ return keyvalue.lower()
1740
+
1741
+
1742
+ def disable_window(window):
1743
+ for m in window.Children:
1744
+ if hasattr(m, "Disable"):
1745
+ m.Disable()
1746
+ if hasattr(m, "Children"):
1747
+ disable_window(m)
1748
+
1749
+
1750
+ def set_ctrl_value(ctrl, value):
1751
+ # Let's try to save the caret position
1752
+ try:
1753
+ cursor = ctrl.GetInsertionPoint()
1754
+ if ctrl.GetValue() != value:
1755
+ ctrl.SetValue(value)
1756
+ ctrl.SetInsertionPoint(min(len(value), cursor))
1757
+ except RuntimeError:
1758
+ # Control might already have been destroyed
1759
+ pass
1760
+
1761
+
1762
+ def dip_size(frame, x, y):
1763
+ # wx.Window.FromDIP was introduced with wxPython 4.1, so not all distros may have this
1764
+ wxsize = wx.Size(x, y)
1765
+ try:
1766
+ dipsize = frame.FromDIP(wxsize)
1767
+ return dipsize
1768
+ except AttributeError:
1769
+ return wxsize