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

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