meerk40t 0.9.3001__py2.py3-none-any.whl → 0.9.7010__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 (445) 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 +1195 -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 +1844 -1507
  50. meerk40t/core/elements/clipboard.py +229 -219
  51. meerk40t/core/elements/element_treeops.py +4561 -2837
  52. meerk40t/core/elements/element_types.py +125 -105
  53. meerk40t/core/elements/elements.py +4329 -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 +933 -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/trace.py +651 -563
  66. meerk40t/core/elements/tree_commands.py +415 -409
  67. meerk40t/core/elements/undo_redo.py +116 -58
  68. meerk40t/core/elements/wordlist.py +319 -200
  69. meerk40t/core/exceptions.py +9 -9
  70. meerk40t/core/laserjob.py +220 -220
  71. meerk40t/core/logging.py +63 -63
  72. meerk40t/core/node/blobnode.py +83 -86
  73. meerk40t/core/node/bootstrap.py +105 -103
  74. meerk40t/core/node/branch_elems.py +40 -31
  75. meerk40t/core/node/branch_ops.py +45 -38
  76. meerk40t/core/node/branch_regmark.py +48 -41
  77. meerk40t/core/node/cutnode.py +29 -32
  78. meerk40t/core/node/effect_hatch.py +375 -257
  79. meerk40t/core/node/effect_warp.py +398 -0
  80. meerk40t/core/node/effect_wobble.py +441 -309
  81. meerk40t/core/node/elem_ellipse.py +404 -309
  82. meerk40t/core/node/elem_image.py +1082 -801
  83. meerk40t/core/node/elem_line.py +358 -292
  84. meerk40t/core/node/elem_path.py +259 -201
  85. meerk40t/core/node/elem_point.py +129 -102
  86. meerk40t/core/node/elem_polyline.py +310 -246
  87. meerk40t/core/node/elem_rect.py +376 -286
  88. meerk40t/core/node/elem_text.py +445 -418
  89. meerk40t/core/node/filenode.py +59 -40
  90. meerk40t/core/node/groupnode.py +138 -74
  91. meerk40t/core/node/image_processed.py +777 -766
  92. meerk40t/core/node/image_raster.py +156 -113
  93. meerk40t/core/node/layernode.py +31 -31
  94. meerk40t/core/node/mixins.py +135 -107
  95. meerk40t/core/node/node.py +1427 -1304
  96. meerk40t/core/node/nutils.py +117 -114
  97. meerk40t/core/node/op_cut.py +462 -335
  98. meerk40t/core/node/op_dots.py +296 -251
  99. meerk40t/core/node/op_engrave.py +414 -311
  100. meerk40t/core/node/op_image.py +755 -369
  101. meerk40t/core/node/op_raster.py +787 -522
  102. meerk40t/core/node/place_current.py +37 -40
  103. meerk40t/core/node/place_point.py +329 -126
  104. meerk40t/core/node/refnode.py +58 -47
  105. meerk40t/core/node/rootnode.py +225 -219
  106. meerk40t/core/node/util_console.py +48 -48
  107. meerk40t/core/node/util_goto.py +84 -65
  108. meerk40t/core/node/util_home.py +61 -61
  109. meerk40t/core/node/util_input.py +102 -102
  110. meerk40t/core/node/util_output.py +102 -102
  111. meerk40t/core/node/util_wait.py +65 -65
  112. meerk40t/core/parameters.py +709 -707
  113. meerk40t/core/planner.py +875 -785
  114. meerk40t/core/plotplanner.py +656 -652
  115. meerk40t/core/space.py +120 -113
  116. meerk40t/core/spoolers.py +706 -705
  117. meerk40t/core/svg_io.py +1836 -1549
  118. meerk40t/core/treeop.py +534 -445
  119. meerk40t/core/undos.py +278 -124
  120. meerk40t/core/units.py +784 -680
  121. meerk40t/core/view.py +393 -322
  122. meerk40t/core/webhelp.py +62 -62
  123. meerk40t/core/wordlist.py +513 -504
  124. meerk40t/cylinder/cylinder.py +247 -0
  125. meerk40t/cylinder/gui/cylindersettings.py +41 -0
  126. meerk40t/cylinder/gui/gui.py +24 -0
  127. meerk40t/device/__init__.py +1 -1
  128. meerk40t/device/basedevice.py +322 -123
  129. meerk40t/device/devicechoices.py +50 -0
  130. meerk40t/device/dummydevice.py +163 -128
  131. meerk40t/device/gui/defaultactions.py +618 -602
  132. meerk40t/device/gui/effectspanel.py +114 -0
  133. meerk40t/device/gui/formatterpanel.py +253 -290
  134. meerk40t/device/gui/warningpanel.py +337 -260
  135. meerk40t/device/mixins.py +13 -13
  136. meerk40t/dxf/__init__.py +1 -1
  137. meerk40t/dxf/dxf_io.py +766 -554
  138. meerk40t/dxf/plugin.py +47 -35
  139. meerk40t/external_plugins.py +79 -79
  140. meerk40t/external_plugins_build.py +28 -28
  141. meerk40t/extra/cag.py +112 -116
  142. meerk40t/extra/coolant.py +403 -0
  143. meerk40t/extra/encode_detect.py +198 -0
  144. meerk40t/extra/ezd.py +1165 -1165
  145. meerk40t/extra/hershey.py +835 -340
  146. meerk40t/extra/imageactions.py +322 -316
  147. meerk40t/extra/inkscape.py +630 -622
  148. meerk40t/extra/lbrn.py +424 -424
  149. meerk40t/extra/outerworld.py +284 -0
  150. meerk40t/extra/param_functions.py +1542 -1556
  151. meerk40t/extra/potrace.py +257 -253
  152. meerk40t/extra/serial_exchange.py +118 -0
  153. meerk40t/extra/updater.py +602 -453
  154. meerk40t/extra/vectrace.py +147 -146
  155. meerk40t/extra/winsleep.py +83 -83
  156. meerk40t/extra/xcs_reader.py +597 -0
  157. meerk40t/fill/fills.py +781 -335
  158. meerk40t/fill/patternfill.py +1061 -1061
  159. meerk40t/fill/patterns.py +614 -567
  160. meerk40t/grbl/control.py +87 -87
  161. meerk40t/grbl/controller.py +990 -903
  162. meerk40t/grbl/device.py +1081 -768
  163. meerk40t/grbl/driver.py +989 -771
  164. meerk40t/grbl/emulator.py +532 -497
  165. meerk40t/grbl/gcodejob.py +783 -767
  166. meerk40t/grbl/gui/grblconfiguration.py +373 -298
  167. meerk40t/grbl/gui/grblcontroller.py +485 -271
  168. meerk40t/grbl/gui/grblhardwareconfig.py +269 -153
  169. meerk40t/grbl/gui/grbloperationconfig.py +105 -0
  170. meerk40t/grbl/gui/gui.py +147 -116
  171. meerk40t/grbl/interpreter.py +44 -44
  172. meerk40t/grbl/loader.py +22 -22
  173. meerk40t/grbl/mock_connection.py +56 -56
  174. meerk40t/grbl/plugin.py +294 -264
  175. meerk40t/grbl/serial_connection.py +93 -88
  176. meerk40t/grbl/tcp_connection.py +81 -79
  177. meerk40t/grbl/ws_connection.py +112 -0
  178. meerk40t/gui/__init__.py +1 -1
  179. meerk40t/gui/about.py +2042 -296
  180. meerk40t/gui/alignment.py +1644 -1608
  181. meerk40t/gui/autoexec.py +199 -0
  182. meerk40t/gui/basicops.py +791 -670
  183. meerk40t/gui/bufferview.py +77 -71
  184. meerk40t/gui/busy.py +170 -133
  185. meerk40t/gui/choicepropertypanel.py +1673 -1469
  186. meerk40t/gui/consolepanel.py +706 -542
  187. meerk40t/gui/devicepanel.py +687 -581
  188. meerk40t/gui/dialogoptions.py +110 -107
  189. meerk40t/gui/executejob.py +316 -306
  190. meerk40t/gui/fonts.py +90 -90
  191. meerk40t/gui/functionwrapper.py +252 -0
  192. meerk40t/gui/gui_mixins.py +729 -0
  193. meerk40t/gui/guicolors.py +205 -182
  194. meerk40t/gui/help_assets/help_assets.py +218 -201
  195. meerk40t/gui/helper.py +154 -0
  196. meerk40t/gui/hersheymanager.py +1430 -846
  197. meerk40t/gui/icons.py +3422 -2747
  198. meerk40t/gui/imagesplitter.py +555 -508
  199. meerk40t/gui/keymap.py +354 -344
  200. meerk40t/gui/laserpanel.py +892 -806
  201. meerk40t/gui/laserrender.py +1470 -1232
  202. meerk40t/gui/lasertoolpanel.py +805 -793
  203. meerk40t/gui/magnetoptions.py +436 -0
  204. meerk40t/gui/materialmanager.py +2917 -0
  205. meerk40t/gui/materialtest.py +1722 -1694
  206. meerk40t/gui/mkdebug.py +646 -359
  207. meerk40t/gui/mwindow.py +163 -140
  208. meerk40t/gui/navigationpanels.py +2605 -2467
  209. meerk40t/gui/notes.py +143 -142
  210. meerk40t/gui/opassignment.py +414 -410
  211. meerk40t/gui/operation_info.py +310 -299
  212. meerk40t/gui/plugin.py +494 -328
  213. meerk40t/gui/position.py +714 -669
  214. meerk40t/gui/preferences.py +901 -650
  215. meerk40t/gui/propertypanels/attributes.py +1461 -1131
  216. meerk40t/gui/propertypanels/blobproperty.py +117 -114
  217. meerk40t/gui/propertypanels/consoleproperty.py +83 -80
  218. meerk40t/gui/propertypanels/gotoproperty.py +77 -0
  219. meerk40t/gui/propertypanels/groupproperties.py +223 -217
  220. meerk40t/gui/propertypanels/hatchproperty.py +489 -469
  221. meerk40t/gui/propertypanels/imageproperty.py +2244 -1384
  222. meerk40t/gui/propertypanels/inputproperty.py +59 -58
  223. meerk40t/gui/propertypanels/opbranchproperties.py +82 -80
  224. meerk40t/gui/propertypanels/operationpropertymain.py +1890 -1638
  225. meerk40t/gui/propertypanels/outputproperty.py +59 -58
  226. meerk40t/gui/propertypanels/pathproperty.py +389 -380
  227. meerk40t/gui/propertypanels/placementproperty.py +1214 -383
  228. meerk40t/gui/propertypanels/pointproperty.py +140 -136
  229. meerk40t/gui/propertypanels/propertywindow.py +313 -181
  230. meerk40t/gui/propertypanels/rasterwizardpanels.py +996 -912
  231. meerk40t/gui/propertypanels/regbranchproperties.py +76 -0
  232. meerk40t/gui/propertypanels/textproperty.py +770 -755
  233. meerk40t/gui/propertypanels/waitproperty.py +56 -55
  234. meerk40t/gui/propertypanels/warpproperty.py +121 -0
  235. meerk40t/gui/propertypanels/wobbleproperty.py +255 -204
  236. meerk40t/gui/ribbon.py +2468 -2210
  237. meerk40t/gui/scene/scene.py +1100 -1051
  238. meerk40t/gui/scene/sceneconst.py +22 -22
  239. meerk40t/gui/scene/scenepanel.py +439 -349
  240. meerk40t/gui/scene/scenespacewidget.py +365 -365
  241. meerk40t/gui/scene/widget.py +518 -505
  242. meerk40t/gui/scenewidgets/affinemover.py +215 -215
  243. meerk40t/gui/scenewidgets/attractionwidget.py +315 -309
  244. meerk40t/gui/scenewidgets/bedwidget.py +120 -97
  245. meerk40t/gui/scenewidgets/elementswidget.py +137 -107
  246. meerk40t/gui/scenewidgets/gridwidget.py +785 -745
  247. meerk40t/gui/scenewidgets/guidewidget.py +765 -765
  248. meerk40t/gui/scenewidgets/laserpathwidget.py +66 -66
  249. meerk40t/gui/scenewidgets/machineoriginwidget.py +86 -86
  250. meerk40t/gui/scenewidgets/nodeselector.py +28 -28
  251. meerk40t/gui/scenewidgets/rectselectwidget.py +589 -346
  252. meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
  253. meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
  254. meerk40t/gui/scenewidgets/selectionwidget.py +2952 -2756
  255. meerk40t/gui/simpleui.py +357 -333
  256. meerk40t/gui/simulation.py +2431 -2094
  257. meerk40t/gui/snapoptions.py +208 -203
  258. meerk40t/gui/spoolerpanel.py +1227 -1180
  259. meerk40t/gui/statusbarwidgets/defaultoperations.py +480 -353
  260. meerk40t/gui/statusbarwidgets/infowidget.py +520 -483
  261. meerk40t/gui/statusbarwidgets/opassignwidget.py +356 -355
  262. meerk40t/gui/statusbarwidgets/selectionwidget.py +172 -171
  263. meerk40t/gui/statusbarwidgets/shapepropwidget.py +754 -236
  264. meerk40t/gui/statusbarwidgets/statusbar.py +272 -260
  265. meerk40t/gui/statusbarwidgets/statusbarwidget.py +268 -270
  266. meerk40t/gui/statusbarwidgets/strokewidget.py +267 -251
  267. meerk40t/gui/themes.py +200 -78
  268. meerk40t/gui/tips.py +591 -0
  269. meerk40t/gui/toolwidgets/circlebrush.py +35 -35
  270. meerk40t/gui/toolwidgets/toolcircle.py +248 -242
  271. meerk40t/gui/toolwidgets/toolcontainer.py +82 -77
  272. meerk40t/gui/toolwidgets/tooldraw.py +97 -90
  273. meerk40t/gui/toolwidgets/toolellipse.py +219 -212
  274. meerk40t/gui/toolwidgets/toolimagecut.py +25 -132
  275. meerk40t/gui/toolwidgets/toolline.py +39 -144
  276. meerk40t/gui/toolwidgets/toollinetext.py +79 -236
  277. meerk40t/gui/toolwidgets/toollinetext_inline.py +296 -0
  278. meerk40t/gui/toolwidgets/toolmeasure.py +160 -216
  279. meerk40t/gui/toolwidgets/toolnodeedit.py +2088 -2074
  280. meerk40t/gui/toolwidgets/toolnodemove.py +92 -94
  281. meerk40t/gui/toolwidgets/toolparameter.py +754 -668
  282. meerk40t/gui/toolwidgets/toolplacement.py +108 -108
  283. meerk40t/gui/toolwidgets/toolpoint.py +68 -59
  284. meerk40t/gui/toolwidgets/toolpointlistbuilder.py +294 -0
  285. meerk40t/gui/toolwidgets/toolpointmove.py +183 -0
  286. meerk40t/gui/toolwidgets/toolpolygon.py +288 -403
  287. meerk40t/gui/toolwidgets/toolpolyline.py +38 -196
  288. meerk40t/gui/toolwidgets/toolrect.py +211 -207
  289. meerk40t/gui/toolwidgets/toolrelocate.py +72 -72
  290. meerk40t/gui/toolwidgets/toolribbon.py +598 -113
  291. meerk40t/gui/toolwidgets/tooltabedit.py +546 -0
  292. meerk40t/gui/toolwidgets/tooltext.py +98 -89
  293. meerk40t/gui/toolwidgets/toolvector.py +213 -204
  294. meerk40t/gui/toolwidgets/toolwidget.py +39 -39
  295. meerk40t/gui/usbconnect.py +98 -91
  296. meerk40t/gui/utilitywidgets/buttonwidget.py +18 -18
  297. meerk40t/gui/utilitywidgets/checkboxwidget.py +90 -90
  298. meerk40t/gui/utilitywidgets/controlwidget.py +14 -14
  299. meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +343 -340
  300. meerk40t/gui/utilitywidgets/debugwidgets.py +148 -0
  301. meerk40t/gui/utilitywidgets/handlewidget.py +27 -27
  302. meerk40t/gui/utilitywidgets/harmonograph.py +450 -447
  303. meerk40t/gui/utilitywidgets/openclosewidget.py +40 -40
  304. meerk40t/gui/utilitywidgets/rotationwidget.py +54 -54
  305. meerk40t/gui/utilitywidgets/scalewidget.py +75 -75
  306. meerk40t/gui/utilitywidgets/seekbarwidget.py +183 -183
  307. meerk40t/gui/utilitywidgets/togglewidget.py +142 -142
  308. meerk40t/gui/utilitywidgets/toolbarwidget.py +8 -8
  309. meerk40t/gui/wordlisteditor.py +985 -931
  310. meerk40t/gui/wxmeerk40t.py +1444 -1169
  311. meerk40t/gui/wxmmain.py +5578 -4112
  312. meerk40t/gui/wxmribbon.py +1591 -1076
  313. meerk40t/gui/wxmscene.py +1635 -1453
  314. meerk40t/gui/wxmtree.py +2410 -2089
  315. meerk40t/gui/wxutils.py +1769 -1099
  316. meerk40t/gui/zmatrix.py +102 -102
  317. meerk40t/image/__init__.py +1 -1
  318. meerk40t/image/dither.py +429 -0
  319. meerk40t/image/imagetools.py +2778 -2269
  320. meerk40t/internal_plugins.py +150 -130
  321. meerk40t/kernel/__init__.py +63 -12
  322. meerk40t/kernel/channel.py +259 -212
  323. meerk40t/kernel/context.py +538 -538
  324. meerk40t/kernel/exceptions.py +41 -41
  325. meerk40t/kernel/functions.py +463 -414
  326. meerk40t/kernel/jobs.py +100 -100
  327. meerk40t/kernel/kernel.py +3809 -3571
  328. meerk40t/kernel/lifecycles.py +71 -71
  329. meerk40t/kernel/module.py +49 -49
  330. meerk40t/kernel/service.py +147 -147
  331. meerk40t/kernel/settings.py +383 -343
  332. meerk40t/lihuiyu/controller.py +883 -876
  333. meerk40t/lihuiyu/device.py +1181 -1069
  334. meerk40t/lihuiyu/driver.py +1466 -1372
  335. meerk40t/lihuiyu/gui/gui.py +127 -106
  336. meerk40t/lihuiyu/gui/lhyaccelgui.py +377 -363
  337. meerk40t/lihuiyu/gui/lhycontrollergui.py +741 -651
  338. meerk40t/lihuiyu/gui/lhydrivergui.py +470 -446
  339. meerk40t/lihuiyu/gui/lhyoperationproperties.py +238 -237
  340. meerk40t/lihuiyu/gui/tcpcontroller.py +226 -190
  341. meerk40t/lihuiyu/interpreter.py +53 -53
  342. meerk40t/lihuiyu/laserspeed.py +450 -450
  343. meerk40t/lihuiyu/loader.py +90 -90
  344. meerk40t/lihuiyu/parser.py +404 -404
  345. meerk40t/lihuiyu/plugin.py +101 -102
  346. meerk40t/lihuiyu/tcp_connection.py +111 -109
  347. meerk40t/main.py +231 -165
  348. meerk40t/moshi/builder.py +788 -781
  349. meerk40t/moshi/controller.py +505 -499
  350. meerk40t/moshi/device.py +495 -442
  351. meerk40t/moshi/driver.py +862 -696
  352. meerk40t/moshi/gui/gui.py +78 -76
  353. meerk40t/moshi/gui/moshicontrollergui.py +538 -522
  354. meerk40t/moshi/gui/moshidrivergui.py +87 -75
  355. meerk40t/moshi/plugin.py +43 -43
  356. meerk40t/network/console_server.py +102 -57
  357. meerk40t/network/kernelserver.py +10 -9
  358. meerk40t/network/tcp_server.py +142 -140
  359. meerk40t/network/udp_server.py +103 -77
  360. meerk40t/network/web_server.py +390 -0
  361. meerk40t/newly/controller.py +1158 -1144
  362. meerk40t/newly/device.py +874 -732
  363. meerk40t/newly/driver.py +540 -412
  364. meerk40t/newly/gui/gui.py +219 -188
  365. meerk40t/newly/gui/newlyconfig.py +116 -101
  366. meerk40t/newly/gui/newlycontroller.py +193 -186
  367. meerk40t/newly/gui/operationproperties.py +51 -51
  368. meerk40t/newly/mock_connection.py +82 -82
  369. meerk40t/newly/newly_params.py +56 -56
  370. meerk40t/newly/plugin.py +1214 -1246
  371. meerk40t/newly/usb_connection.py +322 -322
  372. meerk40t/rotary/gui/gui.py +52 -46
  373. meerk40t/rotary/gui/rotarysettings.py +240 -232
  374. meerk40t/rotary/rotary.py +202 -98
  375. meerk40t/ruida/control.py +291 -91
  376. meerk40t/ruida/controller.py +138 -1088
  377. meerk40t/ruida/device.py +672 -231
  378. meerk40t/ruida/driver.py +534 -472
  379. meerk40t/ruida/emulator.py +1494 -1491
  380. meerk40t/ruida/exceptions.py +4 -4
  381. meerk40t/ruida/gui/gui.py +71 -76
  382. meerk40t/ruida/gui/ruidaconfig.py +239 -72
  383. meerk40t/ruida/gui/ruidacontroller.py +187 -184
  384. meerk40t/ruida/gui/ruidaoperationproperties.py +48 -47
  385. meerk40t/ruida/loader.py +54 -52
  386. meerk40t/ruida/mock_connection.py +57 -109
  387. meerk40t/ruida/plugin.py +124 -87
  388. meerk40t/ruida/rdjob.py +2084 -945
  389. meerk40t/ruida/serial_connection.py +116 -0
  390. meerk40t/ruida/tcp_connection.py +146 -0
  391. meerk40t/ruida/udp_connection.py +73 -0
  392. meerk40t/svgelements.py +9671 -9669
  393. meerk40t/tools/driver_to_path.py +584 -579
  394. meerk40t/tools/geomstr.py +5583 -4680
  395. meerk40t/tools/jhfparser.py +357 -292
  396. meerk40t/tools/kerftest.py +904 -890
  397. meerk40t/tools/livinghinges.py +1168 -1033
  398. meerk40t/tools/pathtools.py +987 -949
  399. meerk40t/tools/pmatrix.py +234 -0
  400. meerk40t/tools/pointfinder.py +942 -942
  401. meerk40t/tools/polybool.py +940 -940
  402. meerk40t/tools/rasterplotter.py +1660 -547
  403. meerk40t/tools/shxparser.py +989 -901
  404. meerk40t/tools/ttfparser.py +726 -446
  405. meerk40t/tools/zinglplotter.py +595 -593
  406. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/LICENSE +21 -21
  407. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/METADATA +150 -139
  408. meerk40t-0.9.7010.dist-info/RECORD +445 -0
  409. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/WHEEL +1 -1
  410. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/top_level.txt +0 -1
  411. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/zip-safe +1 -1
  412. meerk40t/balormk/elementlightjob.py +0 -159
  413. meerk40t-0.9.3001.dist-info/RECORD +0 -437
  414. test/bootstrap.py +0 -63
  415. test/test_cli.py +0 -12
  416. test/test_core_cutcode.py +0 -418
  417. test/test_core_elements.py +0 -144
  418. test/test_core_plotplanner.py +0 -397
  419. test/test_core_viewports.py +0 -312
  420. test/test_drivers_grbl.py +0 -108
  421. test/test_drivers_lihuiyu.py +0 -443
  422. test/test_drivers_newly.py +0 -113
  423. test/test_element_degenerate_points.py +0 -43
  424. test/test_elements_classify.py +0 -97
  425. test/test_elements_penbox.py +0 -22
  426. test/test_file_svg.py +0 -176
  427. test/test_fill.py +0 -155
  428. test/test_geomstr.py +0 -1523
  429. test/test_geomstr_nodes.py +0 -18
  430. test/test_imagetools_actualize.py +0 -306
  431. test/test_imagetools_wizard.py +0 -258
  432. test/test_kernel.py +0 -200
  433. test/test_laser_speeds.py +0 -3303
  434. test/test_length.py +0 -57
  435. test/test_lifecycle.py +0 -66
  436. test/test_operations.py +0 -251
  437. test/test_operations_hatch.py +0 -57
  438. test/test_ruida.py +0 -19
  439. test/test_spooler.py +0 -22
  440. test/test_tools_rasterplotter.py +0 -29
  441. test/test_wobble.py +0 -133
  442. test/test_zingl.py +0 -124
  443. {test → meerk40t/cylinder}/__init__.py +0 -0
  444. /meerk40t/{core/element_commands.py → cylinder/gui/__init__.py} +0 -0
  445. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.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, 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