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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. meerk40t/__init__.py +1 -1
  2. meerk40t/balormk/balor_params.py +167 -167
  3. meerk40t/balormk/clone_loader.py +457 -457
  4. meerk40t/balormk/controller.py +1566 -1512
  5. meerk40t/balormk/cylindermod.py +64 -0
  6. meerk40t/balormk/device.py +966 -1959
  7. meerk40t/balormk/driver.py +778 -591
  8. meerk40t/balormk/galvo_commands.py +1194 -0
  9. meerk40t/balormk/gui/balorconfig.py +237 -111
  10. meerk40t/balormk/gui/balorcontroller.py +191 -184
  11. meerk40t/balormk/gui/baloroperationproperties.py +116 -115
  12. meerk40t/balormk/gui/corscene.py +845 -0
  13. meerk40t/balormk/gui/gui.py +179 -147
  14. meerk40t/balormk/livelightjob.py +466 -382
  15. meerk40t/balormk/mock_connection.py +131 -109
  16. meerk40t/balormk/plugin.py +133 -135
  17. meerk40t/balormk/usb_connection.py +306 -301
  18. meerk40t/camera/__init__.py +1 -1
  19. meerk40t/camera/camera.py +514 -397
  20. meerk40t/camera/gui/camerapanel.py +1241 -1095
  21. meerk40t/camera/gui/gui.py +58 -58
  22. meerk40t/camera/plugin.py +441 -399
  23. meerk40t/ch341/__init__.py +27 -27
  24. meerk40t/ch341/ch341device.py +628 -628
  25. meerk40t/ch341/libusb.py +595 -589
  26. meerk40t/ch341/mock.py +171 -171
  27. meerk40t/ch341/windriver.py +157 -157
  28. meerk40t/constants.py +13 -0
  29. meerk40t/core/__init__.py +1 -1
  30. meerk40t/core/bindalias.py +550 -539
  31. meerk40t/core/core.py +47 -47
  32. meerk40t/core/cutcode/cubiccut.py +73 -73
  33. meerk40t/core/cutcode/cutcode.py +315 -312
  34. meerk40t/core/cutcode/cutgroup.py +141 -137
  35. meerk40t/core/cutcode/cutobject.py +192 -185
  36. meerk40t/core/cutcode/dwellcut.py +37 -37
  37. meerk40t/core/cutcode/gotocut.py +29 -29
  38. meerk40t/core/cutcode/homecut.py +29 -29
  39. meerk40t/core/cutcode/inputcut.py +34 -34
  40. meerk40t/core/cutcode/linecut.py +33 -33
  41. meerk40t/core/cutcode/outputcut.py +34 -34
  42. meerk40t/core/cutcode/plotcut.py +335 -335
  43. meerk40t/core/cutcode/quadcut.py +61 -61
  44. meerk40t/core/cutcode/rastercut.py +168 -148
  45. meerk40t/core/cutcode/waitcut.py +34 -34
  46. meerk40t/core/cutplan.py +1843 -1316
  47. meerk40t/core/drivers.py +330 -329
  48. meerk40t/core/elements/align.py +801 -669
  49. meerk40t/core/elements/branches.py +1858 -1507
  50. meerk40t/core/elements/clipboard.py +229 -219
  51. meerk40t/core/elements/element_treeops.py +4595 -2837
  52. meerk40t/core/elements/element_types.py +125 -105
  53. meerk40t/core/elements/elements.py +4315 -3617
  54. meerk40t/core/elements/files.py +117 -64
  55. meerk40t/core/elements/geometry.py +473 -224
  56. meerk40t/core/elements/grid.py +467 -316
  57. meerk40t/core/elements/materials.py +158 -94
  58. meerk40t/core/elements/notes.py +50 -38
  59. meerk40t/core/elements/offset_clpr.py +934 -912
  60. meerk40t/core/elements/offset_mk.py +963 -955
  61. meerk40t/core/elements/penbox.py +339 -267
  62. meerk40t/core/elements/placements.py +300 -83
  63. meerk40t/core/elements/render.py +785 -687
  64. meerk40t/core/elements/shapes.py +2618 -2092
  65. meerk40t/core/elements/testcases.py +105 -0
  66. meerk40t/core/elements/trace.py +651 -563
  67. meerk40t/core/elements/tree_commands.py +415 -409
  68. meerk40t/core/elements/undo_redo.py +116 -58
  69. meerk40t/core/elements/wordlist.py +319 -200
  70. meerk40t/core/exceptions.py +9 -9
  71. meerk40t/core/laserjob.py +220 -220
  72. meerk40t/core/logging.py +63 -63
  73. meerk40t/core/node/blobnode.py +83 -86
  74. meerk40t/core/node/bootstrap.py +105 -103
  75. meerk40t/core/node/branch_elems.py +40 -31
  76. meerk40t/core/node/branch_ops.py +45 -38
  77. meerk40t/core/node/branch_regmark.py +48 -41
  78. meerk40t/core/node/cutnode.py +29 -32
  79. meerk40t/core/node/effect_hatch.py +375 -257
  80. meerk40t/core/node/effect_warp.py +398 -0
  81. meerk40t/core/node/effect_wobble.py +441 -309
  82. meerk40t/core/node/elem_ellipse.py +404 -309
  83. meerk40t/core/node/elem_image.py +1082 -801
  84. meerk40t/core/node/elem_line.py +358 -292
  85. meerk40t/core/node/elem_path.py +259 -201
  86. meerk40t/core/node/elem_point.py +129 -102
  87. meerk40t/core/node/elem_polyline.py +310 -246
  88. meerk40t/core/node/elem_rect.py +376 -286
  89. meerk40t/core/node/elem_text.py +445 -418
  90. meerk40t/core/node/filenode.py +59 -40
  91. meerk40t/core/node/groupnode.py +138 -74
  92. meerk40t/core/node/image_processed.py +777 -766
  93. meerk40t/core/node/image_raster.py +156 -113
  94. meerk40t/core/node/layernode.py +31 -31
  95. meerk40t/core/node/mixins.py +135 -107
  96. meerk40t/core/node/node.py +1427 -1304
  97. meerk40t/core/node/nutils.py +117 -114
  98. meerk40t/core/node/op_cut.py +463 -335
  99. meerk40t/core/node/op_dots.py +296 -251
  100. meerk40t/core/node/op_engrave.py +414 -311
  101. meerk40t/core/node/op_image.py +755 -369
  102. meerk40t/core/node/op_raster.py +787 -522
  103. meerk40t/core/node/place_current.py +37 -40
  104. meerk40t/core/node/place_point.py +329 -126
  105. meerk40t/core/node/refnode.py +58 -47
  106. meerk40t/core/node/rootnode.py +225 -219
  107. meerk40t/core/node/util_console.py +48 -48
  108. meerk40t/core/node/util_goto.py +84 -65
  109. meerk40t/core/node/util_home.py +61 -61
  110. meerk40t/core/node/util_input.py +102 -102
  111. meerk40t/core/node/util_output.py +102 -102
  112. meerk40t/core/node/util_wait.py +65 -65
  113. meerk40t/core/parameters.py +709 -707
  114. meerk40t/core/planner.py +875 -785
  115. meerk40t/core/plotplanner.py +656 -652
  116. meerk40t/core/space.py +120 -113
  117. meerk40t/core/spoolers.py +706 -705
  118. meerk40t/core/svg_io.py +1836 -1549
  119. meerk40t/core/treeop.py +534 -445
  120. meerk40t/core/undos.py +278 -124
  121. meerk40t/core/units.py +784 -680
  122. meerk40t/core/view.py +393 -322
  123. meerk40t/core/webhelp.py +62 -62
  124. meerk40t/core/wordlist.py +513 -504
  125. meerk40t/cylinder/cylinder.py +247 -0
  126. meerk40t/cylinder/gui/cylindersettings.py +41 -0
  127. meerk40t/cylinder/gui/gui.py +24 -0
  128. meerk40t/device/__init__.py +1 -1
  129. meerk40t/device/basedevice.py +322 -123
  130. meerk40t/device/devicechoices.py +50 -0
  131. meerk40t/device/dummydevice.py +163 -128
  132. meerk40t/device/gui/defaultactions.py +618 -602
  133. meerk40t/device/gui/effectspanel.py +114 -0
  134. meerk40t/device/gui/formatterpanel.py +253 -290
  135. meerk40t/device/gui/warningpanel.py +337 -260
  136. meerk40t/device/mixins.py +13 -13
  137. meerk40t/dxf/__init__.py +1 -1
  138. meerk40t/dxf/dxf_io.py +766 -554
  139. meerk40t/dxf/plugin.py +47 -35
  140. meerk40t/external_plugins.py +79 -79
  141. meerk40t/external_plugins_build.py +28 -28
  142. meerk40t/extra/cag.py +112 -116
  143. meerk40t/extra/coolant.py +403 -0
  144. meerk40t/extra/encode_detect.py +204 -0
  145. meerk40t/extra/ezd.py +1165 -1165
  146. meerk40t/extra/hershey.py +834 -340
  147. meerk40t/extra/imageactions.py +322 -316
  148. meerk40t/extra/inkscape.py +628 -622
  149. meerk40t/extra/lbrn.py +424 -424
  150. meerk40t/extra/outerworld.py +283 -0
  151. meerk40t/extra/param_functions.py +1542 -1556
  152. meerk40t/extra/potrace.py +257 -253
  153. meerk40t/extra/serial_exchange.py +118 -0
  154. meerk40t/extra/updater.py +602 -453
  155. meerk40t/extra/vectrace.py +147 -146
  156. meerk40t/extra/winsleep.py +83 -83
  157. meerk40t/extra/xcs_reader.py +597 -0
  158. meerk40t/fill/fills.py +781 -335
  159. meerk40t/fill/patternfill.py +1061 -1061
  160. meerk40t/fill/patterns.py +614 -567
  161. meerk40t/grbl/control.py +87 -87
  162. meerk40t/grbl/controller.py +990 -903
  163. meerk40t/grbl/device.py +1084 -768
  164. meerk40t/grbl/driver.py +989 -771
  165. meerk40t/grbl/emulator.py +532 -497
  166. meerk40t/grbl/gcodejob.py +783 -767
  167. meerk40t/grbl/gui/grblconfiguration.py +373 -298
  168. meerk40t/grbl/gui/grblcontroller.py +485 -271
  169. meerk40t/grbl/gui/grblhardwareconfig.py +269 -153
  170. meerk40t/grbl/gui/grbloperationconfig.py +105 -0
  171. meerk40t/grbl/gui/gui.py +147 -116
  172. meerk40t/grbl/interpreter.py +44 -44
  173. meerk40t/grbl/loader.py +22 -22
  174. meerk40t/grbl/mock_connection.py +56 -56
  175. meerk40t/grbl/plugin.py +294 -264
  176. meerk40t/grbl/serial_connection.py +93 -88
  177. meerk40t/grbl/tcp_connection.py +81 -79
  178. meerk40t/grbl/ws_connection.py +112 -0
  179. meerk40t/gui/__init__.py +1 -1
  180. meerk40t/gui/about.py +2042 -296
  181. meerk40t/gui/alignment.py +1644 -1608
  182. meerk40t/gui/autoexec.py +199 -0
  183. meerk40t/gui/basicops.py +791 -670
  184. meerk40t/gui/bufferview.py +77 -71
  185. meerk40t/gui/busy.py +232 -133
  186. meerk40t/gui/choicepropertypanel.py +1662 -1469
  187. meerk40t/gui/consolepanel.py +706 -542
  188. meerk40t/gui/devicepanel.py +687 -581
  189. meerk40t/gui/dialogoptions.py +110 -107
  190. meerk40t/gui/executejob.py +316 -306
  191. meerk40t/gui/fonts.py +90 -90
  192. meerk40t/gui/functionwrapper.py +252 -0
  193. meerk40t/gui/gui_mixins.py +729 -0
  194. meerk40t/gui/guicolors.py +205 -182
  195. meerk40t/gui/help_assets/help_assets.py +218 -201
  196. meerk40t/gui/helper.py +154 -0
  197. meerk40t/gui/hersheymanager.py +1440 -846
  198. meerk40t/gui/icons.py +3422 -2747
  199. meerk40t/gui/imagesplitter.py +555 -508
  200. meerk40t/gui/keymap.py +354 -344
  201. meerk40t/gui/laserpanel.py +897 -806
  202. meerk40t/gui/laserrender.py +1470 -1232
  203. meerk40t/gui/lasertoolpanel.py +805 -793
  204. meerk40t/gui/magnetoptions.py +436 -0
  205. meerk40t/gui/materialmanager.py +2944 -0
  206. meerk40t/gui/materialtest.py +1722 -1694
  207. meerk40t/gui/mkdebug.py +646 -359
  208. meerk40t/gui/mwindow.py +163 -140
  209. meerk40t/gui/navigationpanels.py +2605 -2467
  210. meerk40t/gui/notes.py +143 -142
  211. meerk40t/gui/opassignment.py +414 -410
  212. meerk40t/gui/operation_info.py +310 -299
  213. meerk40t/gui/plugin.py +500 -328
  214. meerk40t/gui/position.py +714 -669
  215. meerk40t/gui/preferences.py +901 -650
  216. meerk40t/gui/propertypanels/attributes.py +1461 -1131
  217. meerk40t/gui/propertypanels/blobproperty.py +117 -114
  218. meerk40t/gui/propertypanels/consoleproperty.py +83 -80
  219. meerk40t/gui/propertypanels/gotoproperty.py +77 -0
  220. meerk40t/gui/propertypanels/groupproperties.py +223 -217
  221. meerk40t/gui/propertypanels/hatchproperty.py +489 -469
  222. meerk40t/gui/propertypanels/imageproperty.py +2244 -1384
  223. meerk40t/gui/propertypanels/inputproperty.py +59 -58
  224. meerk40t/gui/propertypanels/opbranchproperties.py +82 -80
  225. meerk40t/gui/propertypanels/operationpropertymain.py +1890 -1638
  226. meerk40t/gui/propertypanels/outputproperty.py +59 -58
  227. meerk40t/gui/propertypanels/pathproperty.py +389 -380
  228. meerk40t/gui/propertypanels/placementproperty.py +1214 -383
  229. meerk40t/gui/propertypanels/pointproperty.py +140 -136
  230. meerk40t/gui/propertypanels/propertywindow.py +313 -181
  231. meerk40t/gui/propertypanels/rasterwizardpanels.py +996 -912
  232. meerk40t/gui/propertypanels/regbranchproperties.py +76 -0
  233. meerk40t/gui/propertypanels/textproperty.py +770 -755
  234. meerk40t/gui/propertypanels/waitproperty.py +56 -55
  235. meerk40t/gui/propertypanels/warpproperty.py +121 -0
  236. meerk40t/gui/propertypanels/wobbleproperty.py +255 -204
  237. meerk40t/gui/ribbon.py +2471 -2210
  238. meerk40t/gui/scene/scene.py +1100 -1051
  239. meerk40t/gui/scene/sceneconst.py +22 -22
  240. meerk40t/gui/scene/scenepanel.py +439 -349
  241. meerk40t/gui/scene/scenespacewidget.py +365 -365
  242. meerk40t/gui/scene/widget.py +518 -505
  243. meerk40t/gui/scenewidgets/affinemover.py +215 -215
  244. meerk40t/gui/scenewidgets/attractionwidget.py +315 -309
  245. meerk40t/gui/scenewidgets/bedwidget.py +120 -97
  246. meerk40t/gui/scenewidgets/elementswidget.py +137 -107
  247. meerk40t/gui/scenewidgets/gridwidget.py +785 -745
  248. meerk40t/gui/scenewidgets/guidewidget.py +765 -765
  249. meerk40t/gui/scenewidgets/laserpathwidget.py +66 -66
  250. meerk40t/gui/scenewidgets/machineoriginwidget.py +86 -86
  251. meerk40t/gui/scenewidgets/nodeselector.py +28 -28
  252. meerk40t/gui/scenewidgets/rectselectwidget.py +592 -346
  253. meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
  254. meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
  255. meerk40t/gui/scenewidgets/selectionwidget.py +2958 -2756
  256. meerk40t/gui/simpleui.py +362 -333
  257. meerk40t/gui/simulation.py +2451 -2094
  258. meerk40t/gui/snapoptions.py +208 -203
  259. meerk40t/gui/spoolerpanel.py +1227 -1180
  260. meerk40t/gui/statusbarwidgets/defaultoperations.py +480 -353
  261. meerk40t/gui/statusbarwidgets/infowidget.py +520 -483
  262. meerk40t/gui/statusbarwidgets/opassignwidget.py +356 -355
  263. meerk40t/gui/statusbarwidgets/selectionwidget.py +172 -171
  264. meerk40t/gui/statusbarwidgets/shapepropwidget.py +754 -236
  265. meerk40t/gui/statusbarwidgets/statusbar.py +272 -260
  266. meerk40t/gui/statusbarwidgets/statusbarwidget.py +268 -270
  267. meerk40t/gui/statusbarwidgets/strokewidget.py +267 -251
  268. meerk40t/gui/themes.py +200 -78
  269. meerk40t/gui/tips.py +590 -0
  270. meerk40t/gui/toolwidgets/circlebrush.py +35 -35
  271. meerk40t/gui/toolwidgets/toolcircle.py +248 -242
  272. meerk40t/gui/toolwidgets/toolcontainer.py +82 -77
  273. meerk40t/gui/toolwidgets/tooldraw.py +97 -90
  274. meerk40t/gui/toolwidgets/toolellipse.py +219 -212
  275. meerk40t/gui/toolwidgets/toolimagecut.py +25 -132
  276. meerk40t/gui/toolwidgets/toolline.py +39 -144
  277. meerk40t/gui/toolwidgets/toollinetext.py +79 -236
  278. meerk40t/gui/toolwidgets/toollinetext_inline.py +296 -0
  279. meerk40t/gui/toolwidgets/toolmeasure.py +163 -216
  280. meerk40t/gui/toolwidgets/toolnodeedit.py +2088 -2074
  281. meerk40t/gui/toolwidgets/toolnodemove.py +92 -94
  282. meerk40t/gui/toolwidgets/toolparameter.py +754 -668
  283. meerk40t/gui/toolwidgets/toolplacement.py +108 -108
  284. meerk40t/gui/toolwidgets/toolpoint.py +68 -59
  285. meerk40t/gui/toolwidgets/toolpointlistbuilder.py +294 -0
  286. meerk40t/gui/toolwidgets/toolpointmove.py +183 -0
  287. meerk40t/gui/toolwidgets/toolpolygon.py +288 -403
  288. meerk40t/gui/toolwidgets/toolpolyline.py +38 -196
  289. meerk40t/gui/toolwidgets/toolrect.py +211 -207
  290. meerk40t/gui/toolwidgets/toolrelocate.py +72 -72
  291. meerk40t/gui/toolwidgets/toolribbon.py +598 -113
  292. meerk40t/gui/toolwidgets/tooltabedit.py +546 -0
  293. meerk40t/gui/toolwidgets/tooltext.py +98 -89
  294. meerk40t/gui/toolwidgets/toolvector.py +213 -204
  295. meerk40t/gui/toolwidgets/toolwidget.py +39 -39
  296. meerk40t/gui/usbconnect.py +98 -91
  297. meerk40t/gui/utilitywidgets/buttonwidget.py +18 -18
  298. meerk40t/gui/utilitywidgets/checkboxwidget.py +90 -90
  299. meerk40t/gui/utilitywidgets/controlwidget.py +14 -14
  300. meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +343 -340
  301. meerk40t/gui/utilitywidgets/debugwidgets.py +148 -0
  302. meerk40t/gui/utilitywidgets/handlewidget.py +27 -27
  303. meerk40t/gui/utilitywidgets/harmonograph.py +450 -447
  304. meerk40t/gui/utilitywidgets/openclosewidget.py +40 -40
  305. meerk40t/gui/utilitywidgets/rotationwidget.py +54 -54
  306. meerk40t/gui/utilitywidgets/scalewidget.py +75 -75
  307. meerk40t/gui/utilitywidgets/seekbarwidget.py +183 -183
  308. meerk40t/gui/utilitywidgets/togglewidget.py +142 -142
  309. meerk40t/gui/utilitywidgets/toolbarwidget.py +8 -8
  310. meerk40t/gui/wordlisteditor.py +985 -931
  311. meerk40t/gui/wxmeerk40t.py +1447 -1169
  312. meerk40t/gui/wxmmain.py +5644 -4112
  313. meerk40t/gui/wxmribbon.py +1591 -1076
  314. meerk40t/gui/wxmscene.py +1631 -1453
  315. meerk40t/gui/wxmtree.py +2416 -2089
  316. meerk40t/gui/wxutils.py +1769 -1099
  317. meerk40t/gui/zmatrix.py +102 -102
  318. meerk40t/image/__init__.py +1 -1
  319. meerk40t/image/dither.py +429 -0
  320. meerk40t/image/imagetools.py +2793 -2269
  321. meerk40t/internal_plugins.py +150 -130
  322. meerk40t/kernel/__init__.py +63 -12
  323. meerk40t/kernel/channel.py +259 -212
  324. meerk40t/kernel/context.py +538 -538
  325. meerk40t/kernel/exceptions.py +41 -41
  326. meerk40t/kernel/functions.py +463 -414
  327. meerk40t/kernel/jobs.py +100 -100
  328. meerk40t/kernel/kernel.py +3828 -3571
  329. meerk40t/kernel/lifecycles.py +71 -71
  330. meerk40t/kernel/module.py +49 -49
  331. meerk40t/kernel/service.py +147 -147
  332. meerk40t/kernel/settings.py +383 -343
  333. meerk40t/lihuiyu/controller.py +883 -876
  334. meerk40t/lihuiyu/device.py +1181 -1069
  335. meerk40t/lihuiyu/driver.py +1466 -1372
  336. meerk40t/lihuiyu/gui/gui.py +127 -106
  337. meerk40t/lihuiyu/gui/lhyaccelgui.py +377 -363
  338. meerk40t/lihuiyu/gui/lhycontrollergui.py +741 -651
  339. meerk40t/lihuiyu/gui/lhydrivergui.py +470 -446
  340. meerk40t/lihuiyu/gui/lhyoperationproperties.py +238 -237
  341. meerk40t/lihuiyu/gui/tcpcontroller.py +226 -190
  342. meerk40t/lihuiyu/interpreter.py +53 -53
  343. meerk40t/lihuiyu/laserspeed.py +450 -450
  344. meerk40t/lihuiyu/loader.py +90 -90
  345. meerk40t/lihuiyu/parser.py +404 -404
  346. meerk40t/lihuiyu/plugin.py +101 -102
  347. meerk40t/lihuiyu/tcp_connection.py +111 -109
  348. meerk40t/main.py +231 -165
  349. meerk40t/moshi/builder.py +788 -781
  350. meerk40t/moshi/controller.py +505 -499
  351. meerk40t/moshi/device.py +495 -442
  352. meerk40t/moshi/driver.py +862 -696
  353. meerk40t/moshi/gui/gui.py +78 -76
  354. meerk40t/moshi/gui/moshicontrollergui.py +538 -522
  355. meerk40t/moshi/gui/moshidrivergui.py +87 -75
  356. meerk40t/moshi/plugin.py +43 -43
  357. meerk40t/network/console_server.py +140 -57
  358. meerk40t/network/kernelserver.py +10 -9
  359. meerk40t/network/tcp_server.py +142 -140
  360. meerk40t/network/udp_server.py +103 -77
  361. meerk40t/network/web_server.py +404 -0
  362. meerk40t/newly/controller.py +1158 -1144
  363. meerk40t/newly/device.py +874 -732
  364. meerk40t/newly/driver.py +540 -412
  365. meerk40t/newly/gui/gui.py +219 -188
  366. meerk40t/newly/gui/newlyconfig.py +116 -101
  367. meerk40t/newly/gui/newlycontroller.py +193 -186
  368. meerk40t/newly/gui/operationproperties.py +51 -51
  369. meerk40t/newly/mock_connection.py +82 -82
  370. meerk40t/newly/newly_params.py +56 -56
  371. meerk40t/newly/plugin.py +1214 -1246
  372. meerk40t/newly/usb_connection.py +322 -322
  373. meerk40t/rotary/gui/gui.py +52 -46
  374. meerk40t/rotary/gui/rotarysettings.py +240 -232
  375. meerk40t/rotary/rotary.py +202 -98
  376. meerk40t/ruida/control.py +291 -91
  377. meerk40t/ruida/controller.py +138 -1088
  378. meerk40t/ruida/device.py +676 -231
  379. meerk40t/ruida/driver.py +534 -472
  380. meerk40t/ruida/emulator.py +1494 -1491
  381. meerk40t/ruida/exceptions.py +4 -4
  382. meerk40t/ruida/gui/gui.py +71 -76
  383. meerk40t/ruida/gui/ruidaconfig.py +239 -72
  384. meerk40t/ruida/gui/ruidacontroller.py +187 -184
  385. meerk40t/ruida/gui/ruidaoperationproperties.py +48 -47
  386. meerk40t/ruida/loader.py +54 -52
  387. meerk40t/ruida/mock_connection.py +57 -109
  388. meerk40t/ruida/plugin.py +124 -87
  389. meerk40t/ruida/rdjob.py +2084 -945
  390. meerk40t/ruida/serial_connection.py +116 -0
  391. meerk40t/ruida/tcp_connection.py +146 -0
  392. meerk40t/ruida/udp_connection.py +73 -0
  393. meerk40t/svgelements.py +9671 -9669
  394. meerk40t/tools/driver_to_path.py +584 -579
  395. meerk40t/tools/geomstr.py +5583 -4680
  396. meerk40t/tools/jhfparser.py +357 -292
  397. meerk40t/tools/kerftest.py +904 -890
  398. meerk40t/tools/livinghinges.py +1168 -1033
  399. meerk40t/tools/pathtools.py +987 -949
  400. meerk40t/tools/pmatrix.py +234 -0
  401. meerk40t/tools/pointfinder.py +942 -942
  402. meerk40t/tools/polybool.py +941 -940
  403. meerk40t/tools/rasterplotter.py +1660 -547
  404. meerk40t/tools/shxparser.py +1047 -901
  405. meerk40t/tools/ttfparser.py +726 -446
  406. meerk40t/tools/zinglplotter.py +595 -593
  407. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/LICENSE +21 -21
  408. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/METADATA +150 -139
  409. meerk40t-0.9.7020.dist-info/RECORD +446 -0
  410. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/WHEEL +1 -1
  411. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/top_level.txt +0 -1
  412. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/zip-safe +1 -1
  413. meerk40t/balormk/elementlightjob.py +0 -159
  414. meerk40t-0.9.3001.dist-info/RECORD +0 -437
  415. test/bootstrap.py +0 -63
  416. test/test_cli.py +0 -12
  417. test/test_core_cutcode.py +0 -418
  418. test/test_core_elements.py +0 -144
  419. test/test_core_plotplanner.py +0 -397
  420. test/test_core_viewports.py +0 -312
  421. test/test_drivers_grbl.py +0 -108
  422. test/test_drivers_lihuiyu.py +0 -443
  423. test/test_drivers_newly.py +0 -113
  424. test/test_element_degenerate_points.py +0 -43
  425. test/test_elements_classify.py +0 -97
  426. test/test_elements_penbox.py +0 -22
  427. test/test_file_svg.py +0 -176
  428. test/test_fill.py +0 -155
  429. test/test_geomstr.py +0 -1523
  430. test/test_geomstr_nodes.py +0 -18
  431. test/test_imagetools_actualize.py +0 -306
  432. test/test_imagetools_wizard.py +0 -258
  433. test/test_kernel.py +0 -200
  434. test/test_laser_speeds.py +0 -3303
  435. test/test_length.py +0 -57
  436. test/test_lifecycle.py +0 -66
  437. test/test_operations.py +0 -251
  438. test/test_operations_hatch.py +0 -57
  439. test/test_ruida.py +0 -19
  440. test/test_spooler.py +0 -22
  441. test/test_tools_rasterplotter.py +0 -29
  442. test/test_wobble.py +0 -133
  443. test/test_zingl.py +0 -124
  444. {test → meerk40t/cylinder}/__init__.py +0 -0
  445. /meerk40t/{core/element_commands.py → cylinder/gui/__init__.py} +0 -0
  446. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/entry_points.txt +0 -0
@@ -1,1469 +1,1662 @@
1
- import wx
2
-
3
- from meerk40t.core.units import Angle, Length
4
- from meerk40t.gui.laserrender import swizzlecolor
5
- from meerk40t.gui.wxutils import (
6
- CheckBox,
7
- EditableListCtrl,
8
- ScrolledPanel,
9
- StaticBoxSizer,
10
- TextCtrl,
11
- dip_size,
12
- )
13
- from meerk40t.kernel import Context
14
- from meerk40t.svgelements import Color
15
-
16
- _ = wx.GetTranslation
17
-
18
-
19
- class ChoicePropertyPanel(ScrolledPanel):
20
- """
21
- ChoicePropertyPanel is a generic panel that presents a list of properties to be viewed and edited.
22
- In most cases it can be initialized by passing a choices value which will read the registered choice values
23
- and display the given properties, automatically generating an appropriate changers for that property.
24
-
25
- In most cases the ChoicePropertyPanel should be used for properties of a dynamic nature. A lot of different
26
- relationships can be established and the class should be kept fairly easy to extend. With a set dictionary
27
- either registered in the Kernel as a choice or called directly on the ChoicePropertyPanel you can make dynamic
28
- controls to set various properties. This avoids needing to create a new static window when a panel is just
29
- providing options to change settings.
30
- The choices need to be provided either as list of dictionaries or indirectly via
31
- a string indicating a stored list registered in the given context under "choices"
32
- The dictionary recognizes the following entries:
33
-
34
- "object": The object to which the property defined in attr belongs to
35
- "attr": The name of the attribute
36
- "default": The default value if no value has been given before
37
- "label": The label will be used for labelling the to be created UI-elements
38
- "trailer": this text will be displayed immediately after the element
39
- "tip": The tooltip that will be used for this element
40
- "dynamic": a function called with the current dictionary choice. This is to update
41
- values that may have changed since the choice was first established.
42
- "type": This can be one of (no quotation marks, real python data types):
43
- bool: will always be represented by a checkbox
44
- str: normally be represented by a textbox (may be influenced by style)
45
- int: normally be represented by a textbox (may be influenced by style)
46
- float: normally be represented by a textbox (may be influenced by style)
47
- Length: represented by a textbox
48
- Angle: represented by a textbox
49
- Color: represented by a color picker
50
- "style": If given then the standard representation for a data-type (see above)
51
- will be replaced by more tailored UI-elements:
52
- "file": (only available for str) a file selection dialog is used,
53
- this recognizes a further property "wildcard"
54
- "slider:" Creates a slider (for int and float) that will use two additional
55
- entries, "min" and "max.
56
- "combo": see combosmall (but larger).
57
- "option": Creates a combo box but also takes "display" as a parameter
58
- that displays these strings rather than the underlying choices.
59
- "combosmall": Available for str, int, float will fill the combo
60
- with values defined in "choices" (additional parameter)
61
- "binary": uses two additional settings "mask" and "bit" to
62
- allow the bitwise manipulation of an int data type
63
- "weight": only valid in subsections, default value 1, i.e. equal width
64
- allocation, can be changed to force a different sizing behaviour
65
- UI-Appearance
66
- "page":
67
- "section":
68
- "subsection":
69
- "priority":
70
- These entries will create visible separation/joining of elements.
71
- The dictionary list will be sorted first by priority, then page,
72
- then section, then subsection. While normally every item ends up
73
- on a new line, elements within a subsection remain in one horizontal
74
- container.
75
- Notabene:
76
- a) to influence ordering without compromising the intended Page,
77
- Section etc. names, the routine will remove a leading "_xxxx_" string
78
- b) The Page, Section etc. names will be translated, so please provide
79
- them in plain English
80
-
81
- There are some special hacks to influence appearance / internal logic
82
- "hidden": if set then this expert property will only appear if the
83
- developer-mode has been set
84
- "enabled": Is the control enabled (default yes, so does not need to be
85
- provided)
86
- "conditional": if given as tuple (cond_obj, cond_prop) then the (boolean)
87
- value of the property cond_obj.cond_prop will decide if the element
88
- will be enabled or not. (If a third value then the value must equal that value).
89
- "signals": This for advanced treatment, normally any change to a property
90
- will be announced to the wider mk-universe by sending a signal with the
91
- attributes name as signal-indicator (this is used to inform other UI-
92
- elements with the same content of such a change). If you want to invoke
93
- additional logic (or don't want to write a specific signal-listen routine
94
- to forward it to other routines) then you can add a single signal-name
95
- or a list of signal-names to be called
96
- """
97
-
98
- def __init__(
99
- self,
100
- *args,
101
- context: Context = None,
102
- choices=None,
103
- scrolling=True,
104
- constraint=None,
105
- entries_per_column=None,
106
- injector=None,
107
- **kwds,
108
- ):
109
- # constraints are either
110
- # - None (default) - all choices will be display
111
- # - a pair of integers (start, end), from where to where (index) to display
112
- # use case: display the first 10 entries constraint=(0, 9)
113
- # then the remaining: constraint=(10, -1)
114
- # -1 defines the min / max boundaries
115
- # - a list of strings that describe
116
- # the pages to show : constraint=("page1", "page2")
117
- # the pages to omit : constraint=("-page1", "-page2")
118
- # a leading hyphen establishes omission
119
- kwds["style"] = kwds.get("style", 0) | wx.TAB_TRAVERSAL
120
- ScrolledPanel.__init__(self, *args, **kwds)
121
- self.context = context
122
- self.listeners = list()
123
- self.entries_per_column = entries_per_column
124
- self._detached = False
125
- if choices is None:
126
- return
127
- if isinstance(choices, str):
128
- choices = [choices]
129
-
130
- new_choices = []
131
- # we need to create an independent copy of the lookup, otherwise
132
- # any amendments to choices like injector will affect the original
133
-
134
- for choice in choices:
135
- if isinstance(choice, dict):
136
- new_choices.append(choice)
137
- elif isinstance(choice, str):
138
- lookup_choice = self.context.lookup("choices", choice)
139
- if lookup_choice is None:
140
- continue
141
- new_choices.extend(lookup_choice)
142
- else:
143
- new_choices.extend(choice)
144
- choices = new_choices
145
- if injector is not None:
146
- # We have additional stuff to be added, so be it
147
- for c in injector:
148
- choices.append(c)
149
- if len(choices) == 0:
150
- # No choices to process.
151
- return
152
- for c in choices:
153
- needs_dynamic_call = c.get("dynamic")
154
- if needs_dynamic_call:
155
- # Calls dynamic function to update this dictionary before production
156
- needs_dynamic_call(c)
157
- # Let's see whether we have a section and a page property...
158
- for c in choices:
159
- try:
160
- dummy = c["subsection"]
161
- except KeyError:
162
- c["subsection"] = ""
163
- try:
164
- dummy = c["section"]
165
- except KeyError:
166
- c["section"] = ""
167
- try:
168
- dummy = c["page"]
169
- except KeyError:
170
- c["page"] = ""
171
- try:
172
- dummy = c["priority"]
173
- except KeyError:
174
- c["priority"] = "ZZZZZZZZ"
175
- # print ("Choices: " , choices)
176
- prechoices = sorted(
177
- sorted(
178
- sorted(
179
- sorted(choices, key=lambda d: d["priority"]),
180
- key=lambda d: d["subsection"],
181
- ),
182
- key=lambda d: d["section"],
183
- ),
184
- key=lambda d: d["page"],
185
- )
186
- self.choices = list()
187
- dealt_with = False
188
- if constraint is not None:
189
- if isinstance(constraint, (tuple, list, str)):
190
- if isinstance(constraint, str):
191
- # make it a tuple
192
- constraint = (constraint,)
193
- if len(constraint) > 0:
194
- if isinstance(constraint[0], str):
195
- dealt_with = True
196
- # Section list
197
- positive = list()
198
- negative = list()
199
- for item in constraint:
200
- if item.startswith("-"):
201
- item = item[1:]
202
- negative.append(item.lower())
203
- else:
204
- positive.append(item.lower())
205
- for i, c in enumerate(prechoices):
206
- try:
207
- this_page = c["page"].lower()
208
- except KeyError:
209
- this_page = ""
210
- if len(negative) > 0 and len(positive) > 0:
211
- # Negative takes precedence:
212
- if not this_page in negative and this_page in positive:
213
- self.choices.append(c)
214
- elif len(negative) > 0:
215
- # only negative....
216
- if not this_page in negative:
217
- self.choices.append(c)
218
- elif len(positive) > 0:
219
- # only positive....
220
- if this_page in positive:
221
- self.choices.append(c)
222
- else:
223
- dealt_with = True
224
- # Section list
225
- start_from = 0
226
- end_at = len(prechoices)
227
- if constraint[0] >= 0:
228
- start_from = constraint[0]
229
- if len(constraint) > 1 and constraint[1] >= 0:
230
- end_at = constraint[1]
231
- if start_from < 0:
232
- start_from = 0
233
- if end_at > len(prechoices):
234
- end_at = len(prechoices)
235
- if end_at < start_from:
236
- end_at = len(prechoices)
237
- for i, c in enumerate(prechoices):
238
- if start_from <= i < end_at:
239
- self.choices.append(c)
240
- else:
241
- # Empty constraint
242
- pass
243
- if not dealt_with:
244
- # no valid constraints
245
- self.choices = prechoices
246
- if len(self.choices) == 0:
247
- return
248
- sizer_very_main = wx.BoxSizer(wx.HORIZONTAL)
249
- sizer_main = wx.BoxSizer(wx.VERTICAL)
250
- sizer_very_main.Add(sizer_main, 1, wx.EXPAND, 0)
251
- last_page = ""
252
- last_section = ""
253
- last_subsection = ""
254
- last_box = None
255
- current_main_sizer = sizer_main
256
- current_sec_sizer = sizer_main
257
- current_sizer = sizer_main
258
- # By default, 0 as we are stacking up stuff
259
- expansion_flag = 0
260
- current_col_entry = -1
261
- for i, c in enumerate(self.choices):
262
- wants_listener = True
263
- current_col_entry += 1
264
- if self.entries_per_column is not None:
265
- if current_col_entry >= self.entries_per_column:
266
- current_col_entry = -1
267
- prev_main = sizer_main
268
- sizer_main = wx.BoxSizer(wx.VERTICAL)
269
- if prev_main == current_main_sizer:
270
- current_main_sizer = sizer_main
271
- if prev_main == current_sec_sizer:
272
- current_sec_sizer = sizer_main
273
- if prev_main == current_sizer:
274
- current_sizer = sizer_main
275
-
276
- sizer_very_main.Add(sizer_main, 1, wx.EXPAND, 0)
277
- # I think we should reset all sections to make them
278
- # reappear in the next columns
279
- last_page = ""
280
- last_section = ""
281
- last_subsection = ""
282
-
283
- if isinstance(c, tuple):
284
- # If c is tuple
285
- dict_c = dict()
286
- try:
287
- dict_c["object"] = c[0]
288
- dict_c["attr"] = c[1]
289
- dict_c["default"] = c[2]
290
- dict_c["label"] = c[3]
291
- dict_c["tip"] = c[4]
292
- dict_c["type"] = c[5]
293
- except IndexError:
294
- pass
295
- c = dict_c
296
- try:
297
- attr = c["attr"]
298
- obj = c["object"]
299
- except KeyError:
300
- continue
301
- this_subsection = c.get("subsection", "")
302
- this_section = c.get("section", "")
303
- this_page = c.get("page", "")
304
- ctrl_width = c.get("width", 0)
305
- # Do we have a parameter to add a trailing label after the control
306
- trailer = c.get("trailer")
307
- # Is there another signal to send?
308
- additional_signal = []
309
- sig = c.get("signals")
310
- if isinstance(sig, str):
311
- additional_signal.append(sig)
312
- elif isinstance(sig, (tuple, list)):
313
- for _sig in sig:
314
- additional_signal.append(_sig)
315
-
316
- # Do we have a parameter to hide the control unless in expert mode
317
- hidden = c.get("hidden", False)
318
- hidden = (
319
- bool(hidden) if hidden != "False" else False
320
- ) # bool("False") = True
321
- # Do we have a parameter to affect the space consumption?
322
- weight = int(c.get("weight", 1))
323
- if weight < 0:
324
- weight = 0
325
- developer_mode = self.context.root.setting(bool, "developer_mode", False)
326
- if not developer_mode and hidden:
327
- continue
328
- # get default value
329
- if hasattr(obj, attr):
330
- data = getattr(obj, attr)
331
- else:
332
- # if obj lacks attr, default must have been assigned.
333
- try:
334
- data = c["default"]
335
- except KeyError:
336
- # This choice is in error.
337
- continue
338
- data_style = c.get("style", None)
339
- data_type = type(data)
340
- data_type = c.get("type", data_type)
341
- choice_list = None
342
- label = c.get("label", attr) # Undefined label is the attr
343
-
344
- if last_page != this_page:
345
- expansion_flag = 0
346
- last_section = ""
347
- last_subsection = ""
348
- # We could do a notebook, but let's choose a simple StaticBoxSizer instead...
349
- last_box = StaticBoxSizer(
350
- self, wx.ID_ANY, _(self.unsorted_label(this_page)), wx.VERTICAL
351
- )
352
- sizer_main.Add(last_box, 0, wx.EXPAND, 0)
353
- current_main_sizer = last_box
354
- current_sec_sizer = last_box
355
- current_sizer = last_box
356
-
357
- if last_section != this_section:
358
- expansion_flag = 0
359
- last_subsection = ""
360
- if this_section != "":
361
- last_box = StaticBoxSizer(
362
- self,
363
- id=wx.ID_ANY,
364
- label=_(self.unsorted_label(this_section)),
365
- orientation=wx.VERTICAL,
366
- )
367
- current_main_sizer.Add(last_box, 0, wx.EXPAND, 0)
368
- else:
369
- last_box = current_main_sizer
370
- current_sizer = last_box
371
- current_sec_sizer = last_box
372
-
373
- if last_subsection != this_subsection:
374
- expansion_flag = 0
375
- if this_subsection != "":
376
- expansion_flag = 1
377
- lbl = _(self.unsorted_label(this_subsection))
378
- if lbl != "":
379
- last_box = StaticBoxSizer(
380
- self,
381
- id=wx.ID_ANY,
382
- label=lbl,
383
- orientation=wx.HORIZONTAL,
384
- )
385
- else:
386
- last_box = wx.BoxSizer(wx.HORIZONTAL)
387
- current_sec_sizer.Add(last_box, 0, wx.EXPAND, 0)
388
- img = c.get("icon", None)
389
- if img is not None:
390
- icon = wx.StaticBitmap(self, wx.ID_ANY, bitmap=img)
391
- last_box.Add(icon, 0, wx.ALIGN_CENTER_VERTICAL, 0)
392
- last_box.AddSpacer(5)
393
- else:
394
- last_box = current_sec_sizer
395
- current_sizer = last_box
396
-
397
- control = None
398
- control_sizer = None
399
- if data_type == str and data_style == "info":
400
- # This is just an info box.
401
- wants_listener = False
402
- msgs = label.split("\n")
403
- controls = []
404
- for lbl in msgs:
405
- control = wx.StaticText(self, label=lbl)
406
- current_sizer.Add(control, expansion_flag * weight, wx.EXPAND, 0)
407
- elif data_type == bool and data_style == "button":
408
- # This is just a signal to the outside world.
409
- wants_listener = False
410
- control = wx.Button(self, label=label)
411
-
412
- def on_button(param, obj, addsig):
413
- def check(event=None):
414
- # We just set it to True to kick it off
415
- setattr(obj, param, True)
416
- # We don't signal ourselves...
417
- self.context.signal(param, True, obj)
418
- for _sig in addsig:
419
- self.context.signal(_sig)
420
-
421
- return check
422
-
423
- control.Bind(
424
- wx.EVT_BUTTON,
425
- on_button(attr, obj, additional_signal),
426
- )
427
- if ctrl_width > 0:
428
- control.SetMaxSize(dip_size(self, ctrl_width, -1))
429
- current_sizer.Add(control, expansion_flag * weight, wx.EXPAND, 0)
430
- elif data_type == bool:
431
- # Bool type objects get a checkbox.
432
- control = CheckBox(self, label=label)
433
- control.SetValue(data)
434
- control.SetMinSize(dip_size(self, -1, 23))
435
-
436
- def on_checkbox_check(param, ctrl, obj, addsig):
437
- def check(event=None):
438
- v = ctrl.GetValue()
439
- current_value = getattr(obj, param)
440
- if current_value != bool(v):
441
- setattr(obj, param, bool(v))
442
- self.context.signal(param, v, obj)
443
- for _sig in addsig:
444
- self.context.signal(_sig)
445
-
446
- return check
447
-
448
- control.Bind(
449
- wx.EVT_CHECKBOX,
450
- on_checkbox_check(attr, control, obj, additional_signal),
451
- )
452
-
453
- current_sizer.Add(control, expansion_flag * weight, wx.EXPAND, 0)
454
- elif data_type == str and data_style == "file":
455
- control_sizer = StaticBoxSizer(self, wx.ID_ANY, label, wx.HORIZONTAL)
456
- control = TextCtrl(
457
- self,
458
- wx.ID_ANY,
459
- style=wx.TE_PROCESS_ENTER,
460
- )
461
- control_btn = wx.Button(self, wx.ID_ANY, "...")
462
-
463
- def set_file(filename: str):
464
- # if not filename:
465
- # filename = _("No File")
466
- control.SetValue(filename)
467
-
468
- def on_button_filename(param, ctrl, obj, wildcard, addsig):
469
- def click(event=None):
470
- with wx.FileDialog(
471
- self,
472
- label,
473
- wildcard=wildcard if wildcard else "*",
474
- style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST,
475
- ) as fileDialog:
476
- if fileDialog.ShowModal() == wx.ID_CANCEL:
477
- return # the user changed their mind
478
- pathname = str(fileDialog.GetPath())
479
- ctrl.SetValue(pathname)
480
- self.Layout()
481
- current_value = getattr(obj, param)
482
- if current_value != pathname:
483
- try:
484
- setattr(obj, param, pathname)
485
- self.context.signal(param, pathname, obj)
486
- for _sig in addsig:
487
- self.context.signal(_sig)
488
- except ValueError:
489
- # cannot cast to data_type, pass
490
- pass
491
-
492
- return click
493
-
494
- def on_file_text(param, ctrl, obj, dtype, addsig):
495
- def filetext():
496
- v = ctrl.GetValue()
497
- try:
498
- dtype_v = dtype(v)
499
- current_value = getattr(obj, param)
500
- if current_value != dtype_v:
501
- setattr(obj, param, dtype_v)
502
- self.context.signal(param, dtype_v, obj)
503
- for _sig in addsig:
504
- self.context.signal(_sig)
505
- except ValueError:
506
- # cannot cast to data_type, pass
507
- pass
508
-
509
- return filetext
510
-
511
- control.SetActionRoutine(
512
- on_file_text(attr, control, obj, data_type, additional_signal)
513
- )
514
-
515
- ctrl_width = c.get("width", 0)
516
- if ctrl_width > 0:
517
- control.SetMaxSize(dip_size(self, ctrl_width, -1))
518
- control.SetValue(str(data))
519
- if ctrl_width > 0:
520
- control.SetMaxSize(dip_size(self, ctrl_width, -1))
521
- if ctrl_width > 0:
522
- control.SetMaxSize(dip_size(self, ctrl_width, -1))
523
- control_sizer.Add(control, 1, wx.EXPAND, 0)
524
- control_sizer.Add(control_btn, 0, wx.EXPAND, 0)
525
- control_btn.Bind(
526
- wx.EVT_BUTTON,
527
- on_button_filename(
528
- attr, control, obj, c.get("wildcard", "*"), additional_signal
529
- ),
530
- )
531
- current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
532
- elif data_type in (int, float) and data_style == "slider":
533
- if label != "":
534
- control_sizer = StaticBoxSizer(
535
- self, wx.ID_ANY, label, wx.HORIZONTAL
536
- )
537
- else:
538
- control_sizer = wx.BoxSizer(wx.HORIZONTAL)
539
- minvalue = c.get("min", 0)
540
- maxvalue = c.get("max", 0)
541
- if data_type == float:
542
- value = float(data)
543
- elif data_type == int:
544
- value = int(data)
545
- else:
546
- value = int(data)
547
- control = wx.Slider(
548
- self,
549
- wx.ID_ANY,
550
- value=value,
551
- minValue=minvalue,
552
- maxValue=maxvalue,
553
- style=wx.SL_HORIZONTAL | wx.SL_VALUE_LABEL,
554
- )
555
-
556
- def on_slider(param, ctrl, obj, dtype, addsig):
557
- def select(event=None):
558
- v = dtype(ctrl.GetValue())
559
- current_value = getattr(obj, param)
560
- if current_value != v:
561
- setattr(obj, param, v)
562
- self.context.signal(param, v, obj)
563
- for _sig in addsig:
564
- self.context.signal(_sig)
565
-
566
- return select
567
-
568
- if ctrl_width > 0:
569
- control.SetMaxSize(dip_size(self, ctrl_width, -1))
570
- control_sizer.Add(control, 1, wx.EXPAND, 0)
571
- control.Bind(
572
- wx.EVT_SLIDER,
573
- on_slider(attr, control, obj, data_type, additional_signal),
574
- )
575
- current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
576
- elif data_type in (str, int, float) and data_style == "combo":
577
- if label != "":
578
- control_sizer = StaticBoxSizer(
579
- self, wx.ID_ANY, label, wx.HORIZONTAL
580
- )
581
- else:
582
- control_sizer = wx.BoxSizer(wx.HORIZONTAL)
583
- choice_list = list(map(str, c.get("choices", [c.get("default")])))
584
- control = wx.ComboBox(
585
- self,
586
- wx.ID_ANY,
587
- choices=choice_list,
588
- style=wx.CB_DROPDOWN | wx.CB_READONLY,
589
- )
590
- if data is not None:
591
- if data_type == str:
592
- control.SetValue(str(data))
593
- else:
594
- least = None
595
- for entry in choice_list:
596
- if least is None:
597
- least = entry
598
- else:
599
- if abs(data_type(entry) - data) < abs(
600
- data_type(least) - data
601
- ):
602
- least = entry
603
- if least is not None:
604
- control.SetValue(least)
605
-
606
- def on_combo_text(param, ctrl, obj, dtype, addsig):
607
- def select(event=None):
608
- v = dtype(ctrl.GetValue())
609
- current_value = getattr(obj, param)
610
- if current_value != v:
611
- setattr(obj, param, v)
612
- self.context.signal(param, v, obj)
613
- for _sig in addsig:
614
- self.context.signal(_sig)
615
-
616
- return select
617
-
618
- if ctrl_width > 0:
619
- control.SetMaxSize(dip_size(self, ctrl_width, -1))
620
- control_sizer.Add(control, 1, wx.ALIGN_CENTER_VERTICAL, 0)
621
- control.Bind(
622
- wx.EVT_COMBOBOX,
623
- on_combo_text(attr, control, obj, data_type, additional_signal),
624
- )
625
- current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
626
- elif data_type in (str, int) and data_style == "radio":
627
- control_sizer = wx.BoxSizer(wx.HORIZONTAL)
628
- choice_list = list(map(str, c.get("choices", [c.get("default")])))
629
- control = wx.RadioBox(
630
- self,
631
- wx.ID_ANY,
632
- label,
633
- choices=choice_list,
634
- majorDimension=3,
635
- style=wx.RA_SPECIFY_COLS, # wx.RA_SPECIFY_ROWS,
636
- )
637
- if data is not None:
638
- if data_type == str:
639
- control.SetSelection(0)
640
- for i, c in enumerate(choice_list):
641
- if c == data:
642
- control.SetSelection(i)
643
- else:
644
- control.SetSelection(int(data))
645
-
646
- def on_radio_select(param, ctrl, obj, dtype, addsig):
647
- def select(event=None):
648
- if dtype == int:
649
- v = dtype(ctrl.GetSelection())
650
- else:
651
- v = dtype(ctrl.GetLabel())
652
- current_value = getattr(obj, param)
653
- if current_value != v:
654
- setattr(obj, param, v)
655
- self.context.signal(param, v, obj)
656
- for _sig in addsig:
657
- self.context.signal(_sig)
658
-
659
- return select
660
-
661
- if ctrl_width > 0:
662
- control.SetMaxSize(dip_size(self, ctrl_width, -1))
663
- control_sizer.Add(control, 1, wx.ALIGN_CENTER_VERTICAL, 0)
664
- control.Bind(
665
- wx.EVT_RADIOBOX,
666
- on_radio_select(attr, control, obj, data_type, additional_signal),
667
- )
668
- current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
669
- elif data_type in (int, str) and data_style == "option":
670
- control_sizer = wx.BoxSizer(wx.HORIZONTAL)
671
- display_list = list(map(str, c.get("display")))
672
- choice_list = list(map(str, c.get("choices", [c.get("default")])))
673
- try:
674
- index = choice_list.index(str(data))
675
- except ValueError:
676
- # Value was not in list.
677
- index = 0
678
- if data is None:
679
- data = c.get("default")
680
- display_list.insert(0, str(data))
681
- choice_list.insert(0, str(data))
682
- control = wx.ComboBox(
683
- self,
684
- wx.ID_ANY,
685
- choices=display_list,
686
- style=wx.CB_DROPDOWN | wx.CB_READONLY,
687
- )
688
- control.SetSelection(index)
689
-
690
- # Constrain the width
691
- testsize = control.GetBestSize()
692
- control.SetMaxSize(dip_size(self, testsize[0] + 30, -1))
693
- # print ("Display: %s" % display_list)
694
- # print ("Choices: %s" % choice_list)
695
- # print ("To set: %s" % str(data))
696
-
697
- def on_combosmall_option(param, ctrl, obj, dtype, addsig, choice_list):
698
- def select(event=None):
699
- cl = choice_list[ctrl.GetSelection()]
700
- v = dtype(cl)
701
- current_value = getattr(obj, param)
702
- if current_value != v:
703
- setattr(obj, param, v)
704
- self.context.signal(param, v, obj)
705
- for _sig in addsig:
706
- self.context.signal(_sig)
707
-
708
- return select
709
-
710
- if label != "":
711
- # Try to center it vertically to the controls extent
712
- wd, ht = control.GetSize()
713
- label_text = wx.StaticText(self, id=wx.ID_ANY, label=label + " ")
714
- # label_text.SetMinSize(dip_size(self, -1, ht))
715
- control_sizer.Add(label_text, 0, wx.ALIGN_CENTER_VERTICAL, 0)
716
- control_sizer.Add(control, 1, wx.ALIGN_CENTER_VERTICAL, 0)
717
- control.Bind(
718
- wx.EVT_COMBOBOX,
719
- on_combosmall_option(
720
- attr, control, obj, data_type, additional_signal, choice_list
721
- ),
722
- )
723
- current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
724
- elif data_type in (str, int, float) and data_style == "combosmall":
725
- control_sizer = wx.BoxSizer(wx.HORIZONTAL)
726
-
727
- choice_list = list(map(str, c.get("choices", [c.get("default")])))
728
- control = wx.ComboBox(
729
- self,
730
- wx.ID_ANY,
731
- choices=choice_list,
732
- style=wx.CB_DROPDOWN | wx.CB_READONLY,
733
- )
734
- # Constrain the width
735
- testsize = control.GetBestSize()
736
- control.SetMaxSize(dip_size(self, testsize[0] + 30, -1))
737
- # print ("Choices: %s" % choice_list)
738
- # print ("To set: %s" % str(data))
739
- if data is not None:
740
- if data_type == str:
741
- control.SetValue(str(data))
742
- else:
743
- least = None
744
- for entry in choice_list:
745
- if least is None:
746
- least = entry
747
- else:
748
- if abs(data_type(entry) - data) < abs(
749
- data_type(least) - data
750
- ):
751
- least = entry
752
- if least is not None:
753
- control.SetValue(least)
754
-
755
- def on_combosmall_text(param, ctrl, obj, dtype, addsig):
756
- def select(event=None):
757
- v = dtype(ctrl.GetValue())
758
- current_value = getattr(obj, param)
759
- if current_value != v:
760
- setattr(obj, param, v)
761
- self.context.signal(param, v, obj)
762
- for _sig in addsig:
763
- self.context.signal(_sig)
764
-
765
- return select
766
-
767
- if label != "":
768
- # Try to center it vertically to the controls extent
769
- wd, ht = control.GetSize()
770
- label_text = wx.StaticText(self, id=wx.ID_ANY, label=label + " ")
771
- # label_text.SetMinSize(dip_size(self, -1, ht))
772
- control_sizer.Add(label_text, 0, wx.ALIGN_CENTER_VERTICAL, 0)
773
- if ctrl_width > 0:
774
- control.SetMaxSize(dip_size(self, ctrl_width, -1))
775
- control_sizer.Add(control, 1, wx.ALIGN_CENTER_VERTICAL, 0)
776
- control.Bind(
777
- wx.EVT_COMBOBOX,
778
- on_combosmall_text(
779
- attr, control, obj, data_type, additional_signal
780
- ),
781
- )
782
- current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
783
- elif data_type == int and data_style == "binary":
784
- mask = c.get("mask")
785
-
786
- # get default value
787
- mask_bits = 0
788
- if mask is not None and hasattr(obj, mask):
789
- mask_bits = getattr(obj, mask)
790
-
791
- if label != "":
792
- control_sizer = StaticBoxSizer(
793
- self, wx.ID_ANY, label, wx.HORIZONTAL
794
- )
795
- else:
796
- control_sizer = wx.BoxSizer(wx.HORIZONTAL)
797
-
798
- def on_checkbox_check(param, ctrl, obj, bit, addsig, enable_ctrl=None):
799
- def check(event=None):
800
- v = ctrl.GetValue()
801
- if enable_ctrl is not None:
802
- enable_ctrl.Enable(v)
803
- current = getattr(obj, param)
804
- if v:
805
- current |= 1 << bit
806
- else:
807
- current = ~((~current) | (1 << bit))
808
- current_value = getattr(obj, param)
809
- if current_value != current:
810
- setattr(obj, param, current)
811
- self.context.signal(param, v, obj)
812
- for _sig in addsig:
813
- self.context.signal(_sig)
814
-
815
- return check
816
-
817
- bit_sizer = wx.BoxSizer(wx.VERTICAL)
818
- label_text = wx.StaticText(
819
- self, wx.ID_ANY, "", style=wx.ALIGN_CENTRE_HORIZONTAL
820
- )
821
- bit_sizer.Add(label_text, 0, wx.EXPAND, 0)
822
- if mask is not None:
823
- label_text = wx.StaticText(
824
- self,
825
- wx.ID_ANY,
826
- _("mask") + " ",
827
- style=wx.ALIGN_CENTRE_HORIZONTAL,
828
- )
829
- bit_sizer.Add(label_text, 0, wx.EXPAND, 0)
830
- label_text = wx.StaticText(
831
- self, wx.ID_ANY, _("value") + " ", style=wx.ALIGN_CENTRE_HORIZONTAL
832
- )
833
- bit_sizer.Add(label_text, 0, wx.EXPAND, 0)
834
- control_sizer.Add(bit_sizer, 0, wx.EXPAND, 0)
835
-
836
- bits = c.get("bits", 8)
837
- for b in range(bits):
838
- # Label
839
- bit_sizer = wx.BoxSizer(wx.VERTICAL)
840
- label_text = wx.StaticText(
841
- self, wx.ID_ANY, str(b), style=wx.ALIGN_CENTRE_HORIZONTAL
842
- )
843
- bit_sizer.Add(label_text, 0, wx.EXPAND, 0)
844
-
845
- # value bit
846
- control = wx.CheckBox(self)
847
- control.SetValue(bool((data >> b) & 1))
848
- if mask:
849
- control.Enable(bool((mask_bits >> b) & 1))
850
- control.Bind(
851
- wx.EVT_CHECKBOX,
852
- on_checkbox_check(attr, control, obj, b, additional_signal),
853
- )
854
-
855
- # mask bit
856
- if mask:
857
- mask_ctrl = wx.CheckBox(self)
858
- mask_ctrl.SetValue(bool((mask_bits >> b) & 1))
859
- mask_ctrl.Bind(
860
- wx.EVT_CHECKBOX,
861
- on_checkbox_check(
862
- mask,
863
- mask_ctrl,
864
- obj,
865
- b,
866
- additional_signal,
867
- enable_ctrl=control,
868
- ),
869
- )
870
- bit_sizer.Add(mask_ctrl, 0, wx.EXPAND, 0)
871
-
872
- bit_sizer.Add(control, 0, wx.EXPAND, 0)
873
- control_sizer.Add(bit_sizer, 0, wx.EXPAND, 0)
874
-
875
- current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
876
- elif data_type == str and data_style == "color":
877
- # str data_type with style "color" objects do get a button with the background.
878
- control_sizer = wx.BoxSizer(wx.HORIZONTAL)
879
- control = wx.Button(self, -1)
880
-
881
- def set_color(ctrl, color: Color):
882
- ctrl.SetLabel(str(color.hex))
883
- ctrl.SetBackgroundColour(wx.Colour(swizzlecolor(color)))
884
- if Color.distance(color, Color("black")) > Color.distance(
885
- color, Color("white")
886
- ):
887
- ctrl.SetForegroundColour(wx.BLACK)
888
- else:
889
- ctrl.SetForegroundColour(wx.WHITE)
890
- ctrl.color = color
891
-
892
- def on_button_color(param, ctrl, obj, addsig):
893
- def click(event=None):
894
- color_data = wx.ColourData()
895
- color_data.SetColour(wx.Colour(swizzlecolor(ctrl.color)))
896
- dlg = wx.ColourDialog(self, color_data)
897
- if dlg.ShowModal() == wx.ID_OK:
898
- color_data = dlg.GetColourData()
899
- data = Color(
900
- swizzlecolor(color_data.GetColour().GetRGB()), 1.0
901
- )
902
- set_color(ctrl, data)
903
- try:
904
- data_v = data.hexa
905
- current_value = getattr(obj, param)
906
- if current_value != data_v:
907
- setattr(obj, param, data_v)
908
- self.context.signal(param, data_v, obj)
909
- for _sig in addsig:
910
- self.context.signal(_sig)
911
- except ValueError:
912
- # cannot cast to data_type, pass
913
- pass
914
-
915
- return click
916
-
917
- datastr = data
918
- data = Color(datastr)
919
- set_color(control, data)
920
- if ctrl_width > 0:
921
- control.SetMaxSize(dip_size(self, ctrl_width, -1))
922
- control_sizer.Add(control, 0, wx.EXPAND, 0)
923
- color_info = wx.StaticText(self, wx.ID_ANY, label)
924
- control_sizer.Add(color_info, 1, wx.ALIGN_CENTER_VERTICAL)
925
-
926
- control.Bind(
927
- wx.EVT_BUTTON,
928
- on_button_color(attr, control, obj, additional_signal),
929
- )
930
- if ctrl_width > 0:
931
- control.SetMaxSize(dip_size(self, ctrl_width, -1))
932
- current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
933
- elif data_type == list and data_style == "chart":
934
- chart = EditableListCtrl(
935
- self,
936
- wx.ID_ANY,
937
- style=wx.LC_HRULES | wx.LC_REPORT | wx.LC_VRULES | wx.LC_SINGLE_SEL,
938
- )
939
- columns = c.get("columns", [])
940
-
941
- def fill_ctrl():
942
- chart.ClearAll()
943
- for column in columns:
944
- chart.AppendColumn(
945
- column.get("label", ""),
946
- format=wx.LIST_FORMAT_LEFT,
947
- width=column.get("width", 150),
948
- )
949
- for dataline in data:
950
- row_id = chart.InsertItem(
951
- chart.GetItemCount(),
952
- dataline.get("speed", 0), # TODO: Speed hardcoded
953
- )
954
- for column_id, column in enumerate(columns):
955
- c_attr = column.get("attr")
956
- chart.SetItem(
957
- row_id, column_id, str(dataline.get(c_attr, ""))
958
- )
959
-
960
- fill_ctrl()
961
-
962
- def on_chart_start(columns, param, ctrl, obj):
963
- def chart_start(event=None):
964
- for column in columns:
965
- if column.get("editable", False):
966
- event.Allow()
967
- else:
968
- event.Veto()
969
-
970
- return chart_start
971
-
972
- chart.Bind(
973
- wx.EVT_LIST_BEGIN_LABEL_EDIT,
974
- on_chart_start(columns, attr, chart, obj),
975
- )
976
-
977
- def on_chart_stop(columns, param, ctrl, obj):
978
- def chart_stop(event=None):
979
- row_id = event.GetIndex() # Get the current row
980
- col_id = event.GetColumn() # Get the current column
981
- new_data = event.GetLabel() # Get the changed data
982
- ctrl.SetItem(row_id, col_id, new_data)
983
- column = columns[col_id]
984
- c_attr = column.get("attr")
985
- c_type = column.get("type")
986
- values = getattr(obj, attr)
987
- values[row_id][c_attr] = c_type(new_data)
988
- self.context.signal(param, values, row_id, attr)
989
-
990
- return chart_stop
991
-
992
- chart.Bind(
993
- wx.EVT_LIST_END_LABEL_EDIT, on_chart_stop(columns, attr, chart, obj)
994
- )
995
-
996
- allow_deletion = c.get("allow_deletion", False)
997
- allow_duplication = c.get("allow_duplication", False)
998
-
999
- def on_chart_contextmenu(columns, param, ctrl, obj):
1000
- def chart_menu(event=None):
1001
- row_id = event.GetIndex() # Get the current row
1002
- if row_id < 0:
1003
- return
1004
- menu = wx.Menu()
1005
- if allow_deletion:
1006
-
1007
- def on_delete(event):
1008
- values = getattr(obj, attr)
1009
- # try:
1010
- values.pop(row_id)
1011
- self.context.signal(param, values, row_id, attr)
1012
- fill_ctrl()
1013
- # except IndexError:
1014
- # pass
1015
-
1016
- menuitem = menu.Append(
1017
- wx.ID_ANY, _("Delete this entry"), ""
1018
- )
1019
- self.Bind(
1020
- wx.EVT_MENU,
1021
- on_delete,
1022
- id=menuitem.GetId(),
1023
- )
1024
- if allow_duplication:
1025
-
1026
- def on_duplicate(event):
1027
- values = getattr(obj, attr)
1028
- newentry = dict()
1029
- for key, content in values[row_id].items():
1030
- newentry[key] = content
1031
- values.append(newentry)
1032
- self.context.signal(param, values, row_id, attr)
1033
- # except IndexError:
1034
- # pass
1035
- fill_ctrl()
1036
-
1037
- menuitem = menu.Append(
1038
- wx.ID_ANY, _("Duplicate this entry"), ""
1039
- )
1040
- self.Bind(
1041
- wx.EVT_MENU,
1042
- on_duplicate,
1043
- id=menuitem.GetId(),
1044
- )
1045
-
1046
- def on_default(event):
1047
- values = getattr(obj, attr)
1048
- default = c.get("default", [])
1049
- values.clear()
1050
- for e in default:
1051
- values.append(e)
1052
-
1053
- self.context.signal(param, values, 0, attr)
1054
- fill_ctrl()
1055
- # except IndexError:
1056
- # pass
1057
-
1058
- menuitem = menu.Append(wx.ID_ANY, _("Restore defaults"), "")
1059
- self.Bind(
1060
- wx.EVT_MENU,
1061
- on_default,
1062
- id=menuitem.GetId(),
1063
- )
1064
-
1065
- if menu.MenuItemCount != 0:
1066
- self.PopupMenu(menu)
1067
- menu.Destroy()
1068
-
1069
- return chart_menu
1070
-
1071
- chart.Bind(
1072
- wx.EVT_LIST_ITEM_RIGHT_CLICK,
1073
- on_chart_contextmenu(columns, attr, chart, obj),
1074
- )
1075
- sizer_main.Add(chart, 0, wx.EXPAND, 0)
1076
- elif data_type in (str, int, float):
1077
- # str, int, and float type objects get a TextCtrl setter.
1078
- if label != "":
1079
- control_sizer = StaticBoxSizer(
1080
- self, wx.ID_ANY, label, wx.HORIZONTAL
1081
- )
1082
- else:
1083
- control_sizer = wx.BoxSizer(wx.HORIZONTAL)
1084
- if data_type == int:
1085
- check_flag = "int"
1086
- limit = True
1087
- elif data_type == float:
1088
- check_flag = "float"
1089
- limit = True
1090
- else:
1091
- check_flag = ""
1092
- limit = False
1093
- control = TextCtrl(
1094
- self,
1095
- wx.ID_ANY,
1096
- style=wx.TE_PROCESS_ENTER,
1097
- limited=limit,
1098
- check=check_flag,
1099
- )
1100
- ctrl_width = c.get("width", 0)
1101
- if ctrl_width > 0:
1102
- control.SetMaxSize(dip_size(self, ctrl_width, -1))
1103
- control.SetValue(str(data))
1104
- if ctrl_width > 0:
1105
- control.SetMaxSize(dip_size(self, ctrl_width, -1))
1106
- control_sizer.Add(control, 1, wx.EXPAND, 0)
1107
-
1108
- def on_generic_text(param, ctrl, obj, dtype, addsig):
1109
- def text():
1110
- v = ctrl.GetValue()
1111
- try:
1112
- dtype_v = dtype(v)
1113
- current_value = getattr(obj, param)
1114
- if current_value != dtype_v:
1115
- setattr(obj, param, dtype_v)
1116
- self.context.signal(param, dtype_v, obj)
1117
- for _sig in addsig:
1118
- self.context.signal(_sig)
1119
- except ValueError:
1120
- # cannot cast to data_type, pass
1121
- pass
1122
-
1123
- return text
1124
-
1125
- control.SetActionRoutine(
1126
- on_generic_text(attr, control, obj, data_type, additional_signal)
1127
- )
1128
- current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
1129
- elif data_type == Length:
1130
- # Length type is a TextCtrl with special checks
1131
- if label != "":
1132
- control_sizer = StaticBoxSizer(
1133
- self, wx.ID_ANY, label, wx.HORIZONTAL
1134
- )
1135
- else:
1136
- control_sizer = wx.BoxSizer(wx.HORIZONTAL)
1137
- nonzero = c.get("nonzero", False)
1138
- if nonzero is None or not isinstance(nonzero, bool):
1139
- nonzero = False
1140
- control = TextCtrl(
1141
- self,
1142
- wx.ID_ANY,
1143
- style=wx.TE_PROCESS_ENTER,
1144
- limited=True,
1145
- check="length",
1146
- nonzero=nonzero,
1147
- )
1148
- if isinstance(data, Length):
1149
- if not data._digits:
1150
- if data._preferred_units in ("mm", "cm", "in", "inch"):
1151
- data._digits = 4
1152
- control.SetValue(str(data))
1153
- if ctrl_width > 0:
1154
- control.SetMaxSize(dip_size(self, ctrl_width, -1))
1155
- control_sizer.Add(control, 1, wx.EXPAND, 0)
1156
-
1157
- def on_length_text(param, ctrl, obj, dtype, addsig):
1158
- def text():
1159
- try:
1160
- v = Length(ctrl.GetValue())
1161
- data_v = v.preferred_length
1162
- current_value = getattr(obj, param)
1163
- if str(current_value) != str(data_v):
1164
- setattr(obj, param, data_v)
1165
- self.context.signal(param, data_v, obj)
1166
- for _sig in addsig:
1167
- self.context.signal(_sig)
1168
- except ValueError:
1169
- # cannot cast to data_type, pass
1170
- pass
1171
-
1172
- return text
1173
-
1174
- control.SetActionRoutine(
1175
- on_length_text(attr, control, obj, data_type, additional_signal)
1176
- )
1177
- current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
1178
- elif data_type == Angle:
1179
- # Angle type is a TextCtrl with special checks
1180
- if label != "":
1181
- control_sizer = StaticBoxSizer(
1182
- self, wx.ID_ANY, label, wx.HORIZONTAL
1183
- )
1184
- else:
1185
- control_sizer = wx.BoxSizer(wx.HORIZONTAL)
1186
- control = TextCtrl(
1187
- self,
1188
- wx.ID_ANY,
1189
- style=wx.TE_PROCESS_ENTER,
1190
- check="angle",
1191
- limited=True,
1192
- )
1193
- control.SetValue(str(data))
1194
- if ctrl_width > 0:
1195
- control.SetMaxSize(dip_size(self, ctrl_width, -1))
1196
- control_sizer.Add(control, 1, wx.EXPAND, 0)
1197
-
1198
- def on_angle_text(param, ctrl, obj, dtype, addsig):
1199
- def text():
1200
- try:
1201
- v = Angle(ctrl.GetValue(), digits=5)
1202
- data_v = str(v)
1203
- current_value = str(getattr(obj, param))
1204
- if current_value != data_v:
1205
- setattr(obj, param, data_v)
1206
- self.context.signal(param, data_v, obj)
1207
- for _sig in addsig:
1208
- self.context.signal(_sig)
1209
- except ValueError:
1210
- # cannot cast to data_type, pass
1211
- pass
1212
-
1213
- return text
1214
-
1215
- control.SetActionRoutine(
1216
- on_angle_text(attr, control, obj, data_type, additional_signal)
1217
- )
1218
- current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
1219
- elif data_type == Color:
1220
- # Color data_type objects are get a button with the background.
1221
- if label != "":
1222
- control_sizer = StaticBoxSizer(
1223
- self, wx.ID_ANY, label, wx.HORIZONTAL
1224
- )
1225
- else:
1226
- control_sizer = wx.BoxSizer(wx.HORIZONTAL)
1227
- control = wx.Button(self, -1)
1228
-
1229
- def set_color(ctrl, color: Color):
1230
- ctrl.SetLabel(str(color.hex))
1231
- ctrl.SetBackgroundColour(wx.Colour(swizzlecolor(color)))
1232
- if Color.distance(color, Color("black")) > Color.distance(
1233
- color, Color("white")
1234
- ):
1235
- ctrl.SetForegroundColour(wx.BLACK)
1236
- else:
1237
- ctrl.SetForegroundColour(wx.WHITE)
1238
- ctrl.color = color
1239
-
1240
- def on_button_color(param, ctrl, obj, addsig):
1241
- def click(event=None):
1242
- color_data = wx.ColourData()
1243
- color_data.SetColour(wx.Colour(swizzlecolor(ctrl.color)))
1244
- dlg = wx.ColourDialog(self, color_data)
1245
- if dlg.ShowModal() == wx.ID_OK:
1246
- color_data = dlg.GetColourData()
1247
- data = Color(
1248
- swizzlecolor(color_data.GetColour().GetRGB()), 1.0
1249
- )
1250
- set_color(ctrl, data)
1251
- try:
1252
- data_v = data_type(data)
1253
- current_value = getattr(obj, param)
1254
- if current_value != data_v:
1255
- setattr(obj, param, data_v)
1256
- self.context.signal(param, data_v, obj)
1257
- for _sig in addsig:
1258
- self.context.signal(_sig)
1259
- except ValueError:
1260
- # cannot cast to data_type, pass
1261
- pass
1262
-
1263
- return click
1264
-
1265
- set_color(control, data)
1266
- if ctrl_width > 0:
1267
- control.SetMaxSize(dip_size(self, ctrl_width, -1))
1268
- control_sizer.Add(control, 0, wx.EXPAND, 0)
1269
-
1270
- control.Bind(
1271
- wx.EVT_BUTTON,
1272
- on_button_color(attr, control, obj, additional_signal),
1273
- )
1274
- if ctrl_width > 0:
1275
- control.SetMaxSize(dip_size(self, ctrl_width, -1))
1276
- current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
1277
- else:
1278
- # Requires a registered data_type
1279
- continue
1280
-
1281
- if trailer and control_sizer:
1282
- trailer_text = wx.StaticText(self, id=wx.ID_ANY, label=f" {trailer}")
1283
- control_sizer.Add(trailer_text, 0, wx.ALIGN_CENTER_VERTICAL, 0)
1284
-
1285
- if control is None:
1286
- continue # We're binary or some other style without a specific control.
1287
-
1288
- # Get enabled value
1289
- try:
1290
- enabled = c["enabled"]
1291
- control.Enable(enabled)
1292
- except KeyError:
1293
- # Listen to establish whether this control should be enabled based on another control's value.
1294
- try:
1295
- conditional = c["conditional"]
1296
- if len(conditional) == 2:
1297
- c_obj, c_attr = conditional
1298
- enabled = bool(getattr(c_obj, c_attr))
1299
- c_equals = True
1300
- control.Enable(enabled)
1301
- elif len(conditional) == 3:
1302
- c_obj, c_attr, c_equals = conditional
1303
- enabled = bool(getattr(c_obj, c_attr) == c_equals)
1304
- control.Enable(enabled)
1305
- elif len(conditional) == 4:
1306
- c_obj, c_attr, c_from, c_to = conditional
1307
- enabled = bool(c_from <= getattr(c_obj, c_attr) <= c_to)
1308
- c_equals = (c_from, c_to)
1309
- control.Enable(enabled)
1310
-
1311
- def on_enable_listener(param, ctrl, obj, eqs):
1312
- def listen(origin, value, target=None):
1313
- try:
1314
- if isinstance(eqs, (list, tuple)):
1315
- enable = bool(
1316
- eqs[0] <= getattr(obj, param) <= eqs[1]
1317
- )
1318
- else:
1319
- enable = bool(getattr(obj, param) == eqs)
1320
- ctrl.Enable(enable)
1321
- except (IndexError, RuntimeError):
1322
- pass
1323
-
1324
- return listen
1325
-
1326
- listener = on_enable_listener(c_attr, control, c_obj, c_equals)
1327
- self.listeners.append((c_attr, listener))
1328
- context.listen(c_attr, listener)
1329
- except KeyError:
1330
- pass
1331
-
1332
- # Now we listen to 'ourselves' as well to learn about changes somewhere else...
1333
- def on_update_listener(param, ctrl, dtype, dstyle, choicelist, sourceobj):
1334
- def listen_to_myself(origin, value, target=None):
1335
- if target is None or target is not sourceobj:
1336
- # print (f"Signal for {param}={value}, but no target given or different to source")
1337
- return
1338
- update_needed = False
1339
- # print (f"attr={param}, origin={origin}, value={value}, datatype={dtype}, datastyle={dstyle}")
1340
- data = None
1341
- if value is not None:
1342
- try:
1343
- data = dtype(value)
1344
- except ValueError:
1345
- pass
1346
- if data is None:
1347
- try:
1348
- data = c["default"]
1349
- except KeyError:
1350
- pass
1351
- if data is None:
1352
- return
1353
- if dtype == bool:
1354
- # Bool type objects get a checkbox.
1355
- if ctrl.GetValue() != data:
1356
- ctrl.SetValue(data)
1357
- elif dtype == str and dstyle == "file":
1358
- if ctrl.GetValue() != data:
1359
- ctrl.SetValue(data)
1360
- elif dtype in (int, float) and dstyle == "slider":
1361
- if ctrl.GetValue() != data:
1362
- ctrl.SetValue(data)
1363
- elif dtype in (str, int, float) and dstyle == "combo":
1364
- if dtype == str:
1365
- ctrl.SetValue(str(data))
1366
- else:
1367
- least = None
1368
- for entry in choicelist:
1369
- if least is None:
1370
- least = entry
1371
- else:
1372
- if abs(dtype(entry) - data) < abs(
1373
- dtype(least) - data
1374
- ):
1375
- least = entry
1376
- if least is not None:
1377
- ctrl.SetValue(least)
1378
- elif dtype in (str, int, float) and dstyle == "combosmall":
1379
- if dtype == str:
1380
- ctrl.SetValue(str(data))
1381
- else:
1382
- least = None
1383
- for entry in choicelist:
1384
- if least is None:
1385
- least = entry
1386
- else:
1387
- if abs(dtype(entry) - data) < abs(
1388
- dtype(least) - data
1389
- ):
1390
- least = entry
1391
- if least is not None:
1392
- ctrl.SetValue(least)
1393
- elif dtype == int and dstyle == "binary":
1394
- pass # not supported...
1395
- elif dtype in (str, int, float):
1396
- if hasattr(ctrl, "GetValue"):
1397
- try:
1398
- if dtype(ctrl.GetValue()) != data:
1399
- update_needed = True
1400
- except ValueError:
1401
- update_needed = True
1402
- if update_needed:
1403
- ctrl.SetValue(str(data))
1404
- elif dtype == Length:
1405
- if float(data) != float(Length(ctrl.GetValue())):
1406
- update_needed = True
1407
- if update_needed:
1408
- ctrl.SetValue(str(data))
1409
- elif dtype == Angle:
1410
- if ctrl.GetValue() != str(data):
1411
- ctrl.SetValue(str(data))
1412
- elif dtype == Color:
1413
- # Color dtype objects are a button with the background set to the color
1414
- def set_color(color: Color):
1415
- ctrl.SetLabel(str(color.hex))
1416
- ctrl.SetBackgroundColour(wx.Colour(swizzlecolor(color)))
1417
- if Color.distance(color, Color("black")) > Color.distance(
1418
- color, Color("white")
1419
- ):
1420
- ctrl.SetForegroundColour(wx.BLACK)
1421
- else:
1422
- ctrl.SetForegroundColour(wx.WHITE)
1423
- ctrl.color = color
1424
-
1425
- set_color(data)
1426
-
1427
- return listen_to_myself
1428
-
1429
- if wants_listener:
1430
- update_listener = on_update_listener(
1431
- attr, control, data_type, data_style, choice_list, obj
1432
- )
1433
- self.listeners.append((attr, update_listener))
1434
- context.listen(attr, update_listener)
1435
- tip = c.get("tip")
1436
- if tip and not context.root.disable_tool_tips:
1437
- # Set the tool tip if 'tip' is available
1438
- control.SetToolTip(tip)
1439
- last_page = this_page
1440
- last_section = this_section
1441
- last_subsection = this_subsection
1442
-
1443
- self.SetSizer(sizer_very_main)
1444
- sizer_very_main.Fit(self)
1445
- # Make sure stuff gets scrolled if necessary by default
1446
- if scrolling:
1447
- self.SetupScrolling()
1448
-
1449
- @staticmethod
1450
- def unsorted_label(original):
1451
- # Special sort key just to sort stuff - we fix the preceeding "_sortcriteria_Correct label"
1452
- result = original
1453
- if result.startswith("_"):
1454
- idx = result.find("_", 1)
1455
- if idx >= 0:
1456
- result = result[idx + 1 :]
1457
- return result
1458
-
1459
- def module_close(self, *args, **kwargs):
1460
- self.pane_hide()
1461
-
1462
- def pane_hide(self):
1463
- if not self._detached:
1464
- for attr, listener in self.listeners:
1465
- self.context.unlisten(attr, listener)
1466
- self._detached = True
1467
-
1468
- def pane_show(self):
1469
- pass
1
+ from copy import copy
2
+
3
+ import wx
4
+
5
+ from meerk40t.core.units import Angle, Length
6
+ from meerk40t.gui.laserrender import swizzlecolor
7
+ from meerk40t.gui.wxutils import (
8
+ EditableListCtrl,
9
+ ScrolledPanel,
10
+ StaticBoxSizer,
11
+ TextCtrl,
12
+ dip_size,
13
+ wxButton,
14
+ wxCheckBox,
15
+ wxComboBox,
16
+ wxRadioBox,
17
+ wxStaticBitmap,
18
+ wxStaticText,
19
+ )
20
+ from meerk40t.kernel import Context
21
+ from meerk40t.svgelements import Color
22
+
23
+ _ = wx.GetTranslation
24
+
25
+
26
+ class ChoicePropertyPanel(ScrolledPanel):
27
+ """
28
+ ChoicePropertyPanel is a generic panel that presents a list of properties to be viewed and edited.
29
+ In most cases it can be initialized by passing a choices value which will read the registered choice values
30
+ and display the given properties, automatically generating an appropriate changers for that property.
31
+
32
+ In most cases the ChoicePropertyPanel should be used for properties of a dynamic nature. A lot of different
33
+ relationships can be established and the class should be kept fairly easy to extend. With a set dictionary
34
+ either registered in the Kernel as a choice or called directly on the ChoicePropertyPanel you can make dynamic
35
+ controls to set various properties. This avoids needing to create a new static window when a panel is just
36
+ providing options to change settings.
37
+ The choices need to be provided either as list of dictionaries or indirectly via
38
+ a string indicating a stored list registered in the given context under "choices"
39
+ The dictionary recognizes the following entries:
40
+
41
+ "object": The object to which the property defined in attr belongs to
42
+ "attr": The name of the attribute
43
+ "default": The default value if no value has been given before
44
+ "label": The label will be used for labelling the to be created UI-elements
45
+ "trailer": this text will be displayed immediately after the element
46
+ "tip": The tooltip that will be used for this element
47
+ "dynamic": a function called with the current dictionary choice. This is to update
48
+ values that may have changed since the choice was first established.
49
+ "type": This can be one of (no quotation marks, real python data types):
50
+ bool: will always be represented by a checkbox
51
+ str: normally be represented by a textbox (may be influenced by style)
52
+ int: normally be represented by a textbox (may be influenced by style)
53
+ float: normally be represented by a textbox (may be influenced by style)
54
+ Length: represented by a textbox
55
+ Angle: represented by a textbox
56
+ Color: represented by a color picker
57
+ "style": If given then the standard representation for a data-type (see above)
58
+ will be replaced by more tailored UI-elements:
59
+ "file": (only available for str) a file selection dialog is used,
60
+ this recognizes a further property "wildcard"
61
+ "slider:" Creates a slider (for int and float) that will use two additional
62
+ entries, "min" and "max.
63
+ "combo": see combosmall (but larger).
64
+ "option": Creates a combo box but also takes "display" as a parameter
65
+ that displays these strings rather than the underlying choices.
66
+ "combosmall": Available for str, int, float will fill the combo
67
+ with values defined in "choices" (additional parameter)
68
+ "binary": uses two additional settings "mask" and "bit" to
69
+ allow the bitwise manipulation of an int data type
70
+ "multiline": (only available for str) the content allows multiline input
71
+ "weight": only valid in subsections, default value 1, i.e. equal width
72
+ allocation, can be changed to force a different sizing behaviour
73
+ UI-Appearance
74
+ "page":
75
+ "section":
76
+ "subsection":
77
+ "priority":
78
+ These entries will create visible separation/joining of elements.
79
+ The dictionary list will be sorted first by priority, then page,
80
+ then section, then subsection. While normally every item ends up
81
+ on a new line, elements within a subsection remain in one horizontal
82
+ container.
83
+ Notabene:
84
+ a) to influence ordering without compromising the intended Page,
85
+ Section etc. names, the routine will remove a leading "_xxxx_" string
86
+ b) The Page, Section etc. names will be translated, so please provide
87
+ them in plain English
88
+
89
+ There are some special hacks to influence appearance / internal logic
90
+ "hidden": if set then this expert property will only appear if the
91
+ developer-mode has been set
92
+ "enabled": Is the control enabled (default yes, so does not need to be
93
+ provided)
94
+ "conditional": if given as tuple (cond_obj, cond_prop) then the (boolean)
95
+ value of the property cond_obj.cond_prop will decide if the element
96
+ will be enabled or not. (If a third value then the value must equal that value).
97
+ "signals": This for advanced treatment, normally any change to a property
98
+ will be announced to the wider mk-universe by sending a signal with the
99
+ attributes name as signal-indicator (this is used to inform other UI-
100
+ elements with the same content of such a change). If you want to invoke
101
+ additional logic (or don't want to write a specific signal-listen routine
102
+ to forward it to other routines) then you can add a single signal-name
103
+ or a list of signal-names to be called
104
+ """
105
+
106
+ def __init__(
107
+ self,
108
+ *args,
109
+ context: Context = None,
110
+ choices=None,
111
+ scrolling=True,
112
+ constraint=None,
113
+ entries_per_column=None,
114
+ injector=None,
115
+ **kwds,
116
+ ):
117
+ # constraints are either
118
+ # - None (default) - all choices will be display
119
+ # - a pair of integers (start, end), from where to where (index) to display
120
+ # use case: display the first 10 entries constraint=(0, 9)
121
+ # then the remaining: constraint=(10, -1)
122
+ # -1 defines the min / max boundaries
123
+ # - a list of strings that describe
124
+ # the pages to show : constraint=("page1", "page2")
125
+ # the pages to omit : constraint=("-page1", "-page2")
126
+ # a leading hyphen establishes omission
127
+ kwds["style"] = kwds.get("style", 0) | wx.TAB_TRAVERSAL
128
+ ScrolledPanel.__init__(self, *args, **kwds)
129
+ self.context = context
130
+ self.context.themes.set_window_colors(self)
131
+ self.listeners = list()
132
+ self.entries_per_column = entries_per_column
133
+ if choices is None:
134
+ return
135
+ if isinstance(choices, str):
136
+ choices = [choices]
137
+
138
+ new_choices = []
139
+ # we need to create an independent copy of the lookup, otherwise
140
+ # any amendments to choices like injector will affect the original
141
+ standardhelp = ""
142
+
143
+ def on_combo_text(param, ctrl, obj, dtype, addsig):
144
+ def select(event=None):
145
+ v = dtype(ctrl.GetValue())
146
+ current_value = getattr(obj, param)
147
+ if current_value != v:
148
+ setattr(obj, param, v)
149
+ self.context.signal(param, v, obj)
150
+ for _sig in addsig:
151
+ self.context.signal(_sig)
152
+
153
+ return select
154
+
155
+ def on_button(param, obj, addsig):
156
+ def check(event=None):
157
+ # We just set it to True to kick it off
158
+ setattr(obj, param, True)
159
+ # We don't signal ourselves...
160
+ self.context.signal(param, True, obj)
161
+ for _sig in addsig:
162
+ self.context.signal(_sig)
163
+
164
+ return check
165
+
166
+ def on_checkbox_check(param, ctrl, obj, addsig):
167
+ def check(event=None):
168
+ v = ctrl.GetValue()
169
+ current_value = getattr(obj, param)
170
+ if current_value != bool(v):
171
+ setattr(obj, param, bool(v))
172
+ self.context.signal(param, v, obj)
173
+ for _sig in addsig:
174
+ self.context.signal(_sig)
175
+
176
+ return check
177
+
178
+ def on_checkbox_bitcheck(param, ctrl, obj, bit, addsig, enable_ctrl=None):
179
+ def check(event=None):
180
+ v = ctrl.GetValue()
181
+ if enable_ctrl is not None:
182
+ enable_ctrl.Enable(v)
183
+ current = getattr(obj, param)
184
+ if v:
185
+ current |= 1 << bit
186
+ else:
187
+ current = ~((~current) | (1 << bit))
188
+ current_value = getattr(obj, param)
189
+ if current_value != current:
190
+ setattr(obj, param, current)
191
+ self.context.signal(param, v, obj)
192
+ for _sig in addsig:
193
+ self.context.signal(_sig)
194
+
195
+ return check
196
+
197
+ def on_generic_multi(param, ctrl, obj, dtype, addsig):
198
+ def text():
199
+ v = ctrl.GetValue()
200
+ try:
201
+ dtype_v = dtype(v)
202
+ current_value = getattr(obj, param)
203
+ if current_value != dtype_v:
204
+ setattr(obj, param, dtype_v)
205
+ self.context.signal(param, dtype_v, obj)
206
+ for _sig in addsig:
207
+ self.context.signal(_sig)
208
+ except ValueError:
209
+ # cannot cast to data_type, pass
210
+ pass
211
+
212
+ return text
213
+
214
+ def on_button_filename(param, ctrl, obj, wildcard, addsig):
215
+ def click(event=None):
216
+ with wx.FileDialog(
217
+ self,
218
+ label,
219
+ wildcard=wildcard if wildcard else "*",
220
+ style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_PREVIEW,
221
+ ) as fileDialog:
222
+ if fileDialog.ShowModal() == wx.ID_CANCEL:
223
+ return # the user changed their mind
224
+ pathname = str(fileDialog.GetPath())
225
+ ctrl.SetValue(pathname)
226
+ self.Layout()
227
+ current_value = getattr(obj, param)
228
+ if current_value != pathname:
229
+ try:
230
+ setattr(obj, param, pathname)
231
+ self.context.signal(param, pathname, obj)
232
+ for _sig in addsig:
233
+ self.context.signal(_sig)
234
+ except ValueError:
235
+ # cannot cast to data_type, pass
236
+ pass
237
+
238
+ return click
239
+
240
+ def on_file_text(param, ctrl, obj, dtype, addsig):
241
+ def filetext():
242
+ v = ctrl.GetValue()
243
+ try:
244
+ dtype_v = dtype(v)
245
+ current_value = getattr(obj, param)
246
+ if current_value != dtype_v:
247
+ setattr(obj, param, dtype_v)
248
+ self.context.signal(param, dtype_v, obj)
249
+ for _sig in addsig:
250
+ self.context.signal(_sig)
251
+ except ValueError:
252
+ # cannot cast to data_type, pass
253
+ pass
254
+
255
+ return filetext
256
+
257
+ def on_slider(param, ctrl, obj, dtype, addsig):
258
+ def select(event=None):
259
+ v = dtype(ctrl.GetValue())
260
+ current_value = getattr(obj, param)
261
+ if current_value != v:
262
+ setattr(obj, param, v)
263
+ self.context.signal(param, v, obj)
264
+ for _sig in addsig:
265
+ self.context.signal(_sig)
266
+
267
+ return select
268
+
269
+ def on_radio_select(param, ctrl, obj, dtype, addsig):
270
+ def select(event=None):
271
+ if dtype == int:
272
+ v = dtype(ctrl.GetSelection())
273
+ else:
274
+ v = dtype(ctrl.GetLabel())
275
+ current_value = getattr(obj, param)
276
+ if current_value != v:
277
+ setattr(obj, param, v)
278
+ self.context.signal(param, v, obj)
279
+ for _sig in addsig:
280
+ self.context.signal(_sig)
281
+
282
+ return select
283
+
284
+ def on_combosmall_option(param, ctrl, obj, dtype, addsig, choice_list):
285
+ def select(event=None):
286
+ cl = choice_list[ctrl.GetSelection()]
287
+ v = dtype(cl)
288
+ current_value = getattr(obj, param)
289
+ if current_value != v:
290
+ setattr(obj, param, v)
291
+ self.context.signal(param, v, obj)
292
+ for _sig in addsig:
293
+ self.context.signal(_sig)
294
+
295
+ return select
296
+
297
+ def on_combosmall_text(param, ctrl, obj, dtype, addsig):
298
+ def select(event=None):
299
+ v = dtype(ctrl.GetValue())
300
+ current_value = getattr(obj, param)
301
+ if current_value != v:
302
+ # print (f"Setting it to {v}")
303
+ setattr(obj, param, v)
304
+ self.context.signal(param, v, obj)
305
+ for _sig in addsig:
306
+ self.context.signal(_sig)
307
+
308
+ return select
309
+
310
+ def on_button_color(param, ctrl, obj, addsig):
311
+ def click(event=None):
312
+ color_data = wx.ColourData()
313
+ color_data.SetColour(wx.Colour(swizzlecolor(ctrl.color)))
314
+ dlg = wx.ColourDialog(self, color_data)
315
+ if dlg.ShowModal() == wx.ID_OK:
316
+ color_data = dlg.GetColourData()
317
+ data = Color(
318
+ swizzlecolor(color_data.GetColour().GetRGB()), 1.0
319
+ )
320
+ set_color(ctrl, data)
321
+ try:
322
+ data_v = data.hexa
323
+ current_value = getattr(obj, param)
324
+ if current_value != data_v:
325
+ setattr(obj, param, data_v)
326
+ self.context.signal(param, data_v, obj)
327
+ for _sig in addsig:
328
+ self.context.signal(_sig)
329
+ except ValueError:
330
+ # cannot cast to data_type, pass
331
+ pass
332
+
333
+ return click
334
+
335
+ def on_angle_text(param, ctrl, obj, dtype, addsig):
336
+ def text():
337
+ try:
338
+ v = Angle(ctrl.GetValue(), digits=5)
339
+ data_v = str(v)
340
+ current_value = str(getattr(obj, param))
341
+ if current_value != data_v:
342
+ setattr(obj, param, data_v)
343
+ self.context.signal(param, data_v, obj)
344
+ for _sig in addsig:
345
+ self.context.signal(_sig)
346
+ except ValueError:
347
+ # cannot cast to data_type, pass
348
+ pass
349
+
350
+ return text
351
+
352
+ def on_chart_start(columns, param, ctrl, local_obj):
353
+ def chart_start(event=None):
354
+ for column in columns:
355
+ if column.get("editable", False):
356
+ event.Allow()
357
+ else:
358
+ event.Veto()
359
+
360
+ return chart_start
361
+
362
+ def on_chart_stop(columns, param, ctrl, local_obj):
363
+ def chart_stop(event=None):
364
+ row_id = event.GetIndex() # Get the current row
365
+ col_id = event.GetColumn() # Get the current column
366
+ new_data = event.GetLabel() # Get the changed data
367
+ ctrl.SetItem(row_id, col_id, new_data)
368
+ column = columns[col_id]
369
+ c_attr = column.get("attr")
370
+ c_type = column.get("type")
371
+ values = getattr(local_obj, param)
372
+ if isinstance(values[row_id], dict):
373
+ values[row_id][c_attr] = c_type(new_data)
374
+ self.context.signal(param, values, row_id, param)
375
+ elif isinstance(values[row_id], str):
376
+ values[row_id] = c_type(new_data)
377
+ self.context.signal(param, values, row_id)
378
+ else:
379
+ values[row_id][col_id] = c_type(new_data)
380
+ self.context.signal(param, values, row_id)
381
+
382
+ return chart_stop
383
+
384
+ def on_chart_contextmenu(
385
+ columns, param, ctrl, local_obj, allow_del, allow_dup, default
386
+ ):
387
+ def chart_menu(event=None):
388
+ # row_id = event.GetIndex() # Get the current row
389
+
390
+ x, y = event.GetPosition()
391
+ row_id, flags = ctrl.HitTest((x, y))
392
+ if row_id < 0:
393
+ l_allow_del = False
394
+ l_allow_dup = False
395
+ else:
396
+ l_allow_del = allow_del
397
+ l_allow_dup = allow_dup
398
+ menu = wx.Menu()
399
+ if l_allow_del:
400
+
401
+ def on_delete(event):
402
+ values = getattr(local_obj, param)
403
+ # try:
404
+ values.pop(row_id)
405
+ self.context.signal(param, values, 0, param)
406
+ fill_ctrl(ctrl, local_obj, param, columns)
407
+ # except IndexError:
408
+ # pass
409
+
410
+ menuitem = menu.Append(
411
+ wx.ID_ANY, _("Delete this entry"), ""
412
+ )
413
+ self.Bind(
414
+ wx.EVT_MENU,
415
+ on_delete,
416
+ id=menuitem.GetId(),
417
+ )
418
+ if l_allow_dup:
419
+
420
+ def on_duplicate(event):
421
+ values = getattr(local_obj, param)
422
+ if isinstance(values[row_id], dict):
423
+ newentry = dict()
424
+ for key, content in values[row_id].items():
425
+ newentry[key] = content
426
+ else:
427
+ newentry = copy(values[row_id])
428
+ values.append(newentry)
429
+ self.context.signal(param, values, 0, param)
430
+ # except IndexError:
431
+ # pass
432
+ fill_ctrl(ctrl, local_obj, param, columns)
433
+
434
+ menuitem = menu.Append(
435
+ wx.ID_ANY, _("Duplicate this entry"), ""
436
+ )
437
+ self.Bind(
438
+ wx.EVT_MENU,
439
+ on_duplicate,
440
+ id=menuitem.GetId(),
441
+ )
442
+
443
+ def on_default(event):
444
+ values = getattr(local_obj, param)
445
+ values.clear()
446
+ for e in default:
447
+ values.append(e)
448
+
449
+ self.context.signal(param, values, 0, param)
450
+ fill_ctrl(ctrl, local_obj, param, columns)
451
+ # except IndexError:
452
+ # pass
453
+
454
+ menuitem = menu.Append(wx.ID_ANY, _("Restore defaults"), "")
455
+ self.Bind(
456
+ wx.EVT_MENU,
457
+ on_default,
458
+ id=menuitem.GetId(),
459
+ )
460
+
461
+ if menu.MenuItemCount != 0:
462
+ self.PopupMenu(menu)
463
+ menu.Destroy()
464
+
465
+ return chart_menu
466
+
467
+ def on_generic_text(param, ctrl, obj, dtype, addsig):
468
+ def text():
469
+ v = ctrl.GetValue()
470
+ try:
471
+ dtype_v = dtype(v)
472
+ current_value = getattr(obj, param)
473
+ if current_value != dtype_v:
474
+ setattr(obj, param, dtype_v)
475
+ self.context.signal(param, dtype_v, obj)
476
+ for _sig in addsig:
477
+ self.context.signal(_sig)
478
+ except ValueError:
479
+ # cannot cast to data_type, pass
480
+ pass
481
+
482
+ return text
483
+
484
+ def on_length_text(param, ctrl, obj, dtype, addsig):
485
+ def text():
486
+ try:
487
+ v = Length(ctrl.GetValue())
488
+ data_v = v.preferred_length
489
+ current_value = getattr(obj, param)
490
+ if str(current_value) != str(data_v):
491
+ setattr(obj, param, data_v)
492
+ self.context.signal(param, data_v, obj)
493
+ for _sig in addsig:
494
+ self.context.signal(_sig)
495
+ except ValueError:
496
+ # cannot cast to data_type, pass
497
+ pass
498
+
499
+ return text
500
+
501
+
502
+ for choice in choices:
503
+ if isinstance(choice, dict):
504
+ if "help" not in choice:
505
+ choice["help"] = standardhelp
506
+ new_choices.append(choice)
507
+ elif isinstance(choice, str):
508
+ lookup_choice = self.context.lookup("choices", choice)
509
+ if lookup_choice is None:
510
+ continue
511
+ for c in lookup_choice:
512
+ if "help" not in c:
513
+ c["help"] = choice
514
+ new_choices.extend(lookup_choice)
515
+ else:
516
+ for c in choice:
517
+ if "help" not in c:
518
+ c["help"] = standardhelp
519
+ new_choices.extend(choice)
520
+ choices = new_choices
521
+ if injector is not None:
522
+ # We have additional stuff to be added, so be it
523
+ for c in injector:
524
+ choices.append(c)
525
+ if len(choices) == 0:
526
+ # No choices to process.
527
+ return
528
+ for c in choices:
529
+ needs_dynamic_call = c.get("dynamic")
530
+ if needs_dynamic_call:
531
+ # Calls dynamic function to update this dictionary before production
532
+ needs_dynamic_call(c)
533
+ # Let's see whether we have a section and a page property...
534
+ for c in choices:
535
+ try:
536
+ dummy = c["subsection"]
537
+ except KeyError:
538
+ c["subsection"] = ""
539
+ try:
540
+ dummy = c["section"]
541
+ except KeyError:
542
+ c["section"] = ""
543
+ try:
544
+ dummy = c["page"]
545
+ except KeyError:
546
+ c["page"] = ""
547
+ try:
548
+ dummy = c["priority"]
549
+ except KeyError:
550
+ c["priority"] = "ZZZZZZZZ"
551
+ # print ("Choices: " , choices)
552
+ prechoices = sorted(
553
+ sorted(
554
+ sorted(
555
+ sorted(choices, key=lambda d: d["priority"]),
556
+ key=lambda d: d["subsection"],
557
+ ),
558
+ key=lambda d: d["section"],
559
+ ),
560
+ key=lambda d: d["page"],
561
+ )
562
+ self.choices = list()
563
+ dealt_with = False
564
+ if constraint is not None:
565
+ if isinstance(constraint, (tuple, list, str)):
566
+ if isinstance(constraint, str):
567
+ # make it a tuple
568
+ constraint = (constraint,)
569
+ if len(constraint) > 0:
570
+ if isinstance(constraint[0], str):
571
+ dealt_with = True
572
+ # Section list
573
+ positive = list()
574
+ negative = list()
575
+ for item in constraint:
576
+ if item.startswith("-"):
577
+ item = item[1:]
578
+ negative.append(item.lower())
579
+ else:
580
+ positive.append(item.lower())
581
+ for i, c in enumerate(prechoices):
582
+ try:
583
+ this_page = c["page"].lower()
584
+ except KeyError:
585
+ this_page = ""
586
+ if len(negative) > 0 and len(positive) > 0:
587
+ # Negative takes precedence:
588
+ if not this_page in negative and this_page in positive:
589
+ self.choices.append(c)
590
+ elif len(negative) > 0:
591
+ # only negative....
592
+ if not this_page in negative:
593
+ self.choices.append(c)
594
+ elif len(positive) > 0:
595
+ # only positive....
596
+ if this_page in positive:
597
+ self.choices.append(c)
598
+ else:
599
+ dealt_with = True
600
+ # Section list
601
+ start_from = 0
602
+ end_at = len(prechoices)
603
+ if constraint[0] >= 0:
604
+ start_from = constraint[0]
605
+ if len(constraint) > 1 and constraint[1] >= 0:
606
+ end_at = constraint[1]
607
+ if start_from < 0:
608
+ start_from = 0
609
+ if end_at > len(prechoices):
610
+ end_at = len(prechoices)
611
+ if end_at < start_from:
612
+ end_at = len(prechoices)
613
+ for i, c in enumerate(prechoices):
614
+ if start_from <= i < end_at:
615
+ self.choices.append(c)
616
+ else:
617
+ # Empty constraint
618
+ pass
619
+ if not dealt_with:
620
+ # no valid constraints
621
+ self.choices = prechoices
622
+ if len(self.choices) == 0:
623
+ return
624
+ sizer_very_main = wx.BoxSizer(wx.HORIZONTAL)
625
+ sizer_main = wx.BoxSizer(wx.VERTICAL)
626
+ sizer_very_main.Add(sizer_main, 1, wx.EXPAND, 0)
627
+ last_page = ""
628
+ last_section = ""
629
+ last_subsection = ""
630
+ last_box = None
631
+ current_main_sizer = sizer_main
632
+ current_sec_sizer = sizer_main
633
+ current_sizer = sizer_main
634
+ # By default, 0 as we are stacking up stuff
635
+ expansion_flag = 0
636
+ current_col_entry = -1
637
+ for i, c in enumerate(self.choices):
638
+ wants_listener = True
639
+ current_col_entry += 1
640
+ if self.entries_per_column is not None:
641
+ if current_col_entry >= self.entries_per_column:
642
+ current_col_entry = -1
643
+ prev_main = sizer_main
644
+ sizer_main = wx.BoxSizer(wx.VERTICAL)
645
+ if prev_main == current_main_sizer:
646
+ current_main_sizer = sizer_main
647
+ if prev_main == current_sec_sizer:
648
+ current_sec_sizer = sizer_main
649
+ if prev_main == current_sizer:
650
+ current_sizer = sizer_main
651
+
652
+ sizer_very_main.Add(sizer_main, 1, wx.EXPAND, 0)
653
+ # I think we should reset all sections to make them
654
+ # reappear in the next columns
655
+ last_page = ""
656
+ last_section = ""
657
+ last_subsection = ""
658
+
659
+ if isinstance(c, tuple):
660
+ # If c is tuple
661
+ dict_c = dict()
662
+ try:
663
+ dict_c["object"] = c[0]
664
+ dict_c["attr"] = c[1]
665
+ dict_c["default"] = c[2]
666
+ dict_c["label"] = c[3]
667
+ dict_c["tip"] = c[4]
668
+ dict_c["type"] = c[5]
669
+ except IndexError:
670
+ pass
671
+ c = dict_c
672
+ try:
673
+ attr = c["attr"]
674
+ obj = c["object"]
675
+ except KeyError:
676
+ continue
677
+ this_subsection = c.get("subsection", "")
678
+ this_section = c.get("section", "")
679
+ this_page = c.get("page", "")
680
+ ctrl_width = c.get("width", 0)
681
+ # Do we have a parameter to add a trailing label after the control
682
+ trailer = c.get("trailer")
683
+ # Is there another signal to send?
684
+ additional_signal = []
685
+ sig = c.get("signals")
686
+ if isinstance(sig, str):
687
+ additional_signal.append(sig)
688
+ elif isinstance(sig, (tuple, list)):
689
+ for _sig in sig:
690
+ additional_signal.append(_sig)
691
+
692
+ # Do we have a parameter to hide the control unless in expert mode
693
+ hidden = c.get("hidden", False)
694
+ hidden = (
695
+ bool(hidden) if hidden != "False" else False
696
+ ) # bool("False") = True
697
+ # Do we have a parameter to affect the space consumption?
698
+ weight = int(c.get("weight", 1))
699
+ if weight < 0:
700
+ weight = 0
701
+ developer_mode = self.context.root.setting(bool, "developer_mode", False)
702
+ if not developer_mode and hidden:
703
+ continue
704
+ # get default value
705
+ if hasattr(obj, attr):
706
+ data = getattr(obj, attr)
707
+ else:
708
+ # if obj lacks attr, default must have been assigned.
709
+ try:
710
+ data = c["default"]
711
+ except KeyError:
712
+ # This choice is in error.
713
+ continue
714
+ data_style = c.get("style", None)
715
+ data_type = type(data)
716
+ data_type = c.get("type", data_type)
717
+ choice_list = None
718
+ label = c.get("label", attr) # Undefined label is the attr
719
+
720
+ if last_page != this_page:
721
+ expansion_flag = 0
722
+ last_section = ""
723
+ last_subsection = ""
724
+ # We could do a notebook, but let's choose a simple StaticBoxSizer instead...
725
+ last_box = StaticBoxSizer(
726
+ self, wx.ID_ANY, _(self.unsorted_label(this_page)), wx.VERTICAL
727
+ )
728
+ sizer_main.Add(last_box, 0, wx.EXPAND, 0)
729
+ current_main_sizer = last_box
730
+ current_sec_sizer = last_box
731
+ current_sizer = last_box
732
+
733
+ if last_section != this_section:
734
+ expansion_flag = 0
735
+ last_subsection = ""
736
+ if this_section != "":
737
+ last_box = StaticBoxSizer(
738
+ self,
739
+ id=wx.ID_ANY,
740
+ label=_(self.unsorted_label(this_section)),
741
+ orientation=wx.VERTICAL,
742
+ )
743
+ current_main_sizer.Add(last_box, 0, wx.EXPAND, 0)
744
+ else:
745
+ last_box = current_main_sizer
746
+ current_sizer = last_box
747
+ current_sec_sizer = last_box
748
+
749
+ if last_subsection != this_subsection:
750
+ expansion_flag = 0
751
+ if this_subsection != "":
752
+ expansion_flag = 1
753
+ lbl = _(self.unsorted_label(this_subsection))
754
+ if lbl != "":
755
+ last_box = StaticBoxSizer(
756
+ self,
757
+ id=wx.ID_ANY,
758
+ label=lbl,
759
+ orientation=wx.HORIZONTAL,
760
+ )
761
+ else:
762
+ last_box = wx.BoxSizer(wx.HORIZONTAL)
763
+ current_sec_sizer.Add(last_box, 0, wx.EXPAND, 0)
764
+ img = c.get("icon", None)
765
+ if img is not None:
766
+ icon = wxStaticBitmap(self, wx.ID_ANY, bitmap=img)
767
+ last_box.Add(icon, 0, wx.ALIGN_CENTER_VERTICAL, 0)
768
+ last_box.AddSpacer(5)
769
+ else:
770
+ last_box = current_sec_sizer
771
+ current_sizer = last_box
772
+
773
+ control = None
774
+ control_sizer = None
775
+ if data_type == str and data_style == "info":
776
+ # This is just an info box.
777
+ wants_listener = False
778
+ msgs = label.split("\n")
779
+ controls = []
780
+ for lbl in msgs:
781
+ control = wxStaticText(self, label=lbl)
782
+ current_sizer.Add(control, expansion_flag * weight, wx.EXPAND, 0)
783
+ elif data_type == bool and data_style == "button":
784
+ # This is just a signal to the outside world.
785
+ wants_listener = False
786
+ control = wxButton(self, label=label)
787
+
788
+
789
+ control.Bind(
790
+ wx.EVT_BUTTON,
791
+ on_button(attr, obj, additional_signal),
792
+ )
793
+ if ctrl_width > 0:
794
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
795
+ current_sizer.Add(control, expansion_flag * weight, wx.EXPAND, 0)
796
+ elif data_type == bool:
797
+ # Bool type objects get a checkbox.
798
+ control = wxCheckBox(self, label=label)
799
+ control.SetValue(data)
800
+ control.SetMinSize(dip_size(self, -1, 23))
801
+
802
+
803
+ control.Bind(
804
+ wx.EVT_CHECKBOX,
805
+ on_checkbox_check(attr, control, obj, additional_signal),
806
+ )
807
+
808
+ current_sizer.Add(control, expansion_flag * weight, wx.EXPAND, 0)
809
+ elif data_type == str and data_style == "multiline":
810
+ control_sizer = StaticBoxSizer(self, wx.ID_ANY, label, wx.HORIZONTAL)
811
+ control = TextCtrl(
812
+ self,
813
+ wx.ID_ANY,
814
+ style=wx.TE_MULTILINE,
815
+ )
816
+ ctrl_width = c.get("width", 0)
817
+ if ctrl_width > 0:
818
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
819
+ control.SetValue(str(data))
820
+ if ctrl_width > 0:
821
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
822
+ if ctrl_width > 0:
823
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
824
+ control_sizer.Add(control, 1, wx.EXPAND, 0)
825
+ current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
826
+
827
+
828
+ control.SetActionRoutine(
829
+ on_generic_multi(attr, control, obj, data_type, additional_signal)
830
+ )
831
+
832
+ elif data_type == str and data_style == "file":
833
+ control_sizer = StaticBoxSizer(self, wx.ID_ANY, label, wx.HORIZONTAL)
834
+ control = TextCtrl(
835
+ self,
836
+ wx.ID_ANY,
837
+ style=wx.TE_PROCESS_ENTER,
838
+ )
839
+ control_btn = wxButton(self, wx.ID_ANY, "...")
840
+
841
+ def set_file(filename: str):
842
+ # if not filename:
843
+ # filename = _("No File")
844
+ control.SetValue(filename)
845
+
846
+
847
+ control.SetActionRoutine(
848
+ on_file_text(attr, control, obj, data_type, additional_signal)
849
+ )
850
+
851
+ ctrl_width = c.get("width", 0)
852
+ if ctrl_width > 0:
853
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
854
+ control.SetValue(str(data))
855
+ if ctrl_width > 0:
856
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
857
+ if ctrl_width > 0:
858
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
859
+ control_sizer.Add(control, 1, wx.EXPAND, 0)
860
+ control_sizer.Add(control_btn, 0, wx.EXPAND, 0)
861
+ control_btn.Bind(
862
+ wx.EVT_BUTTON,
863
+ on_button_filename(
864
+ attr, control, obj, c.get("wildcard", "*"), additional_signal
865
+ ),
866
+ )
867
+ current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
868
+ elif data_type in (int, float) and data_style == "slider":
869
+ if label != "":
870
+ control_sizer = StaticBoxSizer(
871
+ self, wx.ID_ANY, label, wx.HORIZONTAL
872
+ )
873
+ else:
874
+ control_sizer = wx.BoxSizer(wx.HORIZONTAL)
875
+ minvalue = c.get("min", 0)
876
+ maxvalue = c.get("max", 0)
877
+ if data_type == float:
878
+ value = float(data)
879
+ elif data_type == int:
880
+ value = int(data)
881
+ else:
882
+ value = int(data)
883
+ control = wx.Slider(
884
+ self,
885
+ wx.ID_ANY,
886
+ value=value,
887
+ minValue=minvalue,
888
+ maxValue=maxvalue,
889
+ style=wx.SL_HORIZONTAL | wx.SL_VALUE_LABEL,
890
+ )
891
+
892
+
893
+ if ctrl_width > 0:
894
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
895
+ control_sizer.Add(control, 1, wx.EXPAND, 0)
896
+ control.Bind(
897
+ wx.EVT_SLIDER,
898
+ on_slider(attr, control, obj, data_type, additional_signal),
899
+ )
900
+ current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
901
+ elif data_type in (str, int, float) and data_style == "combo":
902
+ if label != "":
903
+ control_sizer = StaticBoxSizer(
904
+ self, wx.ID_ANY, label, wx.HORIZONTAL
905
+ )
906
+ else:
907
+ control_sizer = wx.BoxSizer(wx.HORIZONTAL)
908
+ choice_list = list(map(str, c.get("choices", [c.get("default")])))
909
+ control = wxComboBox(
910
+ self,
911
+ wx.ID_ANY,
912
+ choices=choice_list,
913
+ style=wx.CB_DROPDOWN | wx.CB_READONLY,
914
+ )
915
+ if data is not None:
916
+ if data_type == str:
917
+ control.SetValue(str(data))
918
+ else:
919
+ least = None
920
+ for entry in choice_list:
921
+ if least is None:
922
+ least = entry
923
+ else:
924
+ if abs(data_type(entry) - data) < abs(
925
+ data_type(least) - data
926
+ ):
927
+ least = entry
928
+ if least is not None:
929
+ control.SetValue(least)
930
+
931
+
932
+ if ctrl_width > 0:
933
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
934
+ control_sizer.Add(control, 1, wx.ALIGN_CENTER_VERTICAL, 0)
935
+ control.Bind(
936
+ wx.EVT_COMBOBOX,
937
+ on_combo_text(attr, control, obj, data_type, additional_signal),
938
+ )
939
+ current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
940
+ elif data_type in (str, int) and data_style == "radio":
941
+ control_sizer = wx.BoxSizer(wx.HORIZONTAL)
942
+ choice_list = list(map(str, c.get("choices", [c.get("default")])))
943
+ control = wxRadioBox(
944
+ self,
945
+ wx.ID_ANY,
946
+ label,
947
+ choices=choice_list,
948
+ majorDimension=3,
949
+ style=wx.RA_SPECIFY_COLS, # wx.RA_SPECIFY_ROWS,
950
+ )
951
+ if data is not None:
952
+ if data_type == str:
953
+ control.SetSelection(0)
954
+ for idx, _c in enumerate(choice_list):
955
+ if _c == data:
956
+ control.SetSelection(idx)
957
+ else:
958
+ control.SetSelection(int(data))
959
+
960
+
961
+ if ctrl_width > 0:
962
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
963
+ control_sizer.Add(control, 1, wx.ALIGN_CENTER_VERTICAL, 0)
964
+ control.Bind(
965
+ wx.EVT_RADIOBOX,
966
+ on_radio_select(attr, control, obj, data_type, additional_signal),
967
+ )
968
+ current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
969
+ elif data_type in (int, str) and data_style == "option":
970
+ control_sizer = wx.BoxSizer(wx.HORIZONTAL)
971
+ display_list = list(map(str, c.get("display")))
972
+ choice_list = list(map(str, c.get("choices", [c.get("default")])))
973
+ try:
974
+ index = choice_list.index(str(data))
975
+ except ValueError:
976
+ # Value was not in list.
977
+ index = 0
978
+ if data is None:
979
+ data = c.get("default")
980
+ display_list.insert(0, str(data))
981
+ choice_list.insert(0, str(data))
982
+ cb_style = wx.CB_DROPDOWN | wx.CB_READONLY
983
+ control = wxComboBox(
984
+ self,
985
+ wx.ID_ANY,
986
+ choices=display_list,
987
+ style=cb_style,
988
+ )
989
+ control.SetSelection(index)
990
+
991
+ # Constrain the width
992
+ testsize = control.GetBestSize()
993
+ control.SetMaxSize(dip_size(self, testsize[0] + 30, -1))
994
+ # print ("Display: %s" % display_list)
995
+ # print ("Choices: %s" % choice_list)
996
+ # print ("To set: %s" % str(data))
997
+
998
+
999
+ if label != "":
1000
+ # Try to center it vertically to the controls extent
1001
+ wd, ht = control.GetSize()
1002
+ label_text = wxStaticText(self, id=wx.ID_ANY, label=label + " ")
1003
+ # label_text.SetMinSize(dip_size(self, -1, ht))
1004
+ control_sizer.Add(label_text, 0, wx.ALIGN_CENTER_VERTICAL, 0)
1005
+ control_sizer.Add(control, 1, wx.ALIGN_CENTER_VERTICAL, 0)
1006
+ control.Bind(
1007
+ wx.EVT_COMBOBOX,
1008
+ on_combosmall_option(
1009
+ attr, control, obj, data_type, additional_signal, choice_list
1010
+ ),
1011
+ )
1012
+ current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
1013
+ elif data_type in (str, int, float) and data_style == "combosmall":
1014
+ control_sizer = wx.BoxSizer(wx.HORIZONTAL)
1015
+ exclusive = c.get("exclusive", True)
1016
+ cb_style = wx.CB_DROPDOWN | wx.CB_READONLY if exclusive else wx.CB_DROPDOWN
1017
+
1018
+ choice_list = list(map(str, c.get("choices", [c.get("default")])))
1019
+ control = wxComboBox(
1020
+ self,
1021
+ wx.ID_ANY,
1022
+ choices=choice_list,
1023
+ style = cb_style,
1024
+ )
1025
+ # Constrain the width
1026
+ testsize = control.GetBestSize()
1027
+ control.SetMaxSize(dip_size(self, testsize[0] + 30, -1))
1028
+ # print ("Choices: %s" % choice_list)
1029
+ # print ("To set: %s" % str(data))
1030
+ if data is not None:
1031
+ if data_type == str:
1032
+ control.SetValue(str(data))
1033
+ else:
1034
+ least = None
1035
+ for entry in choice_list:
1036
+ if least is None:
1037
+ least = entry
1038
+ else:
1039
+ if abs(data_type(entry) - data) < abs(
1040
+ data_type(least) - data
1041
+ ):
1042
+ least = entry
1043
+ if least is not None:
1044
+ control.SetValue(least)
1045
+
1046
+
1047
+ if label != "":
1048
+ # Try to center it vertically to the controls extent
1049
+ wd, ht = control.GetSize()
1050
+ label_text = wxStaticText(self, id=wx.ID_ANY, label=label + " ")
1051
+ # label_text.SetMinSize(dip_size(self, -1, ht))
1052
+ control_sizer.Add(label_text, 0, wx.ALIGN_CENTER_VERTICAL, 0)
1053
+ if ctrl_width > 0:
1054
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
1055
+ control_sizer.Add(control, 1, wx.ALIGN_CENTER_VERTICAL, 0)
1056
+ control.Bind(
1057
+ wx.EVT_COMBOBOX,
1058
+ on_combosmall_text(
1059
+ attr, control, obj, data_type, additional_signal
1060
+ ),
1061
+ )
1062
+ if not exclusive:
1063
+ control.Bind(
1064
+ wx.EVT_TEXT,
1065
+ on_combosmall_text(
1066
+ attr, control, obj, data_type, additional_signal
1067
+ ),
1068
+ )
1069
+
1070
+ current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
1071
+ elif data_type == int and data_style == "binary":
1072
+ mask = c.get("mask")
1073
+
1074
+ # get default value
1075
+ mask_bits = 0
1076
+ if mask is not None and hasattr(obj, mask):
1077
+ mask_bits = getattr(obj, mask)
1078
+
1079
+ if label != "":
1080
+ control_sizer = StaticBoxSizer(
1081
+ self, wx.ID_ANY, label, wx.HORIZONTAL
1082
+ )
1083
+ else:
1084
+ control_sizer = wx.BoxSizer(wx.HORIZONTAL)
1085
+
1086
+
1087
+ bit_sizer = wx.BoxSizer(wx.VERTICAL)
1088
+ label_text = wxStaticText(
1089
+ self, wx.ID_ANY, "", style=wx.ALIGN_CENTRE_HORIZONTAL
1090
+ )
1091
+ bit_sizer.Add(label_text, 0, wx.EXPAND, 0)
1092
+ if mask is not None:
1093
+ label_text = wxStaticText(
1094
+ self,
1095
+ wx.ID_ANY,
1096
+ _("mask") + " ",
1097
+ style=wx.ALIGN_CENTRE_HORIZONTAL,
1098
+ )
1099
+ bit_sizer.Add(label_text, 0, wx.EXPAND, 0)
1100
+ label_text = wxStaticText(
1101
+ self, wx.ID_ANY, _("value") + " ", style=wx.ALIGN_CENTRE_HORIZONTAL
1102
+ )
1103
+ bit_sizer.Add(label_text, 0, wx.EXPAND, 0)
1104
+ control_sizer.Add(bit_sizer, 0, wx.EXPAND, 0)
1105
+
1106
+ bits = c.get("bits", 8)
1107
+ for b in range(bits):
1108
+ # Label
1109
+ bit_sizer = wx.BoxSizer(wx.VERTICAL)
1110
+ label_text = wxStaticText(
1111
+ self, wx.ID_ANY, str(b), style=wx.ALIGN_CENTRE_HORIZONTAL
1112
+ )
1113
+ bit_sizer.Add(label_text, 0, wx.EXPAND, 0)
1114
+
1115
+ # value bit
1116
+ control = wxCheckBox(self)
1117
+ control.SetValue(bool((data >> b) & 1))
1118
+ if mask:
1119
+ control.Enable(bool((mask_bits >> b) & 1))
1120
+ control.Bind(
1121
+ wx.EVT_CHECKBOX,
1122
+ on_checkbox_check(attr, control, obj, b, additional_signal),
1123
+ )
1124
+
1125
+ # mask bit
1126
+ if mask:
1127
+ mask_ctrl = wxCheckBox(self)
1128
+ mask_ctrl.SetValue(bool((mask_bits >> b) & 1))
1129
+ mask_ctrl.Bind(
1130
+ wx.EVT_CHECKBOX,
1131
+ on_checkbox_bitcheck(
1132
+ mask,
1133
+ mask_ctrl,
1134
+ obj,
1135
+ b,
1136
+ additional_signal,
1137
+ enable_ctrl=control,
1138
+ ),
1139
+ )
1140
+ bit_sizer.Add(mask_ctrl, 0, wx.EXPAND, 0)
1141
+
1142
+ bit_sizer.Add(control, 0, wx.EXPAND, 0)
1143
+ control_sizer.Add(bit_sizer, 0, wx.EXPAND, 0)
1144
+
1145
+ current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
1146
+ elif data_type == str and data_style == "color":
1147
+ # str data_type with style "color" objects do get a button with the background.
1148
+ control_sizer = wx.BoxSizer(wx.HORIZONTAL)
1149
+ control = wxButton(self, -1)
1150
+
1151
+ def set_color(ctrl, color: Color):
1152
+ ctrl.SetLabel(str(color.hex))
1153
+ ctrl.SetBackgroundColour(wx.Colour(swizzlecolor(color)))
1154
+ if Color.distance(color, Color("black")) > Color.distance(
1155
+ color, Color("white")
1156
+ ):
1157
+ ctrl.SetForegroundColour(wx.BLACK)
1158
+ else:
1159
+ ctrl.SetForegroundColour(wx.WHITE)
1160
+ ctrl.color = color
1161
+
1162
+ datastr = data
1163
+ data = Color(datastr)
1164
+ set_color(control, data)
1165
+ if ctrl_width > 0:
1166
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
1167
+ control_sizer.Add(control, 0, wx.EXPAND, 0)
1168
+ color_info = wxStaticText(self, wx.ID_ANY, label)
1169
+ control_sizer.Add(color_info, 1, wx.ALIGN_CENTER_VERTICAL)
1170
+
1171
+ control.Bind(
1172
+ wx.EVT_BUTTON,
1173
+ on_button_color(attr, control, obj, additional_signal),
1174
+ )
1175
+ if ctrl_width > 0:
1176
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
1177
+ current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
1178
+ elif data_type == list and data_style == "chart":
1179
+ chart = EditableListCtrl(
1180
+ self,
1181
+ wx.ID_ANY,
1182
+ style=wx.LC_HRULES | wx.LC_REPORT | wx.LC_VRULES | wx.LC_SINGLE_SEL,
1183
+ context=self.context, list_name=f"list_chart_{attr}",
1184
+ )
1185
+ l_columns = c.get("columns", [])
1186
+
1187
+ def fill_ctrl(ctrl, local_obj, param, columns):
1188
+ data = getattr(local_obj, param)
1189
+ ctrl.ClearAll()
1190
+ for column in columns:
1191
+ wd = column.get("width", 150)
1192
+ if wd < 0:
1193
+ wd = ctrl.Size[0] - 10
1194
+ ctrl.AppendColumn(
1195
+ column.get("label", ""),
1196
+ format=wx.LIST_FORMAT_LEFT,
1197
+ width=wd,
1198
+ )
1199
+ ctrl.resize_columns()
1200
+ for dataline in data:
1201
+ if isinstance(dataline, dict):
1202
+ for kk in dataline.keys():
1203
+ key = kk
1204
+ break
1205
+ row_id = ctrl.InsertItem(
1206
+ ctrl.GetItemCount(),
1207
+ dataline.get(key, 0),
1208
+ )
1209
+ for column_id, column in enumerate(columns):
1210
+ c_attr = column.get("attr")
1211
+ ctrl.SetItem(
1212
+ row_id, column_id, str(dataline.get(c_attr, ""))
1213
+ )
1214
+ elif isinstance(dataline, str):
1215
+ row_id = ctrl.InsertItem(
1216
+ ctrl.GetItemCount(),
1217
+ dataline,
1218
+ )
1219
+ elif isinstance(dataline, (list, tuple)):
1220
+ row_id = ctrl.InsertItem(
1221
+ ctrl.GetItemCount(),
1222
+ dataline[0],
1223
+ )
1224
+ for column_id, column in enumerate(columns):
1225
+ # c_attr = column.get("attr")
1226
+ ctrl.SetItem(row_id, column_id, dataline[column_id])
1227
+
1228
+ fill_ctrl(chart, obj, attr, l_columns)
1229
+
1230
+
1231
+ chart.Bind(
1232
+ wx.EVT_LIST_BEGIN_LABEL_EDIT,
1233
+ on_chart_start(l_columns, attr, chart, obj),
1234
+ )
1235
+
1236
+
1237
+ chart.Bind(
1238
+ wx.EVT_LIST_END_LABEL_EDIT,
1239
+ on_chart_stop(l_columns, attr, chart, obj),
1240
+ )
1241
+
1242
+ allow_deletion = c.get("allow_deletion", False)
1243
+ allow_duplication = c.get("allow_duplication", False)
1244
+
1245
+ default = c.get("default", [])
1246
+ # chart.Bind(
1247
+ # wx.EVT_LIST_ITEM_RIGHT_CLICK,
1248
+ # on_chart_contextmenu(
1249
+ # l_columns,
1250
+ # attr,
1251
+ # chart,
1252
+ # obj,
1253
+ # allow_deletion,
1254
+ # allow_duplication,
1255
+ # default,
1256
+ # ),
1257
+ # )
1258
+ # chart.Bind(
1259
+ # wx.EVT_LIST_COL_RIGHT_CLICK,
1260
+ # on_chart_contextmenu(
1261
+ # l_columns,
1262
+ # attr,
1263
+ # chart,
1264
+ # obj,
1265
+ # allow_deletion,
1266
+ # allow_duplication,
1267
+ # default,
1268
+ # ),
1269
+ # )
1270
+ chart.Bind(
1271
+ wx.EVT_RIGHT_DOWN,
1272
+ on_chart_contextmenu(
1273
+ l_columns,
1274
+ attr,
1275
+ chart,
1276
+ obj,
1277
+ allow_deletion,
1278
+ allow_duplication,
1279
+ default,
1280
+ ),
1281
+ )
1282
+ current_sizer.Add(chart, expansion_flag * weight, wx.EXPAND, 0)
1283
+ elif data_type in (str, int, float):
1284
+ # str, int, and float type objects get a TextCtrl setter.
1285
+ if label != "" and data_style != "flat":
1286
+ control_sizer = StaticBoxSizer(
1287
+ self, wx.ID_ANY, label, wx.HORIZONTAL
1288
+ )
1289
+ else:
1290
+ control_sizer = wx.BoxSizer(wx.HORIZONTAL)
1291
+ if label != "":
1292
+ label_text = wxStaticText(self, id=wx.ID_ANY, label=label)
1293
+ control_sizer.Add(label_text, 0, wx.ALIGN_CENTER_VERTICAL, 0)
1294
+
1295
+ if data_type == int:
1296
+ check_flag = "int"
1297
+ limit = True
1298
+ lower_range = c.get("lower", None)
1299
+ upper_range = c.get("upper", None)
1300
+ elif data_type == float:
1301
+ check_flag = "float"
1302
+ limit = True
1303
+ lower_range = c.get("lower", None)
1304
+ upper_range = c.get("upper", None)
1305
+ else:
1306
+ check_flag = ""
1307
+ limit = False
1308
+ lower_range = None
1309
+ upper_range = None
1310
+
1311
+ control = TextCtrl(
1312
+ self,
1313
+ wx.ID_ANY,
1314
+ style=wx.TE_PROCESS_ENTER,
1315
+ limited=limit,
1316
+ check=check_flag,
1317
+ )
1318
+ if lower_range is not None:
1319
+ control.lower_limit = lower_range
1320
+ control.lower_limit_err = lower_range
1321
+ if upper_range is not None:
1322
+ control.upper_limit = upper_range
1323
+ control.upper_limit_err = upper_range
1324
+ ctrl_width = c.get("width", 0)
1325
+ if ctrl_width > 0:
1326
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
1327
+ control.SetValue(str(data))
1328
+ if ctrl_width > 0:
1329
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
1330
+ control_sizer.Add(control, 1, wx.EXPAND, 0)
1331
+
1332
+
1333
+ control.SetActionRoutine(
1334
+ on_generic_text(attr, control, obj, data_type, additional_signal)
1335
+ )
1336
+ current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
1337
+ elif data_type == Length:
1338
+ # Length type is a TextCtrl with special checks
1339
+ if label != "":
1340
+ control_sizer = StaticBoxSizer(
1341
+ self, wx.ID_ANY, label, wx.HORIZONTAL
1342
+ )
1343
+ else:
1344
+ control_sizer = wx.BoxSizer(wx.HORIZONTAL)
1345
+ nonzero = c.get("nonzero", False)
1346
+ if nonzero is None or not isinstance(nonzero, bool):
1347
+ nonzero = False
1348
+ control = TextCtrl(
1349
+ self,
1350
+ wx.ID_ANY,
1351
+ style=wx.TE_PROCESS_ENTER,
1352
+ limited=True,
1353
+ check="length",
1354
+ nonzero=nonzero,
1355
+ )
1356
+ if isinstance(data, Length):
1357
+ if not data._preferred_units:
1358
+ data._preferred_units = "mm"
1359
+ if not data._digits:
1360
+ if data._preferred_units in ("mm", "cm", "in", "inch"):
1361
+ data._digits = 4
1362
+ display_value = data.preferred_length
1363
+ else:
1364
+ display_value = str(data)
1365
+ control.SetValue(display_value)
1366
+ if ctrl_width > 0:
1367
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
1368
+ control_sizer.Add(control, 1, wx.EXPAND, 0)
1369
+
1370
+ control.SetActionRoutine(
1371
+ on_length_text(attr, control, obj, data_type, additional_signal)
1372
+ )
1373
+ current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
1374
+ elif data_type == Angle:
1375
+ # Angle type is a TextCtrl with special checks
1376
+ if label != "":
1377
+ control_sizer = StaticBoxSizer(
1378
+ self, wx.ID_ANY, label, wx.HORIZONTAL
1379
+ )
1380
+ else:
1381
+ control_sizer = wx.BoxSizer(wx.HORIZONTAL)
1382
+ control = TextCtrl(
1383
+ self,
1384
+ wx.ID_ANY,
1385
+ style=wx.TE_PROCESS_ENTER,
1386
+ check="angle",
1387
+ limited=True,
1388
+ )
1389
+ control.SetValue(str(data))
1390
+ if ctrl_width > 0:
1391
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
1392
+ control_sizer.Add(control, 1, wx.EXPAND, 0)
1393
+
1394
+ control.SetActionRoutine(
1395
+ on_angle_text(attr, control, obj, data_type, additional_signal)
1396
+ )
1397
+ current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
1398
+ elif data_type == Color:
1399
+ # Color data_type objects are get a button with the background.
1400
+ if label != "":
1401
+ control_sizer = StaticBoxSizer(
1402
+ self, wx.ID_ANY, label, wx.HORIZONTAL
1403
+ )
1404
+ else:
1405
+ control_sizer = wx.BoxSizer(wx.HORIZONTAL)
1406
+ control = wxButton(self, -1)
1407
+
1408
+ def set_color(ctrl, color: Color):
1409
+ ctrl.SetLabel(str(color.hex))
1410
+ ctrl.SetBackgroundColour(wx.Colour(swizzlecolor(color)))
1411
+ if Color.distance(color, Color("black")) > Color.distance(
1412
+ color, Color("white")
1413
+ ):
1414
+ ctrl.SetForegroundColour(wx.BLACK)
1415
+ else:
1416
+ ctrl.SetForegroundColour(wx.WHITE)
1417
+ ctrl.color = color
1418
+
1419
+ set_color(control, data)
1420
+ if ctrl_width > 0:
1421
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
1422
+ control_sizer.Add(control, 0, wx.EXPAND, 0)
1423
+
1424
+ control.Bind(
1425
+ wx.EVT_BUTTON,
1426
+ on_button_color(attr, control, obj, additional_signal),
1427
+ )
1428
+ if ctrl_width > 0:
1429
+ control.SetMaxSize(dip_size(self, ctrl_width, -1))
1430
+ current_sizer.Add(control_sizer, expansion_flag * weight, wx.EXPAND, 0)
1431
+ else:
1432
+ # Requires a registered data_type
1433
+ continue
1434
+
1435
+ if trailer and control_sizer:
1436
+ trailer_text = wxStaticText(self, id=wx.ID_ANY, label=f" {trailer}")
1437
+ control_sizer.Add(trailer_text, 0, wx.ALIGN_CENTER_VERTICAL, 0)
1438
+
1439
+ if control is None:
1440
+ continue # We're binary or some other style without a specific control.
1441
+
1442
+ # Get enabled value
1443
+ try:
1444
+ enabled = c["enabled"]
1445
+ control.Enable(enabled)
1446
+ except KeyError:
1447
+ # Listen to establish whether this control should be enabled based on another control's value.
1448
+ try:
1449
+ conditional = c["conditional"]
1450
+ if len(conditional) == 2:
1451
+ c_obj, c_attr = conditional
1452
+ enabled = bool(getattr(c_obj, c_attr))
1453
+ c_equals = True
1454
+ control.Enable(enabled)
1455
+ elif len(conditional) == 3:
1456
+ c_obj, c_attr, c_equals = conditional
1457
+ enabled = bool(getattr(c_obj, c_attr) == c_equals)
1458
+ control.Enable(enabled)
1459
+ elif len(conditional) == 4:
1460
+ c_obj, c_attr, c_from, c_to = conditional
1461
+ enabled = bool(c_from <= getattr(c_obj, c_attr) <= c_to)
1462
+ c_equals = (c_from, c_to)
1463
+ control.Enable(enabled)
1464
+
1465
+ def on_enable_listener(param, ctrl, obj, eqs):
1466
+ def listen(origin, value, target=None):
1467
+ try:
1468
+ if isinstance(eqs, (list, tuple)):
1469
+ enable = bool(
1470
+ eqs[0] <= getattr(obj, param) <= eqs[1]
1471
+ )
1472
+ else:
1473
+ enable = bool(getattr(obj, param) == eqs)
1474
+ ctrl.Enable(enable)
1475
+ except (IndexError, RuntimeError):
1476
+ pass
1477
+
1478
+ return listen
1479
+
1480
+ listener = on_enable_listener(c_attr, control, c_obj, c_equals)
1481
+ self.listeners.append((c_attr, listener, c_obj))
1482
+ context.listen(c_attr, listener)
1483
+ except KeyError:
1484
+ pass
1485
+
1486
+ # Now we listen to 'ourselves' as well to learn about changes somewhere else...
1487
+ def on_update_listener(param, ctrl, dtype, dstyle, choicelist, sourceobj):
1488
+ def listen_to_myself(origin, value, target=None):
1489
+ if self.context.kernel.is_shutdown:
1490
+ return
1491
+
1492
+ if target is None or target is not sourceobj:
1493
+ # print (f"Signal for {param}={value}, but no target given or different to source")
1494
+ return
1495
+ update_needed = False
1496
+ # print (f"attr={param}, origin={origin}, value={value}, datatype={dtype}, datastyle={dstyle}")
1497
+ data = None
1498
+ if value is not None:
1499
+ try:
1500
+ data = dtype(value)
1501
+ except ValueError:
1502
+ pass
1503
+ if data is None:
1504
+ try:
1505
+ data = c["default"]
1506
+ except KeyError:
1507
+ pass
1508
+ if data is None:
1509
+ # print (f"Invalid data based on {value}, exiting")
1510
+ return
1511
+ # Let's just access the ctrl to see whether it has been already
1512
+ # destroyed (as we are in the midst of a shutdown)
1513
+ try:
1514
+ dummy = hasattr(ctrl, "GetValue")
1515
+ except RuntimeError:
1516
+ return
1517
+ if dtype == bool:
1518
+ # Bool type objects get a checkbox.
1519
+ if ctrl.GetValue() != data:
1520
+ ctrl.SetValue(data)
1521
+ elif dtype == str and dstyle == "file":
1522
+ if ctrl.GetValue() != data:
1523
+ ctrl.SetValue(data)
1524
+ elif dtype in (int, float) and dstyle == "slider":
1525
+ if ctrl.GetValue() != data:
1526
+ ctrl.SetValue(data)
1527
+ elif dtype in (str, int, float) and dstyle == "combo":
1528
+ if dtype == str:
1529
+ ctrl.SetValue(str(data))
1530
+ else:
1531
+ least = None
1532
+ for entry in choicelist:
1533
+ if least is None:
1534
+ least = entry
1535
+ else:
1536
+ if abs(dtype(entry) - data) < abs(
1537
+ dtype(least) - data
1538
+ ):
1539
+ least = entry
1540
+ if least is not None:
1541
+ ctrl.SetValue(least)
1542
+ elif dtype in (str, int, float) and dstyle == "combosmall":
1543
+ if dtype == str:
1544
+ ctrl.SetValue(str(data))
1545
+ else:
1546
+ least = None
1547
+ for entry in choicelist:
1548
+ if least is None:
1549
+ least = entry
1550
+ else:
1551
+ if abs(dtype(entry) - data) < abs(
1552
+ dtype(least) - data
1553
+ ):
1554
+ least = entry
1555
+ if least is not None:
1556
+ ctrl.SetValue(least)
1557
+ elif dtype == int and dstyle == "binary":
1558
+ pass # not supported...
1559
+ elif (dtype == str and dstyle == "color") or dtype == Color:
1560
+ # Color dtype objects are a button with the background set to the color
1561
+ def set_color(color: Color):
1562
+ ctrl.SetLabel(str(color.hex))
1563
+ ctrl.SetBackgroundColour(wx.Colour(swizzlecolor(color)))
1564
+ if Color.distance(color, Color("black")) > Color.distance(
1565
+ color, Color("white")
1566
+ ):
1567
+ ctrl.SetForegroundColour(wx.BLACK)
1568
+ else:
1569
+ ctrl.SetForegroundColour(wx.WHITE)
1570
+ ctrl.color = color
1571
+
1572
+ if isinstance(data, str):
1573
+ # print ("Needed to change type")
1574
+ data = Color(data)
1575
+ # print (f"Will set color to {data}")
1576
+ set_color(data)
1577
+ elif dtype in (str, int, float):
1578
+ if hasattr(ctrl, "GetValue"):
1579
+ try:
1580
+ if dtype(ctrl.GetValue()) != data:
1581
+ update_needed = True
1582
+ except ValueError:
1583
+ update_needed = True
1584
+ if update_needed:
1585
+ ctrl.SetValue(str(data))
1586
+ elif dtype == Length:
1587
+ if float(data) != float(Length(ctrl.GetValue())):
1588
+ update_needed = True
1589
+ if update_needed:
1590
+ if not isinstance(data, str):
1591
+ data = Length(data).length_mm
1592
+ ctrl.SetValue(str(data))
1593
+ elif dtype == Angle:
1594
+ if ctrl.GetValue() != str(data):
1595
+ ctrl.SetValue(str(data))
1596
+
1597
+ return listen_to_myself
1598
+
1599
+ if wants_listener:
1600
+ update_listener = on_update_listener(
1601
+ attr, control, data_type, data_style, choice_list, obj
1602
+ )
1603
+ self.listeners.append((attr, update_listener, obj))
1604
+ context.listen(attr, update_listener)
1605
+ tip = c.get("tip")
1606
+ if tip and not context.root.disable_tool_tips:
1607
+ # Set the tool tip if 'tip' is available
1608
+ control.SetToolTip(tip)
1609
+ _help = c.get("help")
1610
+ if _help and hasattr(control, "SetHelpText"):
1611
+ control.SetHelpText(_help)
1612
+ last_page = this_page
1613
+ last_section = this_section
1614
+ last_subsection = this_subsection
1615
+
1616
+ self.SetSizer(sizer_very_main)
1617
+ sizer_very_main.Fit(self)
1618
+ self.Bind(wx.EVT_CLOSE, self.on_close)
1619
+ # Make sure stuff gets scrolled if necessary by default
1620
+ if scrolling:
1621
+ self.SetupScrolling()
1622
+
1623
+ def on_close(self, event):
1624
+ # We should not need this, but better safe than sorry
1625
+ event.Skip()
1626
+ self.pane_hide()
1627
+
1628
+ @staticmethod
1629
+ def unsorted_label(original):
1630
+ # Special sort key just to sort stuff - we fix the preceeding "_sortcriteria_Correct label"
1631
+ result = original
1632
+ if result.startswith("_"):
1633
+ idx = result.find("_", 1)
1634
+ if idx >= 0:
1635
+ result = result[idx + 1 :]
1636
+ return result
1637
+
1638
+ def reload(self):
1639
+ for attr, listener, obj in self.listeners:
1640
+ try:
1641
+ value = getattr(obj, attr)
1642
+ except AttributeError as e:
1643
+ # print(f"error: {e}")
1644
+ continue
1645
+ listener("internal", value, obj)
1646
+
1647
+ def module_close(self, *args, **kwargs):
1648
+ self.pane_hide()
1649
+
1650
+ def pane_hide(self):
1651
+ # print (f"hide called: {len(self.listeners)}")
1652
+ if len(self.listeners):
1653
+ for attr, listener, obj in self.listeners:
1654
+ self.context.unlisten(attr, listener)
1655
+ del listener
1656
+ self.listeners.clear()
1657
+
1658
+ def pane_show(self):
1659
+ # print ("show called")
1660
+ # if len(self.listeners) == 0:
1661
+ # print ("..but no one cares")
1662
+ pass