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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. meerk40t/__init__.py +1 -1
  2. meerk40t/balormk/balor_params.py +167 -167
  3. meerk40t/balormk/clone_loader.py +457 -457
  4. meerk40t/balormk/controller.py +1566 -1512
  5. meerk40t/balormk/cylindermod.py +64 -0
  6. meerk40t/balormk/device.py +966 -1959
  7. meerk40t/balormk/driver.py +778 -591
  8. meerk40t/balormk/galvo_commands.py +1194 -0
  9. meerk40t/balormk/gui/balorconfig.py +237 -111
  10. meerk40t/balormk/gui/balorcontroller.py +191 -184
  11. meerk40t/balormk/gui/baloroperationproperties.py +116 -115
  12. meerk40t/balormk/gui/corscene.py +845 -0
  13. meerk40t/balormk/gui/gui.py +179 -147
  14. meerk40t/balormk/livelightjob.py +466 -382
  15. meerk40t/balormk/mock_connection.py +131 -109
  16. meerk40t/balormk/plugin.py +133 -135
  17. meerk40t/balormk/usb_connection.py +306 -301
  18. meerk40t/camera/__init__.py +1 -1
  19. meerk40t/camera/camera.py +514 -397
  20. meerk40t/camera/gui/camerapanel.py +1241 -1095
  21. meerk40t/camera/gui/gui.py +58 -58
  22. meerk40t/camera/plugin.py +441 -399
  23. meerk40t/ch341/__init__.py +27 -27
  24. meerk40t/ch341/ch341device.py +628 -628
  25. meerk40t/ch341/libusb.py +595 -589
  26. meerk40t/ch341/mock.py +171 -171
  27. meerk40t/ch341/windriver.py +157 -157
  28. meerk40t/constants.py +13 -0
  29. meerk40t/core/__init__.py +1 -1
  30. meerk40t/core/bindalias.py +550 -539
  31. meerk40t/core/core.py +47 -47
  32. meerk40t/core/cutcode/cubiccut.py +73 -73
  33. meerk40t/core/cutcode/cutcode.py +315 -312
  34. meerk40t/core/cutcode/cutgroup.py +141 -137
  35. meerk40t/core/cutcode/cutobject.py +192 -185
  36. meerk40t/core/cutcode/dwellcut.py +37 -37
  37. meerk40t/core/cutcode/gotocut.py +29 -29
  38. meerk40t/core/cutcode/homecut.py +29 -29
  39. meerk40t/core/cutcode/inputcut.py +34 -34
  40. meerk40t/core/cutcode/linecut.py +33 -33
  41. meerk40t/core/cutcode/outputcut.py +34 -34
  42. meerk40t/core/cutcode/plotcut.py +335 -335
  43. meerk40t/core/cutcode/quadcut.py +61 -61
  44. meerk40t/core/cutcode/rastercut.py +168 -148
  45. meerk40t/core/cutcode/waitcut.py +34 -34
  46. meerk40t/core/cutplan.py +1843 -1316
  47. meerk40t/core/drivers.py +330 -329
  48. meerk40t/core/elements/align.py +801 -669
  49. meerk40t/core/elements/branches.py +1858 -1507
  50. meerk40t/core/elements/clipboard.py +229 -219
  51. meerk40t/core/elements/element_treeops.py +4595 -2837
  52. meerk40t/core/elements/element_types.py +125 -105
  53. meerk40t/core/elements/elements.py +4315 -3617
  54. meerk40t/core/elements/files.py +117 -64
  55. meerk40t/core/elements/geometry.py +473 -224
  56. meerk40t/core/elements/grid.py +467 -316
  57. meerk40t/core/elements/materials.py +158 -94
  58. meerk40t/core/elements/notes.py +50 -38
  59. meerk40t/core/elements/offset_clpr.py +934 -912
  60. meerk40t/core/elements/offset_mk.py +963 -955
  61. meerk40t/core/elements/penbox.py +339 -267
  62. meerk40t/core/elements/placements.py +300 -83
  63. meerk40t/core/elements/render.py +785 -687
  64. meerk40t/core/elements/shapes.py +2618 -2092
  65. meerk40t/core/elements/testcases.py +105 -0
  66. meerk40t/core/elements/trace.py +651 -563
  67. meerk40t/core/elements/tree_commands.py +415 -409
  68. meerk40t/core/elements/undo_redo.py +116 -58
  69. meerk40t/core/elements/wordlist.py +319 -200
  70. meerk40t/core/exceptions.py +9 -9
  71. meerk40t/core/laserjob.py +220 -220
  72. meerk40t/core/logging.py +63 -63
  73. meerk40t/core/node/blobnode.py +83 -86
  74. meerk40t/core/node/bootstrap.py +105 -103
  75. meerk40t/core/node/branch_elems.py +40 -31
  76. meerk40t/core/node/branch_ops.py +45 -38
  77. meerk40t/core/node/branch_regmark.py +48 -41
  78. meerk40t/core/node/cutnode.py +29 -32
  79. meerk40t/core/node/effect_hatch.py +375 -257
  80. meerk40t/core/node/effect_warp.py +398 -0
  81. meerk40t/core/node/effect_wobble.py +441 -309
  82. meerk40t/core/node/elem_ellipse.py +404 -309
  83. meerk40t/core/node/elem_image.py +1082 -801
  84. meerk40t/core/node/elem_line.py +358 -292
  85. meerk40t/core/node/elem_path.py +259 -201
  86. meerk40t/core/node/elem_point.py +129 -102
  87. meerk40t/core/node/elem_polyline.py +310 -246
  88. meerk40t/core/node/elem_rect.py +376 -286
  89. meerk40t/core/node/elem_text.py +445 -418
  90. meerk40t/core/node/filenode.py +59 -40
  91. meerk40t/core/node/groupnode.py +138 -74
  92. meerk40t/core/node/image_processed.py +777 -766
  93. meerk40t/core/node/image_raster.py +156 -113
  94. meerk40t/core/node/layernode.py +31 -31
  95. meerk40t/core/node/mixins.py +135 -107
  96. meerk40t/core/node/node.py +1427 -1304
  97. meerk40t/core/node/nutils.py +117 -114
  98. meerk40t/core/node/op_cut.py +463 -335
  99. meerk40t/core/node/op_dots.py +296 -251
  100. meerk40t/core/node/op_engrave.py +414 -311
  101. meerk40t/core/node/op_image.py +755 -369
  102. meerk40t/core/node/op_raster.py +787 -522
  103. meerk40t/core/node/place_current.py +37 -40
  104. meerk40t/core/node/place_point.py +329 -126
  105. meerk40t/core/node/refnode.py +58 -47
  106. meerk40t/core/node/rootnode.py +225 -219
  107. meerk40t/core/node/util_console.py +48 -48
  108. meerk40t/core/node/util_goto.py +84 -65
  109. meerk40t/core/node/util_home.py +61 -61
  110. meerk40t/core/node/util_input.py +102 -102
  111. meerk40t/core/node/util_output.py +102 -102
  112. meerk40t/core/node/util_wait.py +65 -65
  113. meerk40t/core/parameters.py +709 -707
  114. meerk40t/core/planner.py +875 -785
  115. meerk40t/core/plotplanner.py +656 -652
  116. meerk40t/core/space.py +120 -113
  117. meerk40t/core/spoolers.py +706 -705
  118. meerk40t/core/svg_io.py +1836 -1549
  119. meerk40t/core/treeop.py +534 -445
  120. meerk40t/core/undos.py +278 -124
  121. meerk40t/core/units.py +784 -680
  122. meerk40t/core/view.py +393 -322
  123. meerk40t/core/webhelp.py +62 -62
  124. meerk40t/core/wordlist.py +513 -504
  125. meerk40t/cylinder/cylinder.py +247 -0
  126. meerk40t/cylinder/gui/cylindersettings.py +41 -0
  127. meerk40t/cylinder/gui/gui.py +24 -0
  128. meerk40t/device/__init__.py +1 -1
  129. meerk40t/device/basedevice.py +322 -123
  130. meerk40t/device/devicechoices.py +50 -0
  131. meerk40t/device/dummydevice.py +163 -128
  132. meerk40t/device/gui/defaultactions.py +618 -602
  133. meerk40t/device/gui/effectspanel.py +114 -0
  134. meerk40t/device/gui/formatterpanel.py +253 -290
  135. meerk40t/device/gui/warningpanel.py +337 -260
  136. meerk40t/device/mixins.py +13 -13
  137. meerk40t/dxf/__init__.py +1 -1
  138. meerk40t/dxf/dxf_io.py +766 -554
  139. meerk40t/dxf/plugin.py +47 -35
  140. meerk40t/external_plugins.py +79 -79
  141. meerk40t/external_plugins_build.py +28 -28
  142. meerk40t/extra/cag.py +112 -116
  143. meerk40t/extra/coolant.py +403 -0
  144. meerk40t/extra/encode_detect.py +204 -0
  145. meerk40t/extra/ezd.py +1165 -1165
  146. meerk40t/extra/hershey.py +834 -340
  147. meerk40t/extra/imageactions.py +322 -316
  148. meerk40t/extra/inkscape.py +628 -622
  149. meerk40t/extra/lbrn.py +424 -424
  150. meerk40t/extra/outerworld.py +283 -0
  151. meerk40t/extra/param_functions.py +1542 -1556
  152. meerk40t/extra/potrace.py +257 -253
  153. meerk40t/extra/serial_exchange.py +118 -0
  154. meerk40t/extra/updater.py +602 -453
  155. meerk40t/extra/vectrace.py +147 -146
  156. meerk40t/extra/winsleep.py +83 -83
  157. meerk40t/extra/xcs_reader.py +597 -0
  158. meerk40t/fill/fills.py +781 -335
  159. meerk40t/fill/patternfill.py +1061 -1061
  160. meerk40t/fill/patterns.py +614 -567
  161. meerk40t/grbl/control.py +87 -87
  162. meerk40t/grbl/controller.py +990 -903
  163. meerk40t/grbl/device.py +1084 -768
  164. meerk40t/grbl/driver.py +989 -771
  165. meerk40t/grbl/emulator.py +532 -497
  166. meerk40t/grbl/gcodejob.py +783 -767
  167. meerk40t/grbl/gui/grblconfiguration.py +373 -298
  168. meerk40t/grbl/gui/grblcontroller.py +485 -271
  169. meerk40t/grbl/gui/grblhardwareconfig.py +269 -153
  170. meerk40t/grbl/gui/grbloperationconfig.py +105 -0
  171. meerk40t/grbl/gui/gui.py +147 -116
  172. meerk40t/grbl/interpreter.py +44 -44
  173. meerk40t/grbl/loader.py +22 -22
  174. meerk40t/grbl/mock_connection.py +56 -56
  175. meerk40t/grbl/plugin.py +294 -264
  176. meerk40t/grbl/serial_connection.py +93 -88
  177. meerk40t/grbl/tcp_connection.py +81 -79
  178. meerk40t/grbl/ws_connection.py +112 -0
  179. meerk40t/gui/__init__.py +1 -1
  180. meerk40t/gui/about.py +2042 -296
  181. meerk40t/gui/alignment.py +1644 -1608
  182. meerk40t/gui/autoexec.py +199 -0
  183. meerk40t/gui/basicops.py +791 -670
  184. meerk40t/gui/bufferview.py +77 -71
  185. meerk40t/gui/busy.py +232 -133
  186. meerk40t/gui/choicepropertypanel.py +1662 -1469
  187. meerk40t/gui/consolepanel.py +706 -542
  188. meerk40t/gui/devicepanel.py +687 -581
  189. meerk40t/gui/dialogoptions.py +110 -107
  190. meerk40t/gui/executejob.py +316 -306
  191. meerk40t/gui/fonts.py +90 -90
  192. meerk40t/gui/functionwrapper.py +252 -0
  193. meerk40t/gui/gui_mixins.py +729 -0
  194. meerk40t/gui/guicolors.py +205 -182
  195. meerk40t/gui/help_assets/help_assets.py +218 -201
  196. meerk40t/gui/helper.py +154 -0
  197. meerk40t/gui/hersheymanager.py +1440 -846
  198. meerk40t/gui/icons.py +3422 -2747
  199. meerk40t/gui/imagesplitter.py +555 -508
  200. meerk40t/gui/keymap.py +354 -344
  201. meerk40t/gui/laserpanel.py +897 -806
  202. meerk40t/gui/laserrender.py +1470 -1232
  203. meerk40t/gui/lasertoolpanel.py +805 -793
  204. meerk40t/gui/magnetoptions.py +436 -0
  205. meerk40t/gui/materialmanager.py +2944 -0
  206. meerk40t/gui/materialtest.py +1722 -1694
  207. meerk40t/gui/mkdebug.py +646 -359
  208. meerk40t/gui/mwindow.py +163 -140
  209. meerk40t/gui/navigationpanels.py +2605 -2467
  210. meerk40t/gui/notes.py +143 -142
  211. meerk40t/gui/opassignment.py +414 -410
  212. meerk40t/gui/operation_info.py +310 -299
  213. meerk40t/gui/plugin.py +500 -328
  214. meerk40t/gui/position.py +714 -669
  215. meerk40t/gui/preferences.py +901 -650
  216. meerk40t/gui/propertypanels/attributes.py +1461 -1131
  217. meerk40t/gui/propertypanels/blobproperty.py +117 -114
  218. meerk40t/gui/propertypanels/consoleproperty.py +83 -80
  219. meerk40t/gui/propertypanels/gotoproperty.py +77 -0
  220. meerk40t/gui/propertypanels/groupproperties.py +223 -217
  221. meerk40t/gui/propertypanels/hatchproperty.py +489 -469
  222. meerk40t/gui/propertypanels/imageproperty.py +2244 -1384
  223. meerk40t/gui/propertypanels/inputproperty.py +59 -58
  224. meerk40t/gui/propertypanels/opbranchproperties.py +82 -80
  225. meerk40t/gui/propertypanels/operationpropertymain.py +1890 -1638
  226. meerk40t/gui/propertypanels/outputproperty.py +59 -58
  227. meerk40t/gui/propertypanels/pathproperty.py +389 -380
  228. meerk40t/gui/propertypanels/placementproperty.py +1214 -383
  229. meerk40t/gui/propertypanels/pointproperty.py +140 -136
  230. meerk40t/gui/propertypanels/propertywindow.py +313 -181
  231. meerk40t/gui/propertypanels/rasterwizardpanels.py +996 -912
  232. meerk40t/gui/propertypanels/regbranchproperties.py +76 -0
  233. meerk40t/gui/propertypanels/textproperty.py +770 -755
  234. meerk40t/gui/propertypanels/waitproperty.py +56 -55
  235. meerk40t/gui/propertypanels/warpproperty.py +121 -0
  236. meerk40t/gui/propertypanels/wobbleproperty.py +255 -204
  237. meerk40t/gui/ribbon.py +2471 -2210
  238. meerk40t/gui/scene/scene.py +1100 -1051
  239. meerk40t/gui/scene/sceneconst.py +22 -22
  240. meerk40t/gui/scene/scenepanel.py +439 -349
  241. meerk40t/gui/scene/scenespacewidget.py +365 -365
  242. meerk40t/gui/scene/widget.py +518 -505
  243. meerk40t/gui/scenewidgets/affinemover.py +215 -215
  244. meerk40t/gui/scenewidgets/attractionwidget.py +315 -309
  245. meerk40t/gui/scenewidgets/bedwidget.py +120 -97
  246. meerk40t/gui/scenewidgets/elementswidget.py +137 -107
  247. meerk40t/gui/scenewidgets/gridwidget.py +785 -745
  248. meerk40t/gui/scenewidgets/guidewidget.py +765 -765
  249. meerk40t/gui/scenewidgets/laserpathwidget.py +66 -66
  250. meerk40t/gui/scenewidgets/machineoriginwidget.py +86 -86
  251. meerk40t/gui/scenewidgets/nodeselector.py +28 -28
  252. meerk40t/gui/scenewidgets/rectselectwidget.py +592 -346
  253. meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
  254. meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
  255. meerk40t/gui/scenewidgets/selectionwidget.py +2958 -2756
  256. meerk40t/gui/simpleui.py +362 -333
  257. meerk40t/gui/simulation.py +2451 -2094
  258. meerk40t/gui/snapoptions.py +208 -203
  259. meerk40t/gui/spoolerpanel.py +1227 -1180
  260. meerk40t/gui/statusbarwidgets/defaultoperations.py +480 -353
  261. meerk40t/gui/statusbarwidgets/infowidget.py +520 -483
  262. meerk40t/gui/statusbarwidgets/opassignwidget.py +356 -355
  263. meerk40t/gui/statusbarwidgets/selectionwidget.py +172 -171
  264. meerk40t/gui/statusbarwidgets/shapepropwidget.py +754 -236
  265. meerk40t/gui/statusbarwidgets/statusbar.py +272 -260
  266. meerk40t/gui/statusbarwidgets/statusbarwidget.py +268 -270
  267. meerk40t/gui/statusbarwidgets/strokewidget.py +267 -251
  268. meerk40t/gui/themes.py +200 -78
  269. meerk40t/gui/tips.py +590 -0
  270. meerk40t/gui/toolwidgets/circlebrush.py +35 -35
  271. meerk40t/gui/toolwidgets/toolcircle.py +248 -242
  272. meerk40t/gui/toolwidgets/toolcontainer.py +82 -77
  273. meerk40t/gui/toolwidgets/tooldraw.py +97 -90
  274. meerk40t/gui/toolwidgets/toolellipse.py +219 -212
  275. meerk40t/gui/toolwidgets/toolimagecut.py +25 -132
  276. meerk40t/gui/toolwidgets/toolline.py +39 -144
  277. meerk40t/gui/toolwidgets/toollinetext.py +79 -236
  278. meerk40t/gui/toolwidgets/toollinetext_inline.py +296 -0
  279. meerk40t/gui/toolwidgets/toolmeasure.py +163 -216
  280. meerk40t/gui/toolwidgets/toolnodeedit.py +2088 -2074
  281. meerk40t/gui/toolwidgets/toolnodemove.py +92 -94
  282. meerk40t/gui/toolwidgets/toolparameter.py +754 -668
  283. meerk40t/gui/toolwidgets/toolplacement.py +108 -108
  284. meerk40t/gui/toolwidgets/toolpoint.py +68 -59
  285. meerk40t/gui/toolwidgets/toolpointlistbuilder.py +294 -0
  286. meerk40t/gui/toolwidgets/toolpointmove.py +183 -0
  287. meerk40t/gui/toolwidgets/toolpolygon.py +288 -403
  288. meerk40t/gui/toolwidgets/toolpolyline.py +38 -196
  289. meerk40t/gui/toolwidgets/toolrect.py +211 -207
  290. meerk40t/gui/toolwidgets/toolrelocate.py +72 -72
  291. meerk40t/gui/toolwidgets/toolribbon.py +598 -113
  292. meerk40t/gui/toolwidgets/tooltabedit.py +546 -0
  293. meerk40t/gui/toolwidgets/tooltext.py +98 -89
  294. meerk40t/gui/toolwidgets/toolvector.py +213 -204
  295. meerk40t/gui/toolwidgets/toolwidget.py +39 -39
  296. meerk40t/gui/usbconnect.py +98 -91
  297. meerk40t/gui/utilitywidgets/buttonwidget.py +18 -18
  298. meerk40t/gui/utilitywidgets/checkboxwidget.py +90 -90
  299. meerk40t/gui/utilitywidgets/controlwidget.py +14 -14
  300. meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +343 -340
  301. meerk40t/gui/utilitywidgets/debugwidgets.py +148 -0
  302. meerk40t/gui/utilitywidgets/handlewidget.py +27 -27
  303. meerk40t/gui/utilitywidgets/harmonograph.py +450 -447
  304. meerk40t/gui/utilitywidgets/openclosewidget.py +40 -40
  305. meerk40t/gui/utilitywidgets/rotationwidget.py +54 -54
  306. meerk40t/gui/utilitywidgets/scalewidget.py +75 -75
  307. meerk40t/gui/utilitywidgets/seekbarwidget.py +183 -183
  308. meerk40t/gui/utilitywidgets/togglewidget.py +142 -142
  309. meerk40t/gui/utilitywidgets/toolbarwidget.py +8 -8
  310. meerk40t/gui/wordlisteditor.py +985 -931
  311. meerk40t/gui/wxmeerk40t.py +1447 -1169
  312. meerk40t/gui/wxmmain.py +5644 -4112
  313. meerk40t/gui/wxmribbon.py +1591 -1076
  314. meerk40t/gui/wxmscene.py +1631 -1453
  315. meerk40t/gui/wxmtree.py +2416 -2089
  316. meerk40t/gui/wxutils.py +1769 -1099
  317. meerk40t/gui/zmatrix.py +102 -102
  318. meerk40t/image/__init__.py +1 -1
  319. meerk40t/image/dither.py +429 -0
  320. meerk40t/image/imagetools.py +2793 -2269
  321. meerk40t/internal_plugins.py +150 -130
  322. meerk40t/kernel/__init__.py +63 -12
  323. meerk40t/kernel/channel.py +259 -212
  324. meerk40t/kernel/context.py +538 -538
  325. meerk40t/kernel/exceptions.py +41 -41
  326. meerk40t/kernel/functions.py +463 -414
  327. meerk40t/kernel/jobs.py +100 -100
  328. meerk40t/kernel/kernel.py +3828 -3571
  329. meerk40t/kernel/lifecycles.py +71 -71
  330. meerk40t/kernel/module.py +49 -49
  331. meerk40t/kernel/service.py +147 -147
  332. meerk40t/kernel/settings.py +383 -343
  333. meerk40t/lihuiyu/controller.py +883 -876
  334. meerk40t/lihuiyu/device.py +1181 -1069
  335. meerk40t/lihuiyu/driver.py +1466 -1372
  336. meerk40t/lihuiyu/gui/gui.py +127 -106
  337. meerk40t/lihuiyu/gui/lhyaccelgui.py +377 -363
  338. meerk40t/lihuiyu/gui/lhycontrollergui.py +741 -651
  339. meerk40t/lihuiyu/gui/lhydrivergui.py +470 -446
  340. meerk40t/lihuiyu/gui/lhyoperationproperties.py +238 -237
  341. meerk40t/lihuiyu/gui/tcpcontroller.py +226 -190
  342. meerk40t/lihuiyu/interpreter.py +53 -53
  343. meerk40t/lihuiyu/laserspeed.py +450 -450
  344. meerk40t/lihuiyu/loader.py +90 -90
  345. meerk40t/lihuiyu/parser.py +404 -404
  346. meerk40t/lihuiyu/plugin.py +101 -102
  347. meerk40t/lihuiyu/tcp_connection.py +111 -109
  348. meerk40t/main.py +231 -165
  349. meerk40t/moshi/builder.py +788 -781
  350. meerk40t/moshi/controller.py +505 -499
  351. meerk40t/moshi/device.py +495 -442
  352. meerk40t/moshi/driver.py +862 -696
  353. meerk40t/moshi/gui/gui.py +78 -76
  354. meerk40t/moshi/gui/moshicontrollergui.py +538 -522
  355. meerk40t/moshi/gui/moshidrivergui.py +87 -75
  356. meerk40t/moshi/plugin.py +43 -43
  357. meerk40t/network/console_server.py +140 -57
  358. meerk40t/network/kernelserver.py +10 -9
  359. meerk40t/network/tcp_server.py +142 -140
  360. meerk40t/network/udp_server.py +103 -77
  361. meerk40t/network/web_server.py +404 -0
  362. meerk40t/newly/controller.py +1158 -1144
  363. meerk40t/newly/device.py +874 -732
  364. meerk40t/newly/driver.py +540 -412
  365. meerk40t/newly/gui/gui.py +219 -188
  366. meerk40t/newly/gui/newlyconfig.py +116 -101
  367. meerk40t/newly/gui/newlycontroller.py +193 -186
  368. meerk40t/newly/gui/operationproperties.py +51 -51
  369. meerk40t/newly/mock_connection.py +82 -82
  370. meerk40t/newly/newly_params.py +56 -56
  371. meerk40t/newly/plugin.py +1214 -1246
  372. meerk40t/newly/usb_connection.py +322 -322
  373. meerk40t/rotary/gui/gui.py +52 -46
  374. meerk40t/rotary/gui/rotarysettings.py +240 -232
  375. meerk40t/rotary/rotary.py +202 -98
  376. meerk40t/ruida/control.py +291 -91
  377. meerk40t/ruida/controller.py +138 -1088
  378. meerk40t/ruida/device.py +676 -231
  379. meerk40t/ruida/driver.py +534 -472
  380. meerk40t/ruida/emulator.py +1494 -1491
  381. meerk40t/ruida/exceptions.py +4 -4
  382. meerk40t/ruida/gui/gui.py +71 -76
  383. meerk40t/ruida/gui/ruidaconfig.py +239 -72
  384. meerk40t/ruida/gui/ruidacontroller.py +187 -184
  385. meerk40t/ruida/gui/ruidaoperationproperties.py +48 -47
  386. meerk40t/ruida/loader.py +54 -52
  387. meerk40t/ruida/mock_connection.py +57 -109
  388. meerk40t/ruida/plugin.py +124 -87
  389. meerk40t/ruida/rdjob.py +2084 -945
  390. meerk40t/ruida/serial_connection.py +116 -0
  391. meerk40t/ruida/tcp_connection.py +146 -0
  392. meerk40t/ruida/udp_connection.py +73 -0
  393. meerk40t/svgelements.py +9671 -9669
  394. meerk40t/tools/driver_to_path.py +584 -579
  395. meerk40t/tools/geomstr.py +5583 -4680
  396. meerk40t/tools/jhfparser.py +357 -292
  397. meerk40t/tools/kerftest.py +904 -890
  398. meerk40t/tools/livinghinges.py +1168 -1033
  399. meerk40t/tools/pathtools.py +987 -949
  400. meerk40t/tools/pmatrix.py +234 -0
  401. meerk40t/tools/pointfinder.py +942 -942
  402. meerk40t/tools/polybool.py +941 -940
  403. meerk40t/tools/rasterplotter.py +1660 -547
  404. meerk40t/tools/shxparser.py +1047 -901
  405. meerk40t/tools/ttfparser.py +726 -446
  406. meerk40t/tools/zinglplotter.py +595 -593
  407. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/LICENSE +21 -21
  408. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/METADATA +150 -139
  409. meerk40t-0.9.7020.dist-info/RECORD +446 -0
  410. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/WHEEL +1 -1
  411. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/top_level.txt +0 -1
  412. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/zip-safe +1 -1
  413. meerk40t/balormk/elementlightjob.py +0 -159
  414. meerk40t-0.9.3001.dist-info/RECORD +0 -437
  415. test/bootstrap.py +0 -63
  416. test/test_cli.py +0 -12
  417. test/test_core_cutcode.py +0 -418
  418. test/test_core_elements.py +0 -144
  419. test/test_core_plotplanner.py +0 -397
  420. test/test_core_viewports.py +0 -312
  421. test/test_drivers_grbl.py +0 -108
  422. test/test_drivers_lihuiyu.py +0 -443
  423. test/test_drivers_newly.py +0 -113
  424. test/test_element_degenerate_points.py +0 -43
  425. test/test_elements_classify.py +0 -97
  426. test/test_elements_penbox.py +0 -22
  427. test/test_file_svg.py +0 -176
  428. test/test_fill.py +0 -155
  429. test/test_geomstr.py +0 -1523
  430. test/test_geomstr_nodes.py +0 -18
  431. test/test_imagetools_actualize.py +0 -306
  432. test/test_imagetools_wizard.py +0 -258
  433. test/test_kernel.py +0 -200
  434. test/test_laser_speeds.py +0 -3303
  435. test/test_length.py +0 -57
  436. test/test_lifecycle.py +0 -66
  437. test/test_operations.py +0 -251
  438. test/test_operations_hatch.py +0 -57
  439. test/test_ruida.py +0 -19
  440. test/test_spooler.py +0 -22
  441. test/test_tools_rasterplotter.py +0 -29
  442. test/test_wobble.py +0 -133
  443. test/test_zingl.py +0 -124
  444. {test → meerk40t/cylinder}/__init__.py +0 -0
  445. /meerk40t/{core/element_commands.py → cylinder/gui/__init__.py} +0 -0
  446. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/entry_points.txt +0 -0
@@ -1,1180 +1,1227 @@
1
- import threading
2
- import time
3
- from math import isinf, isnan
4
- from pathlib import Path
5
-
6
- import wx
7
- import wx.lib.mixins.listctrl as listmix
8
- from wx import aui
9
-
10
- from meerk40t.gui.icons import (
11
- STD_ICON_SIZE,
12
- get_default_icon_size,
13
- icons8_emergency_stop_button,
14
- icons8_pause,
15
- icons8_route,
16
- )
17
- from meerk40t.gui.mwindow import MWindow
18
- from meerk40t.gui.wxutils import HoverButton
19
- from meerk40t.kernel import Job, get_safe_path, signal_listener
20
-
21
- _ = wx.GetTranslation
22
-
23
- JC_INDEX = 0
24
- JC_DEVICE = 1
25
- JC_JOBNAME = 2
26
- JC_ENTRIES = 3
27
- JC_STATUS = 4
28
- JC_TYPE = 5
29
- JC_STEPS = 6
30
- JC_PASSES = 7
31
- JC_PRIORITY = 8
32
- JC_RUNTIME = 9
33
- JC_ESTIMATE = 10
34
-
35
- HC_INDEX = 0
36
- HC_DEVICE = 1
37
- HC_JOBNAME = 2
38
- HC_START = 3
39
- HC_END = 4
40
- HC_RUNTIME = 5
41
- HC_ESTIMATE = 6
42
- HC_STEPS = 7
43
- HC_PASSES = 8
44
- HC_STATUS = 9
45
- HC_JOBINFO = 10
46
-
47
-
48
- def register_panel_spooler(window, context):
49
- panel = SpoolerPanel(window, wx.ID_ANY, context=context)
50
- pane = (
51
- aui.AuiPaneInfo()
52
- .Bottom()
53
- .Layer(1)
54
- .MinSize(600, 100)
55
- .FloatingSize(600, 230)
56
- .Caption(_("Spooler"))
57
- .Name("spooler")
58
- .CaptionVisible(not context.pane_lock)
59
- .Hide()
60
- )
61
- pane.dock_proportion = 600
62
- pane.control = panel
63
-
64
- window.on_pane_create(pane)
65
- context.register("pane/spooler", pane)
66
-
67
-
68
- class EditableListCtrl(wx.ListCtrl, listmix.TextEditMixin):
69
- """TextEditMixin allows any column to be edited."""
70
-
71
- # ----------------------------------------------------------------------
72
- def __init__(
73
- self, parent, ID=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0
74
- ):
75
- """Constructor"""
76
- wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
77
- listmix.TextEditMixin.__init__(self)
78
-
79
-
80
- class SpoolerPanel(wx.Panel):
81
- def __init__(self, *args, context=None, selected_device=None, **kwds):
82
- # begin wxGlade: SpoolerPanel.__init__
83
- kwds["style"] = kwds.get("style", 0) | wx.TAB_TRAVERSAL
84
- wx.Panel.__init__(self, *args, **kwds)
85
- self.context = context
86
- self.selected_device = selected_device
87
- self.available_devices = context.kernel.services("device")
88
- self.filter_device = None
89
- spools = [s.label for s in self.available_devices]
90
- spools.insert(0, _("-- All available devices --"))
91
- self.queue_entries = []
92
- self.context.setting(int, "spooler_sash_position", 0)
93
- self.context.setting(bool, "spool_history_clear_on_start", False)
94
- self.context.setting(bool, "spool_ignore_helper_jobs", True)
95
-
96
- self.splitter = wx.SplitterWindow(self, id=wx.ID_ANY, style=wx.SP_LIVE_UPDATE)
97
- sty = wx.BORDER_SUNKEN
98
-
99
- self.win_top = wx.Window(self.splitter, style=sty)
100
- self.win_bottom = wx.Window(self.splitter, style=sty)
101
- self.splitter.SetMinimumPaneSize(50)
102
- self.splitter.SplitHorizontally(self.win_top, self.win_bottom, -100)
103
- self.splitter.SetSashPosition(self.context.spooler_sash_position)
104
- self.combo_device = wx.ComboBox(
105
- self.win_top, wx.ID_ANY, choices=spools, style=wx.CB_DROPDOWN
106
- )
107
- self.combo_device.SetSelection(0) # All by default...
108
- self.button_pause = wx.Button(self.win_top, wx.ID_ANY, _("Pause"))
109
- self.button_pause.SetToolTip(_("Pause/Resume the laser"))
110
- self.button_pause.SetBitmap(
111
- icons8_pause.GetBitmap(resize=0.5 * get_default_icon_size())
112
- )
113
- self.button_stop = HoverButton(self.win_top, wx.ID_ANY, _("Abort"))
114
- self.button_stop.SetToolTip(_("Stop the laser"))
115
- self.button_stop.SetBitmap(
116
- icons8_emergency_stop_button.GetBitmap(
117
- resize=0.5 * get_default_icon_size(),
118
- color=self.context.themes.get("stop_fg"),
119
- keepalpha=True,
120
- )
121
- )
122
- self.button_stop.SetBitmapFocus(
123
- icons8_emergency_stop_button.GetBitmap(resize=0.5 * get_default_icon_size())
124
- )
125
- self.button_stop.SetBackgroundColour(self.context.themes.get("stop_bg"))
126
- self.button_stop.SetForegroundColour(self.context.themes.get("stop_fg"))
127
- self.button_stop.SetFocusColour(self.context.themes.get("stop_fg_focus"))
128
-
129
- self.list_job_spool = wx.ListCtrl(
130
- self.win_top,
131
- wx.ID_ANY,
132
- style=wx.LC_HRULES | wx.LC_REPORT | wx.LC_VRULES | wx.LC_SINGLE_SEL,
133
- )
134
-
135
- self.info_label = wx.StaticText(
136
- self.win_bottom, wx.ID_ANY, _("Completed jobs:")
137
- )
138
- self.button_clear_history = wx.Button(
139
- self.win_bottom, wx.ID_ANY, _("Clear History")
140
- )
141
- self.list_job_history = EditableListCtrl(
142
- self.win_bottom,
143
- wx.ID_ANY,
144
- style=wx.LC_HRULES | wx.LC_REPORT | wx.LC_VRULES | wx.LC_SINGLE_SEL,
145
- )
146
-
147
- self.__set_properties()
148
- self.__do_layout()
149
- self.current_item = None
150
- self.Bind(
151
- wx.EVT_BUTTON, self.on_button_clear_history, self.button_clear_history
152
- )
153
- self.button_clear_history.Bind(wx.EVT_RIGHT_DOWN, self.on_right_mouse_history)
154
- self.list_job_history.Bind(wx.EVT_RIGHT_DOWN, self.on_right_mouse_history)
155
- self.list_job_history.Bind(
156
- wx.EVT_LIST_BEGIN_LABEL_EDIT, self.before_history_update
157
- )
158
- self.list_job_history.Bind(wx.EVT_LIST_END_LABEL_EDIT, self.on_history_update)
159
- self.Bind(wx.EVT_BUTTON, self.on_button_pause, self.button_pause)
160
- self.Bind(wx.EVT_BUTTON, self.on_button_stop, self.button_stop)
161
- self.Bind(wx.EVT_COMBOBOX, self.on_combo_device, self.combo_device)
162
- self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.on_list_drag, self.list_job_spool)
163
-
164
- self.splitter.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGED, self.on_sash_changed)
165
- self.splitter.Bind(wx.EVT_SPLITTER_DOUBLECLICKED, self.on_sash_double)
166
-
167
- self.list_job_spool.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_item_selected)
168
- # self.list_job_spool.Bind(wx.EVT_LEFT_DCLICK, self.on_item_doubleclick)
169
- self.Bind(
170
- wx.EVT_LIST_ITEM_RIGHT_CLICK, self.on_item_rightclick, self.list_job_spool
171
- )
172
- # end wxGlade
173
- self._last_invokation = 0
174
- self.dirty = False
175
- self.update_buffer_size = False
176
- self.update_spooler_state = False
177
- self.update_spooler = True
178
-
179
- self.elements_progress = 0
180
- self.elements_progress_total = 0
181
- self.command_index = 0
182
- self.listener_list = None
183
- self.list_lookup = {}
184
- self.map_item_key = {}
185
- self.refresh_history()
186
- self.set_pause_color()
187
- if self.context.spool_history_clear_on_start:
188
- self.clear_history()
189
- # We set a timer job that will periodically check the spooler queue
190
- # in case no signal was received
191
- self.shown = False
192
- self.update_lock = threading.Lock()
193
- self.timerjob = Job(
194
- process=self.update_queue,
195
- job_name="spooler-update",
196
- interval=5,
197
- run_main=True,
198
- )
199
-
200
- def __set_properties(self):
201
- # begin wxGlade: SpoolerPanel.__set_properties
202
- self.combo_device.SetToolTip(_("Select the device"))
203
- self.list_job_spool.SetToolTip(_("List and modify the queued operations"))
204
- self.button_clear_history.SetToolTip(
205
- _("Clear spooler history (right click for more options)")
206
- )
207
- self.list_job_spool.AppendColumn(_("#"), format=wx.LIST_FORMAT_LEFT, width=58)
208
- self.list_job_spool.AppendColumn(
209
- _("Device"),
210
- format=wx.LIST_FORMAT_LEFT,
211
- width=95,
212
- )
213
- self.list_job_spool.AppendColumn(
214
- _("Name"), format=wx.LIST_FORMAT_LEFT, width=95
215
- )
216
- self.list_job_spool.AppendColumn(
217
- _("Items"), format=wx.LIST_FORMAT_LEFT, width=45
218
- )
219
- self.list_job_spool.AppendColumn(
220
- _("Status"), format=wx.LIST_FORMAT_LEFT, width=73
221
- )
222
- self.list_job_spool.AppendColumn(
223
- _("Type"), format=wx.LIST_FORMAT_LEFT, width=60
224
- )
225
- self.list_job_spool.AppendColumn(
226
- _("Steps"), format=wx.LIST_FORMAT_LEFT, width=73
227
- )
228
- self.list_job_spool.AppendColumn(
229
- _("Passes"), format=wx.LIST_FORMAT_LEFT, width=73
230
- )
231
- self.list_job_spool.AppendColumn(
232
- _("Priority"), format=wx.LIST_FORMAT_LEFT, width=73
233
- )
234
- self.list_job_spool.AppendColumn(
235
- _("Runtime"), format=wx.LIST_FORMAT_LEFT, width=73
236
- )
237
- self.list_job_spool.AppendColumn(
238
- _("Estimate"), format=wx.LIST_FORMAT_LEFT, width=73
239
- )
240
-
241
- self.list_job_history.AppendColumn(_("#"), format=wx.LIST_FORMAT_LEFT, width=48)
242
-
243
- self.list_job_history.AppendColumn(
244
- _("Device"), format=wx.LIST_FORMAT_LEFT, width=73
245
- )
246
- self.list_job_history.AppendColumn(
247
- _("Name"), format=wx.LIST_FORMAT_LEFT, width=95
248
- )
249
- self.list_job_history.AppendColumn(
250
- _("Start"), format=wx.LIST_FORMAT_LEFT, width=113
251
- )
252
- self.list_job_history.AppendColumn(
253
- _("End"), format=wx.LIST_FORMAT_LEFT, width=73
254
- )
255
- self.list_job_history.AppendColumn(
256
- _("Runtime"), format=wx.LIST_FORMAT_LEFT, width=73
257
- )
258
- self.list_job_history.AppendColumn(
259
- _("Estimate"), format=wx.LIST_FORMAT_LEFT, width=73
260
- )
261
- self.list_job_history.AppendColumn(
262
- _("Steps"), format=wx.LIST_FORMAT_LEFT, width=73
263
- )
264
- self.list_job_history.AppendColumn(
265
- _("Passes"), format=wx.LIST_FORMAT_LEFT, width=73
266
- )
267
- self.list_job_history.AppendColumn(
268
- _("Status"), format=wx.LIST_FORMAT_LEFT, width=73
269
- )
270
- self.list_job_history.AppendColumn(
271
- _("Jobinfo"), format=wx.LIST_FORMAT_LEFT, width=wx.LIST_AUTOSIZE_USEHEADER
272
- )
273
-
274
- # end wxGlade
275
-
276
- def __do_layout(self):
277
- sizer_main = wx.BoxSizer(wx.VERTICAL)
278
-
279
- sizer_top = wx.BoxSizer(wx.VERTICAL)
280
- sizer_bottom = wx.BoxSizer(wx.VERTICAL)
281
- sizer_combo_cmds = wx.BoxSizer(wx.HORIZONTAL)
282
- sizer_combo_cmds.Add(self.combo_device, 1, wx.ALIGN_CENTER_VERTICAL, 0)
283
- sizer_combo_cmds.Add(self.button_pause, 0, wx.EXPAND, 0)
284
- sizer_combo_cmds.Add(self.button_stop, 0, wx.EXPAND, 0)
285
-
286
- sizer_top.Add(sizer_combo_cmds, 0, wx.EXPAND, 0)
287
- sizer_top.Add(self.list_job_spool, 4, wx.EXPAND, 0)
288
- self.win_top.SetSizer(sizer_top)
289
- sizer_top.Fit(self.win_top)
290
-
291
- hsizer = wx.BoxSizer(wx.HORIZONTAL)
292
- hsizer.Add(self.info_label, 1, wx.ALIGN_CENTER_VERTICAL, 0)
293
- hsizer.Add(self.button_clear_history, 0, wx.EXPAND, 0)
294
- sizer_bottom.Add(hsizer, 0, wx.EXPAND, 0)
295
- sizer_bottom.Add(self.list_job_history, 2, wx.EXPAND, 0)
296
- self.win_bottom.SetSizer(sizer_bottom)
297
- sizer_bottom.Fit(self.win_bottom)
298
-
299
- sizer_main.Add(self.splitter, 1, wx.EXPAND, 0)
300
- self.SetSizer(sizer_main)
301
- sizer_main.Fit(self)
302
- self.Layout()
303
- # end wxGlade
304
-
305
- def on_sash_changed(self, event):
306
- position = self.splitter.GetSashPosition()
307
- self.context.spooler_sash_position = position
308
-
309
- def on_sash_double(self, event):
310
- self.splitter.SetSashPosition(0, True)
311
- self.context.spooler_sash_position = 0
312
-
313
- def on_item_selected(self, event):
314
- self.current_item = event.Index
315
-
316
- def write_csv(self):
317
- filename = Path(get_safe_path(self.context.kernel.name, create=True)).joinpath(
318
- "history.csv"
319
- )
320
- if self.filter_device:
321
- events = self.context.logging.matching_events(
322
- "job", device=self.filter_device
323
- )
324
- else:
325
- events = self.context.logging.matching_events("job")
326
- try:
327
- with open(filename, "w", encoding="utf-8") as f:
328
- simpleline = "device;jobname;start;end;duration;estimate;steps;total;loops;passes;status;info"
329
- f.write(simpleline + "\n")
330
- for key, info in events:
331
- line_items = []
332
- if info["start_time"] is None:
333
- continue
334
- line_items.append(str(info.get("device", "''")))
335
- line_items.append(str(info.get("label", "''")))
336
- line_items.append(
337
- f"{self.datestr(info.get('start_time',0))} {self.timestr(info.get('start_time',0), True)}"
338
- )
339
-
340
- line_items.append(
341
- self.timestr(
342
- info.get("start_time", 0) + info.get("duration", 0), True
343
- )
344
- )
345
- line_items.append(self.timestr(info.get("duration", 0), False))
346
- line_items.append(self.timestr(info.get("estimate", 0), False))
347
- line_items.append(str(info.get("steps_done", 0)))
348
- line_items.append(str(info.get("steps_total", 0)))
349
- line_items.append(str(info.get("loop", 0)))
350
- # First passes then device
351
- line_items.append(str(info.get("passes", "''")))
352
- line_items.append(str(info.get("status", "''")))
353
- line_items.append(str(info.get("info", "''")))
354
- f.write(f'{";".join(line_items)}\n')
355
-
356
- except (PermissionError, OSError, FileNotFoundError):
357
- pass
358
-
359
- def clear_history(self, older_than=None, job_type=None):
360
- if self.filter_device:
361
- to_remove = list(
362
- self.context.logging.matching_events("job", device=self.filter_device)
363
- )
364
- else:
365
- to_remove = list(self.context.logging.matching_events("job"))
366
- for key, event in to_remove:
367
- if event is not None and older_than is not None:
368
- if not "start_time" in event:
369
- continue
370
- if (
371
- event["start_time"] is not None
372
- and event["start_time"] >= older_than
373
- ):
374
- continue
375
- if event is not None and job_type is not None:
376
- if not "status" in event:
377
- continue
378
- if event["status"] is not None and event["status"] != job_type:
379
- continue
380
- del self.context.logging.logs[key]
381
- self.refresh_history()
382
-
383
- def on_button_clear_history(self, event):
384
- self.clear_history(older_than=None, job_type=None)
385
-
386
- def on_right_mouse_history(self, event):
387
- listid = self.list_job_history.GetFirstSelected()
388
- if listid >= 0:
389
- idx = self.list_job_history.GetItemData(listid)
390
- key = self.map_item_key[listid]
391
- else:
392
- # Bad selection.
393
- idx = -1
394
- key = None
395
-
396
- def on_menu_index(idx_to_delete):
397
- def check(event_c):
398
- del self.context.logging.logs[key]
399
- self.refresh_history()
400
-
401
- return check
402
-
403
- def on_menu_time(cutoff, jobtype):
404
- def check(event):
405
- self.clear_history(older_than=dcutoff, job_type=djobtype)
406
-
407
- # Store value locally
408
- dcutoff = cutoff
409
- djobtype = jobtype
410
- return check
411
-
412
- def toggle_1(event):
413
- self.context.spool_history_clear_on_start = (
414
- not self.context.spool_history_clear_on_start
415
- )
416
-
417
- def toggle_2(event):
418
- self.context.spool_ignore_helper_jobs = (
419
- not self.context.spool_ignore_helper_jobs
420
- )
421
- self.refresh_history()
422
-
423
- now = time.time()
424
- week_seconds = 60 * 60 * 24 * 7
425
- options = [(_("All entries"), None, None)]
426
- for week in range(1, 5):
427
- cutoff_time = now - week * week_seconds
428
- options.append(
429
- (_("Older than {week} week").format(week=week), cutoff_time, None)
430
- )
431
- options.append((_("All incomplete jobs"), None, "stopped"))
432
- menu = wx.Menu()
433
- if idx >= 0:
434
- menuitem = menu.Append(wx.ID_ANY, _("Delete this entry"), "")
435
- self.Bind(
436
- wx.EVT_MENU,
437
- on_menu_index(idx),
438
- id=menuitem.GetId(),
439
- )
440
- menu.AppendSeparator()
441
-
442
- menuitem = menu.Append(wx.ID_ANY, _("Delete..."))
443
- menu.Enable(menuitem.GetId(), False)
444
-
445
- for item in options:
446
- menuitem = menu.Append(wx.ID_ANY, item[0], "")
447
- self.Bind(
448
- wx.EVT_MENU,
449
- on_menu_time(item[1], item[2]),
450
- id=menuitem.GetId(),
451
- )
452
-
453
- menu.AppendSeparator()
454
- menuitem = menu.Append(
455
- wx.ID_ANY, _("Clear history on startup"), "", wx.ITEM_CHECK
456
- )
457
- menuitem.Check(self.context.spool_history_clear_on_start)
458
- self.Bind(
459
- wx.EVT_MENU,
460
- toggle_1,
461
- id=menuitem.GetId(),
462
- )
463
- menuitem = menu.Append(wx.ID_ANY, _("Ignore helper jobs"), "", wx.ITEM_CHECK)
464
- menuitem.Check(self.context.spool_ignore_helper_jobs)
465
- self.Bind(
466
- wx.EVT_MENU,
467
- toggle_2,
468
- id=menuitem.GetId(),
469
- )
470
-
471
- item = menu.Append(wx.ID_ANY, _("Write CSV"), "", wx.ITEM_NORMAL)
472
- self.Bind(wx.EVT_MENU, lambda e: self.write_csv(), item)
473
-
474
- if menu.MenuItemCount != 0:
475
- self.PopupMenu(menu)
476
- menu.Destroy()
477
-
478
- def on_button_pause(self, event): # wxGlade: LaserPanel.<event_handler>
479
- self.context("pause\n")
480
-
481
- def on_button_stop(self, event): # wxGlade: LaserPanel.<event_handler>
482
- self.context("estop\n")
483
-
484
- def on_combo_device(self, event=None): # wxGlade: Spooler.<event_handler>
485
- index = self.combo_device.GetSelection()
486
- if index == 0:
487
- self.filter_device = None
488
- else:
489
- self.filter_device = self.available_devices[index - 1].label
490
- self.update_spooler = True
491
- self.refresh_spooler_list()
492
- self.refresh_history()
493
- self.set_pause_color()
494
-
495
- def on_list_drag(self, event): # wxGlade: JobSpooler.<event_handler>
496
- # Todo: Drag to reprioritise jobs
497
- event.Skip()
498
-
499
- def on_item_rightclick(self, event): # wxGlade: JobSpooler.<event_handler>
500
- listindex = event.Index
501
- try:
502
- index = self.list_job_spool.GetItemData(listindex)
503
- except AssertionError:
504
- # Size of list_job_spool changed or is updating.
505
- return
506
- try:
507
- spooler = self.queue_entries[index][0]
508
- qindex = self.queue_entries[index][1]
509
- element = spooler.queue[qindex]
510
- except IndexError:
511
- return
512
-
513
- menu = wx.Menu()
514
- item = menu.Append(
515
- wx.ID_ANY,
516
- f"{str(element)[:30]} [{spooler.context.label}]",
517
- "",
518
- wx.ITEM_NORMAL,
519
- )
520
- item.Enable(False)
521
- can_enable = False
522
- action = _("Remove")
523
- if element.status == "Running":
524
- action = _("Stop")
525
- remove_mode = "stop"
526
- elif hasattr(element, "enabled"):
527
- remove_mode = "remove"
528
- if element.enabled:
529
- action2 = _("Disable")
530
- else:
531
- action2 = _("Enable")
532
- can_enable = True
533
-
534
- item = menu.Append(
535
- wx.ID_ANY,
536
- f"{action}",
537
- "",
538
- wx.ITEM_NORMAL,
539
- )
540
- info_tuple = [spooler, element, remove_mode]
541
- self.Bind(wx.EVT_MENU, self.on_menu_popup_delete(info_tuple), item)
542
- # Are there more loops than just one?
543
- if hasattr(element, "loops"):
544
- # Still something to go?
545
- if element.loops > 1 and element.loops_executed < element.loops:
546
- item = menu.Append(
547
- wx.ID_ANY,
548
- _("Finish after this loop"),
549
- _(
550
- "Stop the current execution after the succesful execution of this loop"
551
- ),
552
- wx.ITEM_NORMAL,
553
- )
554
- info_tuple = [spooler, element]
555
- self.Bind(wx.EVT_MENU, self.on_menu_popup_stop_loop(info_tuple), item)
556
- if not isinf(element.loops):
557
- item = menu.Append(
558
- wx.ID_ANY,
559
- _("add another loop"),
560
- _("add another loop to this job"),
561
- wx.ITEM_NORMAL,
562
- )
563
- info_tuple = [spooler, element]
564
- self.Bind(wx.EVT_MENU, self.on_menu_popup_add_loop(info_tuple), item)
565
-
566
- if can_enable:
567
- item = menu.Append(
568
- wx.ID_ANY,
569
- f"{action2}",
570
- "",
571
- wx.ITEM_NORMAL,
572
- )
573
- info_tuple = [spooler, element]
574
- self.Bind(wx.EVT_MENU, self.on_menu_popup_toggle_enable(info_tuple), item)
575
- menu.AppendSeparator()
576
- item = menu.Append(wx.ID_ANY, _("Clear All"), "", wx.ITEM_NORMAL)
577
- self.Bind(wx.EVT_MENU, self.on_menu_popup_clear(element), item)
578
-
579
- self.PopupMenu(menu)
580
- menu.Destroy()
581
-
582
- def on_menu_popup_clear(self, element=None):
583
- def clear(event=None):
584
- spoolers = []
585
- for device in self.available_devices:
586
- addit = True
587
- if (
588
- self.filter_device is not None
589
- and device.label != self.filter_device
590
- ):
591
- addit = False
592
- if addit:
593
- spoolers.append(device.spooler)
594
- for spooler in spoolers:
595
- spooler.clear_queue()
596
- self.refresh_spooler_list()
597
-
598
- return clear
599
-
600
- def on_menu_popup_delete(self, element):
601
- def delete(event=None):
602
- spooler = element[0]
603
- mode = element[2]
604
- job = element[1]
605
- spooler.remove(job)
606
- # That will remove the job but create a log entry if needed.
607
- if mode == "stop":
608
- if hasattr(job, "stop"):
609
- job.stop()
610
- else:
611
- # Force stop of laser.
612
- self.context("estop\n")
613
- self.refresh_spooler_list()
614
-
615
- return delete
616
-
617
- def on_menu_popup_toggle_enable(self, element):
618
- def routine(event=None):
619
- spooler = element[0]
620
- job = element[1]
621
- job.enabled = not job.enabled
622
- self.refresh_spooler_list()
623
-
624
- return routine
625
-
626
- # def on_menu_popup_next_placement(self, element):
627
- # def routine(event=None):
628
- # spooler = element[0]
629
- # job = element[1]
630
- # if hasattr(job, "jump_to_next"):
631
- # job.jump_to_next()
632
- # self.refresh_spooler_list()
633
-
634
- # return routine
635
-
636
- def on_menu_popup_stop_loop(self, element):
637
- def routine(event=None):
638
- spooler = element[0]
639
- job = element[1]
640
- if hasattr(job, "stop_after_loop"):
641
- job.stop_after_loop()
642
- self.refresh_spooler_list()
643
-
644
- return routine
645
-
646
- def on_menu_popup_add_loop(self, element):
647
- def routine(event=None):
648
- spooler = element[0]
649
- job = element[1]
650
- if hasattr(job, "add_another_loop"):
651
- job.add_another_loop()
652
- self.refresh_spooler_list()
653
-
654
- return routine
655
-
656
- def pane_show(self, *args):
657
- self.shown = True
658
- self.context.schedule(self.timerjob)
659
- self.refresh_spooler_list()
660
-
661
- def pane_hide(self, *args):
662
- self.context.unschedule(self.timerjob)
663
- self.shown = False
664
-
665
- @staticmethod
666
- def _name_str(named_obj):
667
- try:
668
- return named_obj.__name__
669
- except AttributeError:
670
- return str(named_obj)
671
-
672
- def refresh_spooler_list(self):
673
- if not self.update_spooler:
674
- return
675
- try:
676
- self.list_job_spool.DeleteAllItems()
677
- except RuntimeError:
678
- return
679
- self.queue_entries = []
680
- queue_idx = -1
681
- for device in self.available_devices:
682
- spooler = device.spooler
683
- if spooler is None:
684
- continue
685
- if self.filter_device is not None and self.filter_device != device.label:
686
- continue
687
- for idx, e in enumerate(spooler.queue):
688
- self.queue_entries.append([spooler, idx])
689
- queue_idx += 1
690
- # Idx, Status, Type, Passes, Priority, Runtime, Estimate
691
- m = self.list_job_spool.InsertItem(idx, f"#{idx}")
692
- list_id = m
693
- spool_obj = e
694
-
695
- if list_id != -1:
696
- self.list_job_spool.SetItemData(list_id, queue_idx)
697
- # DEVICE
698
- self.list_job_spool.SetItem(list_id, JC_DEVICE, device.label)
699
- # Jobname
700
- to_display = ""
701
- if hasattr(spool_obj, "label"):
702
- to_display = spool_obj.label
703
- if to_display is None:
704
- to_display = ""
705
- if to_display == "":
706
- to_display = SpoolerPanel._name_str(spool_obj)
707
- if to_display.endswith(" items"):
708
- # Look for last ':' and remove everything from there
709
- cpos = -1
710
- lpos = -1
711
- while True:
712
- lpos = to_display.find(":", lpos + 1)
713
- if lpos == -1:
714
- break
715
- cpos = lpos
716
- if cpos > 0:
717
- to_display = to_display[:cpos]
718
-
719
- self.list_job_spool.SetItem(list_id, JC_JOBNAME, to_display)
720
- # Entries
721
- joblen = 1
722
- try:
723
- if hasattr(spool_obj, "items"):
724
- joblen = len(spool_obj.items)
725
- elif hasattr(spool_obj, "elements"):
726
- joblen = len(spool_obj.elements)
727
- except AttributeError:
728
- joblen = 1
729
- self.list_job_spool.SetItem(list_id, JC_ENTRIES, str(joblen))
730
- # STATUS
731
- self.list_job_spool.SetItem(
732
- list_id, JC_STATUS, str(spool_obj.status)
733
- )
734
-
735
- # TYPE
736
- try:
737
- self.list_job_spool.SetItem(
738
- list_id,
739
- JC_TYPE,
740
- str(spool_obj.__class__.__name__),
741
- )
742
- except AttributeError:
743
- pass
744
-
745
- # STEPS
746
- try:
747
- if spool_obj.steps_total == 0:
748
- spool_obj.calc_steps()
749
- self.list_job_spool.SetItem(
750
- list_id,
751
- JC_STEPS,
752
- f"{spool_obj.steps_done}/{spool_obj.steps_total}",
753
- )
754
- except AttributeError:
755
- self.list_job_spool.SetItem(list_id, JC_STEPS, "-")
756
- # PASSES
757
- try:
758
- loop = spool_obj.loops_executed
759
- total = spool_obj.loops
760
- # No invalid values please
761
- if loop is None:
762
- loop = 0
763
- if total is None:
764
- total = 1
765
-
766
- if isinf(total):
767
- total = "∞"
768
- self.list_job_spool.SetItem(
769
- list_id, JC_PASSES, f"{loop}/{total}"
770
- )
771
- except AttributeError:
772
- self.list_job_spool.SetItem(list_id, JC_PASSES, "-")
773
-
774
- # Priority
775
- try:
776
- self.list_job_spool.SetItem(
777
- list_id,
778
- JC_PRIORITY,
779
- str(spool_obj.priority),
780
- )
781
- except AttributeError:
782
- self.list_job_spool.SetItem(list_id, JC_PRIORITY, "-")
783
-
784
- # Runtime
785
- try:
786
- t = spool_obj.elapsed_time()
787
- hours, remainder = divmod(t, 3600)
788
- minutes, seconds = divmod(remainder, 60)
789
- runtime = f"{int(hours)}:{str(int(minutes)).zfill(2)}:{str(int(seconds)).zfill(2)}"
790
- self.list_job_spool.SetItem(list_id, JC_RUNTIME, runtime)
791
- except AttributeError:
792
- self.list_job_spool.SetItem(list_id, JC_RUNTIME, "-")
793
-
794
- # Estimate Time
795
- try:
796
- t = spool_obj.estimate_time()
797
- if isinf(t):
798
- runtime = "∞"
799
- else:
800
- hours, remainder = divmod(t, 3600)
801
- minutes, seconds = divmod(remainder, 60)
802
- runtime = f"{int(hours)}:{str(int(minutes)).zfill(2)}:{str(int(seconds)).zfill(2)}"
803
- self.list_job_spool.SetItem(list_id, JC_ESTIMATE, runtime)
804
- except AttributeError:
805
- self.list_job_spool.SetItem(list_id, JC_ESTIMATE, "-")
806
- self._last_invokation = time.time()
807
-
808
- @staticmethod
809
- def timestr(t, oneday):
810
- if t is None:
811
- return ""
812
- if isinstance(t, str):
813
- return t
814
- if isinf(t) or isnan(t) or t < 0:
815
- return "∞"
816
-
817
- if oneday:
818
- localt = time.localtime(t)
819
- hours = localt[3]
820
- minutes = localt[4]
821
- seconds = localt[5]
822
- else:
823
- hours, remainder = divmod(t, 3600)
824
- minutes, seconds = divmod(remainder, 60)
825
- # Military time display
826
- result = (
827
- f"{int(hours)}:{str(int(minutes)).zfill(2)}:{str(int(seconds)).zfill(2)}"
828
- )
829
- return result
830
-
831
- @staticmethod
832
- def datestr(t):
833
- if t is None:
834
- return ""
835
- if isinstance(t, str):
836
- return t
837
- localt = time.localtime(t)
838
- lyear = localt[0]
839
- syear = lyear % 100
840
- lmonth = int(localt[1])
841
- lday = localt[2]
842
- lhour = localt[3]
843
- lminute = localt[4]
844
- lsecond = localt[5]
845
- # wx.DateTime has a bug: it does always provide the dateformat
846
- # string with a month representation one number too high, so
847
- # wx.DateTime(31,01,1999)
848
- # Arbitrary but with different figures
849
- # Alas this is the only simple method to get locale relevant dateformat...
850
- pattern = None
851
- try:
852
- loc = wx.Locale()
853
- pattern = loc.GetOSInfo(wx.LOCALE_SHORT_DATE_FMT, wx.LOCALE_CAT_DEFAULT)
854
- except AttributeError:
855
- # That's not available, so we use the other algorithm instead...
856
- pass
857
- if pattern is not None:
858
- pattern = pattern.replace("%d", "{dd}")
859
- pattern = pattern.replace("%m", "{mm}")
860
- pattern = pattern.replace("%y", "{y}")
861
- pattern = pattern.replace("%Y", "{yy}")
862
- if pattern is None:
863
- wxdt = wx.DateTime(31, 7, 2022)
864
- pattern = wxdt.FormatDate()
865
- pattern = pattern.replace("2022", "{yy}")
866
- pattern = pattern.replace("22", "{y}")
867
- pattern = pattern.replace("31", "{dd}")
868
- # That would be the right thing, so if the bug is ever fixed, that will work
869
- pattern = pattern.replace("07", "{mm}")
870
- pattern = pattern.replace("7", "{mm}")
871
- # And this is needed to deal with the bug...
872
- pattern = pattern.replace("08", "{mm}")
873
- pattern = pattern.replace("8", "{mm}")
874
- # Deal with years seperately
875
- pattern = pattern.replace("{y}", str(syear).zfill(2))
876
- pattern = pattern.replace("{yy}", str(lyear).zfill(2))
877
- result = pattern.format(
878
- dd=str(lday).zfill(2),
879
- mm=str(lmonth).zfill(2),
880
- )
881
- # Just to show the bug...
882
- # result1 = f"{int(lday)}.{str(int(lmonth)).zfill(2)}.{str(int(lyear)).zfill(2)}"
883
- # wxdt = wx.DateTime(lday, lmonth, lyear, lhour, lminute, lsecond)
884
- # result2 = wxdt.FormatDate()
885
- # print(f"res={result}, wxd={result2}, manual={result1}, pattern={pattern}")
886
- return result
887
-
888
- def refresh_history(self):
889
- self.list_job_history.DeleteAllItems()
890
- self.map_item_key.clear()
891
- if self.filter_device:
892
- if self.context.spool_ignore_helper_jobs:
893
- events = self.context.logging.matching_events(
894
- "job", device=self.filter_device, important=True
895
- )
896
- else:
897
- events = self.context.logging.matching_events(
898
- "job", device=self.filter_device
899
- )
900
-
901
- else:
902
- if self.context.spool_ignore_helper_jobs:
903
- events = self.context.logging.matching_events("job", important=True)
904
- else:
905
- events = self.context.logging.matching_events("job")
906
- has_data = False
907
- for idx, event_and_key in enumerate(reversed(list(events))):
908
- has_data = True
909
- key, info = event_and_key
910
- list_id = self.list_job_history.InsertItem(
911
- self.list_job_history.GetItemCount(), f"#{idx}"
912
- )
913
- self.map_item_key[list_id] = key
914
- start_time = info.get("start_time", 0)
915
- if start_time is None:
916
- start_time = 0
917
- duration = info.get("duration", 0)
918
- if duration is None:
919
- duration = 0
920
- self.list_job_history.SetItem(list_id, HC_JOBNAME, str(info.get("label")))
921
- self.list_job_history.SetItem(
922
- list_id,
923
- HC_START,
924
- f"{self.datestr(start_time)} {self.timestr(start_time, True)}",
925
- )
926
-
927
- self.list_job_history.SetItem(
928
- list_id,
929
- HC_END,
930
- self.timestr(start_time + duration, True),
931
- )
932
- self.list_job_history.SetItem(
933
- list_id,
934
- HC_RUNTIME,
935
- self.timestr(duration, False),
936
- )
937
- nr_loop = info.get("loop")
938
- nr_total = info.get("total")
939
- if nr_total is None:
940
- if nr_loop is None:
941
- passes_str = "n/a"
942
- else:
943
- passes_str = f"{nr_loop}"
944
- elif isinf(float(nr_total)):
945
- passes_str = f"{nr_loop}/∞"
946
- else:
947
- passes_str = f"{nr_loop}/{nr_total}"
948
- self.list_job_history.SetItem(
949
- list_id,
950
- HC_PASSES,
951
- passes_str,
952
- )
953
- self.list_job_history.SetItem(list_id, HC_DEVICE, str(info.get("device")))
954
- self.list_job_history.SetItem(
955
- list_id, HC_STATUS, str(info.get("status", ""))
956
- )
957
- self.list_job_history.SetItem(
958
- list_id, HC_JOBINFO, str(info.get("info", ""))
959
- )
960
- self.list_job_history.SetItem(
961
- list_id,
962
- HC_ESTIMATE,
963
- self.timestr(info.get("estimate", 0), False),
964
- )
965
- self.list_job_history.SetItem(
966
- list_id,
967
- HC_STEPS,
968
- f"{info.get('steps_done',0)}/{info.get('steps_total',0)}",
969
- )
970
- self.list_job_history.SetItemData(list_id, idx)
971
- if has_data:
972
- self.list_job_history.Select(0)
973
-
974
- def before_history_update(self, event):
975
- list_id = event.GetIndex() # Get the current row
976
- col_id = event.GetColumn() # Get the current column
977
- if col_id == HC_JOBINFO:
978
- event.Allow()
979
- else:
980
- event.Veto()
981
-
982
- def on_history_update(self, event):
983
- list_id = event.GetIndex() # Get the current row
984
- col_id = event.GetColumn() # Get the current column
985
- new_data = event.GetLabel() # Get the changed data
986
- if list_id >= 0 and col_id == HC_JOBINFO:
987
- idx = self.list_job_history.GetItemData(list_id)
988
- key = self.map_item_key[idx]
989
- self.context.logging.logs[key]["info"] = new_data
990
-
991
- # Set the new data in the listctrl
992
- self.list_job_history.SetItem(list_id, col_id, new_data)
993
-
994
- def set_pause_color(self):
995
- new_bg_color = None
996
- new_fg_color = None
997
- new_caption = _("Pause")
998
- try:
999
- if self.context.device.driver.paused:
1000
- new_bg_color = self.context.themes.get("pause_bg")
1001
- new_fg_color = self.context.themes.get("pause_fg")
1002
- new_caption = _("Resume")
1003
- except AttributeError:
1004
- pass
1005
- self.button_pause.SetBackgroundColour(new_bg_color)
1006
- self.button_pause.SetForegroundColour(new_fg_color)
1007
- self.button_pause.SetLabelText(new_caption)
1008
-
1009
- @signal_listener("pause")
1010
- def on_device_pause_toggle(self, origin, *args):
1011
- self.set_pause_color()
1012
-
1013
- @signal_listener("activate;device")
1014
- def on_activate_device(self, origin, device):
1015
- self.available_devices = self.context.kernel.services("device")
1016
- self.selected_device = self.context.device
1017
- index = -1
1018
- for i, s in enumerate(self.available_devices):
1019
- if s is self.selected_device:
1020
- index = i + 1
1021
- break
1022
- spools = [s.label for s in self.available_devices]
1023
- spools.insert(0, _("-- All available devices --"))
1024
- # This might not be relevant if you have a stable device set, but there might always be
1025
- # changes to add / rename devices etc.
1026
- if self.combo_device.GetSelection() == 0:
1027
- # all-devices is a superset of any device, so we can leave it...
1028
- index = 0
1029
- self.combo_device.Clear()
1030
- self.combo_device.SetItems(spools)
1031
- self.combo_device.SetSelection(index)
1032
- self.on_combo_device(None)
1033
- self.set_pause_color()
1034
-
1035
- @signal_listener("spooler;completed")
1036
- def on_spooler_completed(self, origin, *args):
1037
- self.refresh_history()
1038
-
1039
- @signal_listener("spooler;queue")
1040
- @signal_listener("spooler;idle")
1041
- @signal_listener("spooler;realtime")
1042
- def on_spooler_update(self, origin, value, *args, **kwargs):
1043
- self.update_spooler = True
1044
- self.refresh_spooler_list()
1045
-
1046
- @signal_listener("driver;position")
1047
- @signal_listener("emulator;position")
1048
- @signal_listener("pipe;usb_status")
1049
- def on_device_update(self, origin, *args):
1050
- doit = True
1051
- with self.update_lock:
1052
- # Only update every 2 seconds or so
1053
- dtime = time.time()
1054
- if dtime - self._last_invokation < 2:
1055
- doit = False
1056
- else:
1057
- self._last_invokation = dtime
1058
- if not doit:
1059
- return
1060
-
1061
- # Two things (at least) could go wrong:
1062
- # 1) You are in the wrong queue, ie there's a job running in the background a
1063
- # that provides an update but the user has changed the device so a different
1064
- # queue is selected
1065
- # 2) As this is a signal it may come later, ie the job has already finished
1066
- #
1067
- # The checks here are rather basic and need to be revisited
1068
- refresh_needed = False
1069
- try:
1070
- listctrl = self.list_job_spool
1071
- except RuntimeError:
1072
- return
1073
- for list_id, entry in enumerate(self.queue_entries):
1074
- spooler = entry[0]
1075
- qindex = entry[1]
1076
- if qindex >= len(spooler.queue):
1077
- # This item is nowhere to be found
1078
- refresh_needed = True
1079
- continue
1080
- spool_obj = spooler.queue[qindex]
1081
- try:
1082
- t = spool_obj.elapsed_time()
1083
- hours, remainder = divmod(t, 3600)
1084
- minutes, seconds = divmod(remainder, 60)
1085
- runtime = f"{int(hours)}:{str(int(minutes)).zfill(2)}:{str(int(seconds)).zfill(2)}"
1086
- if list_id < self.list_job_spool.GetItemCount():
1087
- self.list_job_spool.SetItem(list_id, JC_RUNTIME, runtime)
1088
- except (AttributeError, AssertionError):
1089
- if list_id < self.list_job_spool.GetItemCount():
1090
- self.list_job_spool.SetItem(list_id, JC_RUNTIME, "-")
1091
- else:
1092
- refresh_needed = True
1093
-
1094
- try:
1095
- pass_str = f"{spool_obj.steps_done}/{spool_obj.steps_total}"
1096
- self.list_job_spool.SetItem(list_id, JC_STEPS, pass_str)
1097
- except AttributeError:
1098
- if list_id < self.list_job_spool.GetItemCount():
1099
- self.list_job_spool.SetItem(list_id, JC_STEPS, "-")
1100
- else:
1101
- refresh_needed = True
1102
- try:
1103
- loop = spool_obj.loops_executed
1104
- total = spool_obj.loops
1105
-
1106
- if isinf(total):
1107
- total = ""
1108
- pass_str = f"{loop}/{total}"
1109
- self.list_job_spool.SetItem(list_id, JC_PASSES, pass_str)
1110
- except AttributeError:
1111
- if list_id < self.list_job_spool.GetItemCount():
1112
- self.list_job_spool.SetItem(list_id, JC_PASSES, "-")
1113
- else:
1114
- refresh_needed = True
1115
-
1116
- # Estimate Time
1117
- try:
1118
- t = spool_obj.estimate_time()
1119
- if isinf(t):
1120
- runtime = "∞"
1121
- else:
1122
- hours, remainder = divmod(t, 3600)
1123
- minutes, seconds = divmod(remainder, 60)
1124
- runtime = f"{int(hours)}:{str(int(minutes)).zfill(2)}:{str(int(seconds)).zfill(2)}"
1125
-
1126
- if list_id < self.list_job_spool.GetItemCount():
1127
- self.list_job_spool.SetItem(list_id, JC_ESTIMATE, runtime)
1128
- except (AttributeError, AssertionError):
1129
- if list_id < self.list_job_spool.GetItemCount():
1130
- self.list_job_spool.SetItem(list_id, JC_ESTIMATE, "-")
1131
- else:
1132
- refresh_needed = True
1133
- if refresh_needed:
1134
- self.refresh_spooler_list()
1135
- self.refresh_history()
1136
-
1137
- def update_queue(self):
1138
- if self.shown:
1139
- self.on_device_update(None)
1140
-
1141
-
1142
- class JobSpooler(MWindow):
1143
- def __init__(self, *args, **kwds):
1144
- super().__init__(673, 456, *args, **kwds)
1145
- selected_device = None
1146
- if len(args) >= 4 and args[3]:
1147
- selected_device = args[3]
1148
- self.panel = SpoolerPanel(
1149
- self, wx.ID_ANY, context=self.context, selected_device=selected_device
1150
- )
1151
- self.add_module_delegate(self.panel)
1152
- _icon = wx.NullIcon
1153
- _icon.CopyFromBitmap(icons8_route.GetBitmap())
1154
- self.SetIcon(_icon)
1155
- self.SetTitle(_("Job Spooler"))
1156
- self.Layout()
1157
-
1158
- @staticmethod
1159
- def sub_register(kernel):
1160
- kernel.register("wxpane/JobSpooler", register_panel_spooler)
1161
- kernel.register(
1162
- "button/control/Spooler",
1163
- {
1164
- "label": _("Spooler"),
1165
- "icon": icons8_route,
1166
- "tip": _("Opens Spooler Window"),
1167
- "action": lambda v: kernel.console("window toggle JobSpooler\n"),
1168
- "priority": -1,
1169
- },
1170
- )
1171
-
1172
- def window_open(self):
1173
- self.panel.pane_show()
1174
-
1175
- def window_close(self):
1176
- self.panel.pane_hide()
1177
-
1178
- @staticmethod
1179
- def submenu():
1180
- return "Burning", "Spooler"
1
+ import threading
2
+ import time
3
+ from math import isinf, isnan
4
+ from pathlib import Path
5
+
6
+ import wx
7
+
8
+ # import wx.lib.mixins.listctrl as listmix
9
+ from wx import aui
10
+
11
+ from meerk40t.gui.icons import (
12
+ get_default_icon_size,
13
+ icons8_emergency_stop_button,
14
+ icons8_pause,
15
+ icons8_route,
16
+ )
17
+ from meerk40t.gui.mwindow import MWindow
18
+ from meerk40t.gui.wxutils import (
19
+ EditableListCtrl,
20
+ HoverButton,
21
+ wxButton,
22
+ wxComboBox,
23
+ wxListCtrl,
24
+ wxStaticText,
25
+ )
26
+ from meerk40t.kernel import Job, signal_listener
27
+
28
+ _ = wx.GetTranslation
29
+
30
+ JC_INDEX = 0
31
+ JC_DEVICE = 1
32
+ JC_JOBNAME = 2
33
+ JC_ENTRIES = 3
34
+ JC_STATUS = 4
35
+ JC_TYPE = 5
36
+ JC_STEPS = 6
37
+ JC_PASSES = 7
38
+ JC_PRIORITY = 8
39
+ JC_RUNTIME = 9
40
+ JC_ESTIMATE = 10
41
+
42
+ HC_INDEX = 0
43
+ HC_DEVICE = 1
44
+ HC_JOBNAME = 2
45
+ HC_START = 3
46
+ HC_END = 4
47
+ HC_RUNTIME = 5
48
+ HC_ESTIMATE = 6
49
+ HC_STEPS = 7
50
+ HC_PASSES = 8
51
+ HC_STATUS = 9
52
+ HC_JOBINFO = 10
53
+
54
+
55
+ def register_panel_spooler(window, context):
56
+ panel = SpoolerPanel(window, wx.ID_ANY, context=context)
57
+ pane = (
58
+ aui.AuiPaneInfo()
59
+ .Bottom()
60
+ .Layer(1)
61
+ .MinSize(600, 100)
62
+ .FloatingSize(600, 230)
63
+ .Caption(_("Spooler"))
64
+ .Name("spooler")
65
+ .CaptionVisible(not context.pane_lock)
66
+ .Hide()
67
+ )
68
+ pane.dock_proportion = 600
69
+ pane.control = panel
70
+ pane.helptext = _("Opens the spooler window with all job information")
71
+
72
+ window.on_pane_create(pane)
73
+ context.register("pane/spooler", pane)
74
+
75
+
76
+ # class EditableListCtrl(wx.ListCtrl, listmix.TextEditMixin):
77
+ # """TextEditMixin allows any column to be edited."""
78
+
79
+ # # ----------------------------------------------------------------------
80
+ # def __init__(
81
+ # self, parent, ID=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0
82
+ # ):
83
+ # """Constructor"""
84
+ # wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
85
+ # listmix.TextEditMixin.__init__(self)
86
+
87
+
88
+ class SpoolerPanel(wx.Panel):
89
+ def __init__(self, *args, context=None, selected_device=None, **kwds):
90
+ # begin wxGlade: SpoolerPanel.__init__
91
+ kwds["style"] = kwds.get("style", 0) | wx.TAB_TRAVERSAL
92
+ wx.Panel.__init__(self, *args, **kwds)
93
+ self.context = context
94
+ self.context.themes.set_window_colors(self)
95
+ self.SetHelpText("spooler")
96
+
97
+ self.selected_device = selected_device
98
+ self.available_devices = context.kernel.services("device")
99
+ self.filter_device = None
100
+ spools = [s.label for s in self.available_devices]
101
+ spools.insert(0, _("-- All available devices --"))
102
+ self.queue_entries = []
103
+ self.context.setting(int, "spooler_sash_position", 0)
104
+ self.context.setting(bool, "spool_history_clear_on_start", False)
105
+ self.context.setting(bool, "spool_ignore_helper_jobs", True)
106
+
107
+ self.splitter = wx.SplitterWindow(self, id=wx.ID_ANY, style=wx.SP_LIVE_UPDATE)
108
+ sty = wx.BORDER_SUNKEN
109
+
110
+ self.win_top = wx.Window(self.splitter, style=sty)
111
+ self.win_bottom = wx.Window(self.splitter, style=sty)
112
+ self.splitter.SetMinimumPaneSize(50)
113
+ self.splitter.SplitHorizontally(self.win_top, self.win_bottom, -100)
114
+ self.splitter.SetSashPosition(self.context.spooler_sash_position)
115
+ self.combo_device = wxComboBox(
116
+ self.win_top, wx.ID_ANY, choices=spools, style=wx.CB_DROPDOWN
117
+ )
118
+ self.combo_device.SetSelection(0) # All by default...
119
+ self.button_pause = wxButton(self.win_top, wx.ID_ANY, _("Pause"))
120
+ self.button_pause.SetToolTip(_("Pause/Resume the laser"))
121
+ self.button_pause.SetBitmap(
122
+ icons8_pause.GetBitmap(resize=0.5 * get_default_icon_size(self.context))
123
+ )
124
+ self.button_stop = HoverButton(self.win_top, wx.ID_ANY, _("Abort"))
125
+ self.button_stop.SetToolTip(_("Stop the laser"))
126
+ self.button_stop.SetBitmap(
127
+ icons8_emergency_stop_button.GetBitmap(
128
+ resize=0.5 * get_default_icon_size(self.context),
129
+ color=self.context.themes.get("stop_fg"),
130
+ keepalpha=True,
131
+ )
132
+ )
133
+ self.button_stop.SetBitmapFocus(
134
+ icons8_emergency_stop_button.GetBitmap(resize=0.5 * get_default_icon_size(self.context))
135
+ )
136
+ self.button_stop.SetBackgroundColour(self.context.themes.get("stop_bg"))
137
+ self.button_stop.SetForegroundColour(self.context.themes.get("stop_fg"))
138
+ self.button_stop.SetFocusColour(self.context.themes.get("stop_fg_focus"))
139
+
140
+ self.list_job_spool = wxListCtrl(
141
+ self.win_top,
142
+ wx.ID_ANY,
143
+ style=wx.LC_HRULES | wx.LC_REPORT | wx.LC_VRULES | wx.LC_SINGLE_SEL,
144
+ context=self.context,
145
+ list_name="list_spoolerjobs",
146
+ )
147
+
148
+ self.info_label = wxStaticText(
149
+ self.win_bottom, wx.ID_ANY, _("Completed jobs:")
150
+ )
151
+ self.button_clear_history = wxButton(
152
+ self.win_bottom, wx.ID_ANY, _("Clear History")
153
+ )
154
+ self.list_job_history = EditableListCtrl(
155
+ self.win_bottom,
156
+ wx.ID_ANY,
157
+ style=wx.LC_HRULES | wx.LC_REPORT | wx.LC_VRULES | wx.LC_SINGLE_SEL,
158
+ context=self.context,
159
+ list_name="list_spoolerhistory",
160
+ )
161
+
162
+ self.__set_properties()
163
+ self.__do_layout()
164
+ self.current_item = None
165
+ self.Bind(
166
+ wx.EVT_BUTTON, self.on_button_clear_history, self.button_clear_history
167
+ )
168
+ self.button_clear_history.Bind(wx.EVT_RIGHT_DOWN, self.on_right_mouse_history)
169
+ self.list_job_history.Bind(wx.EVT_RIGHT_DOWN, self.on_right_mouse_history)
170
+ self.list_job_history.Bind(
171
+ wx.EVT_LIST_BEGIN_LABEL_EDIT, self.before_history_update
172
+ )
173
+ self.list_job_history.Bind(wx.EVT_LIST_END_LABEL_EDIT, self.on_history_update)
174
+ self.Bind(wx.EVT_BUTTON, self.on_button_pause, self.button_pause)
175
+ self.Bind(wx.EVT_BUTTON, self.on_button_stop, self.button_stop)
176
+ self.Bind(wx.EVT_COMBOBOX, self.on_combo_device, self.combo_device)
177
+ self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.on_list_drag, self.list_job_spool)
178
+
179
+ self.splitter.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGED, self.on_sash_changed)
180
+ self.splitter.Bind(wx.EVT_SPLITTER_DOUBLECLICKED, self.on_sash_double)
181
+
182
+ self.list_job_spool.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_item_selected)
183
+ # self.list_job_spool.Bind(wx.EVT_LEFT_DCLICK, self.on_item_doubleclick)
184
+ self.Bind(
185
+ wx.EVT_LIST_ITEM_RIGHT_CLICK, self.on_item_rightclick, self.list_job_spool
186
+ )
187
+ # end wxGlade
188
+ self._last_invokation = 0
189
+ self.dirty = False
190
+ self.update_buffer_size = False
191
+ self.update_spooler_state = False
192
+ self.update_spooler = True
193
+
194
+ self.elements_progress = 0
195
+ self.elements_progress_total = 0
196
+ self.command_index = 0
197
+ self.listener_list = None
198
+ self.list_lookup = {}
199
+ self.map_item_key = {}
200
+ self.refresh_history()
201
+ self.set_pause_color()
202
+ if self.context.spool_history_clear_on_start:
203
+ self.clear_history()
204
+ # We set a timer job that will periodically check the spooler queue
205
+ # in case no signal was received
206
+ self.shown = False
207
+ self.update_lock = threading.Lock()
208
+ self.timerjob = Job(
209
+ process=self.update_queue,
210
+ job_name="spooler-update",
211
+ interval=5,
212
+ run_main=True,
213
+ )
214
+
215
+ def __set_properties(self):
216
+ # begin wxGlade: SpoolerPanel.__set_properties
217
+ self.combo_device.SetToolTip(_("Select the device"))
218
+ self.list_job_spool.SetToolTip(_("List and modify the queued operations"))
219
+ self.button_clear_history.SetToolTip(
220
+ _("Clear spooler history (right click for more options)")
221
+ )
222
+ self.list_job_spool.AppendColumn(_("#"), format=wx.LIST_FORMAT_LEFT, width=58)
223
+ self.list_job_spool.AppendColumn(
224
+ _("Device"),
225
+ format=wx.LIST_FORMAT_LEFT,
226
+ width=95,
227
+ )
228
+ self.list_job_spool.AppendColumn(
229
+ _("Name"), format=wx.LIST_FORMAT_LEFT, width=95
230
+ )
231
+ self.list_job_spool.AppendColumn(
232
+ _("Items"), format=wx.LIST_FORMAT_LEFT, width=45
233
+ )
234
+ self.list_job_spool.AppendColumn(
235
+ _("Status"), format=wx.LIST_FORMAT_LEFT, width=73
236
+ )
237
+ self.list_job_spool.AppendColumn(
238
+ _("Type"), format=wx.LIST_FORMAT_LEFT, width=60
239
+ )
240
+ self.list_job_spool.AppendColumn(
241
+ _("Steps"), format=wx.LIST_FORMAT_LEFT, width=73
242
+ )
243
+ self.list_job_spool.AppendColumn(
244
+ _("Passes"), format=wx.LIST_FORMAT_LEFT, width=73
245
+ )
246
+ self.list_job_spool.AppendColumn(
247
+ _("Priority"), format=wx.LIST_FORMAT_LEFT, width=73
248
+ )
249
+ self.list_job_spool.AppendColumn(
250
+ _("Runtime"), format=wx.LIST_FORMAT_LEFT, width=73
251
+ )
252
+ self.list_job_spool.AppendColumn(
253
+ _("Estimate"), format=wx.LIST_FORMAT_LEFT, width=73
254
+ )
255
+ self.list_job_spool.resize_columns()
256
+
257
+ self.list_job_history.AppendColumn(_("#"), format=wx.LIST_FORMAT_LEFT, width=48)
258
+
259
+ self.list_job_history.AppendColumn(
260
+ _("Device"), format=wx.LIST_FORMAT_LEFT, width=73
261
+ )
262
+ self.list_job_history.AppendColumn(
263
+ _("Name"), format=wx.LIST_FORMAT_LEFT, width=95
264
+ )
265
+ self.list_job_history.AppendColumn(
266
+ _("Start"), format=wx.LIST_FORMAT_LEFT, width=113
267
+ )
268
+ self.list_job_history.AppendColumn(
269
+ _("End"), format=wx.LIST_FORMAT_LEFT, width=73
270
+ )
271
+ self.list_job_history.AppendColumn(
272
+ _("Runtime"), format=wx.LIST_FORMAT_LEFT, width=73
273
+ )
274
+ self.list_job_history.AppendColumn(
275
+ _("Estimate"), format=wx.LIST_FORMAT_LEFT, width=73
276
+ )
277
+ self.list_job_history.AppendColumn(
278
+ _("Steps"), format=wx.LIST_FORMAT_LEFT, width=73
279
+ )
280
+ self.list_job_history.AppendColumn(
281
+ _("Passes"), format=wx.LIST_FORMAT_LEFT, width=73
282
+ )
283
+ self.list_job_history.AppendColumn(
284
+ _("Status"), format=wx.LIST_FORMAT_LEFT, width=73
285
+ )
286
+ self.list_job_history.AppendColumn(
287
+ _("Jobinfo"), format=wx.LIST_FORMAT_LEFT, width=wx.LIST_AUTOSIZE_USEHEADER
288
+ )
289
+ self.list_job_history.resize_columns()
290
+ # end wxGlade
291
+
292
+ def __do_layout(self):
293
+ sizer_main = wx.BoxSizer(wx.VERTICAL)
294
+
295
+ sizer_top = wx.BoxSizer(wx.VERTICAL)
296
+ sizer_bottom = wx.BoxSizer(wx.VERTICAL)
297
+ sizer_combo_cmds = wx.BoxSizer(wx.HORIZONTAL)
298
+ sizer_combo_cmds.Add(self.combo_device, 1, wx.ALIGN_CENTER_VERTICAL, 0)
299
+ sizer_combo_cmds.Add(self.button_pause, 0, wx.EXPAND, 0)
300
+ sizer_combo_cmds.Add(self.button_stop, 0, wx.EXPAND, 0)
301
+
302
+ sizer_top.Add(sizer_combo_cmds, 0, wx.EXPAND, 0)
303
+ sizer_top.Add(self.list_job_spool, 4, wx.EXPAND, 0)
304
+ self.win_top.SetSizer(sizer_top)
305
+ sizer_top.Fit(self.win_top)
306
+
307
+ hsizer = wx.BoxSizer(wx.HORIZONTAL)
308
+ hsizer.Add(self.info_label, 1, wx.ALIGN_CENTER_VERTICAL, 0)
309
+ hsizer.Add(self.button_clear_history, 0, wx.EXPAND, 0)
310
+ sizer_bottom.Add(hsizer, 0, wx.EXPAND, 0)
311
+ sizer_bottom.Add(self.list_job_history, 2, wx.EXPAND, 0)
312
+ self.win_bottom.SetSizer(sizer_bottom)
313
+ sizer_bottom.Fit(self.win_bottom)
314
+
315
+ sizer_main.Add(self.splitter, 1, wx.EXPAND, 0)
316
+ self.SetSizer(sizer_main)
317
+ sizer_main.Fit(self)
318
+ self.Layout()
319
+ # end wxGlade
320
+
321
+ def on_sash_changed(self, event):
322
+ position = self.splitter.GetSashPosition()
323
+ self.context.spooler_sash_position = position
324
+
325
+ def on_sash_double(self, event):
326
+ self.splitter.SetSashPosition(0, True)
327
+ self.context.spooler_sash_position = 0
328
+
329
+ def on_item_selected(self, event):
330
+ self.current_item = event.Index
331
+
332
+ def write_csv(self):
333
+ filename = Path(self.context.kernel.os_information["WORKDIR"]).joinpath(
334
+ "history.csv"
335
+ )
336
+ if self.filter_device:
337
+ events = self.context.logging.matching_events(
338
+ "job", device=self.filter_device
339
+ )
340
+ else:
341
+ events = self.context.logging.matching_events("job")
342
+ try:
343
+ with open(filename, "w", encoding="utf-8") as f:
344
+ simpleline = "device;jobname;start;end;duration;estimate;steps;total;loops;passes;status;info"
345
+ f.write(simpleline + "\n")
346
+ for key, info in events:
347
+ line_items = []
348
+ if info["start_time"] is None:
349
+ continue
350
+ line_items.append(str(info.get("device", "''")))
351
+ line_items.append(str(info.get("label", "''")))
352
+ line_items.append(
353
+ f"{self.datestr(info.get('start_time',0))} {self.timestr(info.get('start_time',0), True)}"
354
+ )
355
+
356
+ line_items.append(
357
+ self.timestr(
358
+ info.get("start_time", 0) + info.get("duration", 0), True
359
+ )
360
+ )
361
+ line_items.append(self.timestr(info.get("duration", 0), False))
362
+ line_items.append(self.timestr(info.get("estimate", 0), False))
363
+ line_items.append(str(info.get("steps_done", 0)))
364
+ line_items.append(str(info.get("steps_total", 0)))
365
+ line_items.append(str(info.get("loop", 0)))
366
+ # First passes then device
367
+ line_items.append(str(info.get("passes", "''")))
368
+ line_items.append(str(info.get("status", "''")))
369
+ line_items.append(str(info.get("info", "''")))
370
+ f.write(f'{";".join(line_items)}\n')
371
+
372
+ except (PermissionError, OSError, FileNotFoundError):
373
+ pass
374
+
375
+ def clear_history(self, older_than=None, job_type=None):
376
+ if self.filter_device:
377
+ to_remove = list(
378
+ self.context.logging.matching_events("job", device=self.filter_device)
379
+ )
380
+ else:
381
+ to_remove = list(self.context.logging.matching_events("job"))
382
+ for key, event in to_remove:
383
+ if event is not None and older_than is not None:
384
+ if not "start_time" in event:
385
+ continue
386
+ if (
387
+ event["start_time"] is not None
388
+ and event["start_time"] >= older_than
389
+ ):
390
+ continue
391
+ if event is not None and job_type is not None:
392
+ if not "status" in event:
393
+ continue
394
+ if event["status"] is not None and event["status"] != job_type:
395
+ continue
396
+ del self.context.logging.logs[key]
397
+ self.refresh_history()
398
+
399
+ def on_button_clear_history(self, event):
400
+ self.clear_history(older_than=None, job_type=None)
401
+
402
+ def on_right_mouse_history(self, event):
403
+ listid = self.list_job_history.GetFirstSelected()
404
+ if listid >= 0:
405
+ idx = self.list_job_history.GetItemData(listid)
406
+ key = self.map_item_key[listid]
407
+ else:
408
+ # Bad selection.
409
+ idx = -1
410
+ key = None
411
+
412
+ def on_menu_index(idx_to_delete):
413
+ def check(event_c):
414
+ del self.context.logging.logs[key]
415
+ self.refresh_history()
416
+
417
+ return check
418
+
419
+ def on_menu_time(cutoff, jobtype):
420
+ def check(event):
421
+ self.clear_history(older_than=dcutoff, job_type=djobtype)
422
+
423
+ # Store value locally
424
+ dcutoff = cutoff
425
+ djobtype = jobtype
426
+ return check
427
+
428
+ def toggle_1(event):
429
+ self.context.spool_history_clear_on_start = (
430
+ not self.context.spool_history_clear_on_start
431
+ )
432
+
433
+ def toggle_2(event):
434
+ self.context.spool_ignore_helper_jobs = (
435
+ not self.context.spool_ignore_helper_jobs
436
+ )
437
+ self.refresh_history()
438
+
439
+ now = time.time()
440
+ week_seconds = 60 * 60 * 24 * 7
441
+ options = [(_("All entries"), None, None)]
442
+ for week in range(1, 5):
443
+ cutoff_time = now - week * week_seconds
444
+ options.append(
445
+ (_("Older than {week} week").format(week=week), cutoff_time, None)
446
+ )
447
+ options.append((_("All incomplete jobs"), None, "stopped"))
448
+ menu = wx.Menu()
449
+ if idx >= 0:
450
+ menuitem = menu.Append(wx.ID_ANY, _("Delete this entry"), "")
451
+ self.Bind(
452
+ wx.EVT_MENU,
453
+ on_menu_index(idx),
454
+ id=menuitem.GetId(),
455
+ )
456
+ menu.AppendSeparator()
457
+
458
+ menuitem = menu.Append(wx.ID_ANY, _("Delete..."))
459
+ menu.Enable(menuitem.GetId(), False)
460
+
461
+ for item in options:
462
+ menuitem = menu.Append(wx.ID_ANY, item[0], "")
463
+ self.Bind(
464
+ wx.EVT_MENU,
465
+ on_menu_time(item[1], item[2]),
466
+ id=menuitem.GetId(),
467
+ )
468
+
469
+ menu.AppendSeparator()
470
+ menuitem = menu.Append(
471
+ wx.ID_ANY, _("Clear history on startup"), "", wx.ITEM_CHECK
472
+ )
473
+ menuitem.Check(self.context.spool_history_clear_on_start)
474
+ self.Bind(
475
+ wx.EVT_MENU,
476
+ toggle_1,
477
+ id=menuitem.GetId(),
478
+ )
479
+ menuitem = menu.Append(wx.ID_ANY, _("Ignore helper jobs"), "", wx.ITEM_CHECK)
480
+ menuitem.Check(self.context.spool_ignore_helper_jobs)
481
+ self.Bind(
482
+ wx.EVT_MENU,
483
+ toggle_2,
484
+ id=menuitem.GetId(),
485
+ )
486
+
487
+ item = menu.Append(wx.ID_ANY, _("Write CSV"), "", wx.ITEM_NORMAL)
488
+ self.Bind(wx.EVT_MENU, lambda e: self.write_csv(), item)
489
+
490
+ if menu.MenuItemCount != 0:
491
+ self.PopupMenu(menu)
492
+ menu.Destroy()
493
+
494
+ def on_button_pause(self, event): # wxGlade: LaserPanel.<event_handler>
495
+ self.context("pause\n")
496
+
497
+ def on_button_stop(self, event): # wxGlade: LaserPanel.<event_handler>
498
+ self.context("estop\n")
499
+
500
+ def on_combo_device(self, event=None): # wxGlade: Spooler.<event_handler>
501
+ index = self.combo_device.GetSelection()
502
+ if index == 0:
503
+ self.filter_device = None
504
+ else:
505
+ self.filter_device = self.available_devices[index - 1].label
506
+ self.update_spooler = True
507
+ self.refresh_spooler_list()
508
+ self.refresh_history()
509
+ self.set_pause_color()
510
+
511
+ def on_list_drag(self, event): # wxGlade: JobSpooler.<event_handler>
512
+ # Todo: Drag to reprioritise jobs
513
+ event.Skip()
514
+
515
+ def on_item_rightclick(self, event): # wxGlade: JobSpooler.<event_handler>
516
+ listindex = event.Index
517
+ try:
518
+ index = self.list_job_spool.GetItemData(listindex)
519
+ except AssertionError:
520
+ # Size of list_job_spool changed or is updating.
521
+ return
522
+ try:
523
+ spooler = self.queue_entries[index][0]
524
+ qindex = self.queue_entries[index][1]
525
+ element = spooler.queue[qindex]
526
+ except IndexError:
527
+ return
528
+
529
+ menu = wx.Menu()
530
+ item = menu.Append(
531
+ wx.ID_ANY,
532
+ f"{str(element)[:30]} [{spooler.context.label}]",
533
+ "",
534
+ wx.ITEM_NORMAL,
535
+ )
536
+ item.Enable(False)
537
+ can_enable = False
538
+ action = _("Remove")
539
+ remove_mode = "remove"
540
+ if element.status == "Running":
541
+ action = _("Stop")
542
+ remove_mode = "stop"
543
+ elif hasattr(element, "enabled"):
544
+ remove_mode = "remove"
545
+ if element.enabled:
546
+ action2 = _("Disable")
547
+ else:
548
+ action2 = _("Enable")
549
+ can_enable = True
550
+
551
+ item = menu.Append(
552
+ wx.ID_ANY,
553
+ f"{action}",
554
+ "",
555
+ wx.ITEM_NORMAL,
556
+ )
557
+ info_tuple = [spooler, element, remove_mode]
558
+ self.Bind(wx.EVT_MENU, self.on_menu_popup_delete(info_tuple), item)
559
+ # Are there more loops than just one?
560
+ if hasattr(element, "loops"):
561
+ # Still something to go?
562
+ if element.loops > 1 and element.loops_executed < element.loops:
563
+ item = menu.Append(
564
+ wx.ID_ANY,
565
+ _("Finish after this loop"),
566
+ _(
567
+ "Stop the current execution after the succesful execution of this loop"
568
+ ),
569
+ wx.ITEM_NORMAL,
570
+ )
571
+ info_tuple = [spooler, element]
572
+ self.Bind(wx.EVT_MENU, self.on_menu_popup_stop_loop(info_tuple), item)
573
+ if not isinf(element.loops):
574
+ item = menu.Append(
575
+ wx.ID_ANY,
576
+ _("add another loop"),
577
+ _("add another loop to this job"),
578
+ wx.ITEM_NORMAL,
579
+ )
580
+ info_tuple = [spooler, element]
581
+ self.Bind(wx.EVT_MENU, self.on_menu_popup_add_loop(info_tuple), item)
582
+
583
+ if can_enable:
584
+ item = menu.Append(
585
+ wx.ID_ANY,
586
+ f"{action2}",
587
+ "",
588
+ wx.ITEM_NORMAL,
589
+ )
590
+ info_tuple = [spooler, element]
591
+ self.Bind(wx.EVT_MENU, self.on_menu_popup_toggle_enable(info_tuple), item)
592
+ menu.AppendSeparator()
593
+ item = menu.Append(wx.ID_ANY, _("Clear All"), "", wx.ITEM_NORMAL)
594
+ self.Bind(wx.EVT_MENU, self.on_menu_popup_clear(element), item)
595
+
596
+ self.PopupMenu(menu)
597
+ menu.Destroy()
598
+
599
+ def on_menu_popup_clear(self, element=None):
600
+ def clear(event=None):
601
+ if self.context.kernel.yesno(
602
+ _("Do you really want to delete all entries?"), caption=_("Spooler")
603
+ ):
604
+ spoolers = []
605
+ for device in self.available_devices:
606
+ addit = True
607
+ if (
608
+ self.filter_device is not None
609
+ and device.label != self.filter_device
610
+ ):
611
+ addit = False
612
+ if addit:
613
+ spoolers.append(device.spooler)
614
+ for spooler in spoolers:
615
+ spooler.clear_queue()
616
+ self.refresh_spooler_list()
617
+
618
+ return clear
619
+
620
+ def on_menu_popup_delete(self, element):
621
+ def delete(event=None):
622
+ spooler = element[0]
623
+ mode = element[2]
624
+ job = element[1]
625
+ spooler.remove(job)
626
+ # That will remove the job but create a log entry if needed.
627
+ if mode == "stop":
628
+ if hasattr(job, "stop"):
629
+ job.stop()
630
+ else:
631
+ # Force stop of laser.
632
+ self.context("estop\n")
633
+ self.refresh_spooler_list()
634
+
635
+ return delete
636
+
637
+ def on_menu_popup_toggle_enable(self, element):
638
+ def routine(event=None):
639
+ spooler = element[0]
640
+ job = element[1]
641
+ job.enabled = not job.enabled
642
+ self.refresh_spooler_list()
643
+
644
+ return routine
645
+
646
+ # def on_menu_popup_next_placement(self, element):
647
+ # def routine(event=None):
648
+ # spooler = element[0]
649
+ # job = element[1]
650
+ # if hasattr(job, "jump_to_next"):
651
+ # job.jump_to_next()
652
+ # self.refresh_spooler_list()
653
+
654
+ # return routine
655
+
656
+ def on_menu_popup_stop_loop(self, element):
657
+ def routine(event=None):
658
+ spooler = element[0]
659
+ job = element[1]
660
+ if hasattr(job, "stop_after_loop"):
661
+ job.stop_after_loop()
662
+ self.refresh_spooler_list()
663
+
664
+ return routine
665
+
666
+ def on_menu_popup_add_loop(self, element):
667
+ def routine(event=None):
668
+ spooler = element[0]
669
+ job = element[1]
670
+ if hasattr(job, "add_another_loop"):
671
+ job.add_another_loop()
672
+ self.refresh_spooler_list()
673
+
674
+ return routine
675
+
676
+ def pane_show(self, *args):
677
+ self.shown = True
678
+ self.context.schedule(self.timerjob)
679
+ self.refresh_spooler_list()
680
+
681
+ def pane_hide(self, *args):
682
+ self.context.unschedule(self.timerjob)
683
+ self.shown = False
684
+
685
+ @staticmethod
686
+ def _name_str(named_obj):
687
+ try:
688
+ return named_obj.__name__
689
+ except AttributeError:
690
+ return str(named_obj)
691
+
692
+ def refresh_spooler_list(self):
693
+ if not self.update_spooler:
694
+ return
695
+ try:
696
+ self.list_job_spool.DeleteAllItems()
697
+ except RuntimeError:
698
+ return
699
+ self.queue_entries = []
700
+ queue_idx = -1
701
+ for device in self.available_devices:
702
+ spooler = device.spooler
703
+ if spooler is None:
704
+ continue
705
+ if self.filter_device is not None and self.filter_device != device.label:
706
+ continue
707
+ for idx, e in enumerate(spooler.queue):
708
+ self.queue_entries.append([spooler, idx])
709
+ queue_idx += 1
710
+ # Idx, Status, Type, Passes, Priority, Runtime, Estimate
711
+ m = self.list_job_spool.InsertItem(idx, f"#{idx}")
712
+ list_id = m
713
+ spool_obj = e
714
+
715
+ if list_id != -1:
716
+ self.list_job_spool.SetItemData(list_id, queue_idx)
717
+ # DEVICE
718
+ self.list_job_spool.SetItem(list_id, JC_DEVICE, device.label)
719
+ # Jobname
720
+ to_display = ""
721
+ if hasattr(spool_obj, "label"):
722
+ to_display = spool_obj.label
723
+ if to_display is None:
724
+ to_display = ""
725
+ if to_display == "":
726
+ to_display = SpoolerPanel._name_str(spool_obj)
727
+ if to_display.endswith(" items"):
728
+ # Look for last ':' and remove everything from there
729
+ cpos = -1
730
+ lpos = -1
731
+ while True:
732
+ lpos = to_display.find(":", lpos + 1)
733
+ if lpos == -1:
734
+ break
735
+ cpos = lpos
736
+ if cpos > 0:
737
+ to_display = to_display[:cpos]
738
+
739
+ self.list_job_spool.SetItem(list_id, JC_JOBNAME, to_display)
740
+ # Entries
741
+ joblen = 1
742
+ try:
743
+ if hasattr(spool_obj, "items"):
744
+ joblen = len(spool_obj.items)
745
+ elif hasattr(spool_obj, "elements"):
746
+ joblen = len(spool_obj.elements)
747
+ except AttributeError:
748
+ joblen = 1
749
+ self.list_job_spool.SetItem(list_id, JC_ENTRIES, str(joblen))
750
+ # STATUS
751
+ self.list_job_spool.SetItem(
752
+ list_id, JC_STATUS, str(spool_obj.status)
753
+ )
754
+
755
+ # TYPE
756
+ try:
757
+ self.list_job_spool.SetItem(
758
+ list_id,
759
+ JC_TYPE,
760
+ str(spool_obj.__class__.__name__),
761
+ )
762
+ except AttributeError:
763
+ pass
764
+
765
+ # STEPS
766
+ try:
767
+ if spool_obj.steps_total == 0:
768
+ spool_obj.calc_steps()
769
+ info_s = f"{spool_obj.steps_done}/{spool_obj.steps_total}"
770
+ if hasattr(spooler, "driver"):
771
+ if hasattr(spooler.driver, "get_internal_queue_status"):
772
+ internal_current, internal_total = spooler.driver.get_internal_queue_status()
773
+ if internal_current != 0:
774
+ info_s += f" ({internal_current}/{internal_total})"
775
+ except AttributeError:
776
+ info_s = "-"
777
+ self.list_job_spool.SetItem(list_id, JC_STEPS, info_s)
778
+ # PASSES
779
+ try:
780
+ loop = spool_obj.loops_executed
781
+ total = spool_obj.loops
782
+ # No invalid values please
783
+ if loop is None:
784
+ loop = 0
785
+ if total is None:
786
+ total = 1
787
+
788
+ if isinf(total):
789
+ total = ""
790
+ self.list_job_spool.SetItem(
791
+ list_id, JC_PASSES, f"{loop}/{total}"
792
+ )
793
+ except AttributeError:
794
+ self.list_job_spool.SetItem(list_id, JC_PASSES, "-")
795
+
796
+ # Priority
797
+ try:
798
+ self.list_job_spool.SetItem(
799
+ list_id,
800
+ JC_PRIORITY,
801
+ str(spool_obj.priority),
802
+ )
803
+ except AttributeError:
804
+ self.list_job_spool.SetItem(list_id, JC_PRIORITY, "-")
805
+
806
+ # Runtime
807
+ try:
808
+ t = spool_obj.elapsed_time()
809
+ hours, remainder = divmod(t, 3600)
810
+ minutes, seconds = divmod(remainder, 60)
811
+ runtime = f"{int(hours)}:{str(int(minutes)).zfill(2)}:{str(int(seconds)).zfill(2)}"
812
+ self.list_job_spool.SetItem(list_id, JC_RUNTIME, runtime)
813
+ except AttributeError:
814
+ self.list_job_spool.SetItem(list_id, JC_RUNTIME, "-")
815
+
816
+ # Estimate Time
817
+ try:
818
+ t = spool_obj.estimate_time()
819
+ if isinf(t):
820
+ runtime = "∞"
821
+ else:
822
+ hours, remainder = divmod(t, 3600)
823
+ minutes, seconds = divmod(remainder, 60)
824
+ runtime = f"{int(hours)}:{str(int(minutes)).zfill(2)}:{str(int(seconds)).zfill(2)}"
825
+ self.list_job_spool.SetItem(list_id, JC_ESTIMATE, runtime)
826
+ except AttributeError:
827
+ self.list_job_spool.SetItem(list_id, JC_ESTIMATE, "-")
828
+ self._last_invokation = time.time()
829
+
830
+ @staticmethod
831
+ def timestr(t, oneday):
832
+ if t is None:
833
+ return ""
834
+ if isinstance(t, str):
835
+ return t
836
+ if isinf(t) or isnan(t) or t < 0:
837
+ return "∞"
838
+
839
+ if oneday:
840
+ localt = time.localtime(t)
841
+ hours = localt[3]
842
+ minutes = localt[4]
843
+ seconds = localt[5]
844
+ else:
845
+ hours, remainder = divmod(t, 3600)
846
+ minutes, seconds = divmod(remainder, 60)
847
+ # Military time display
848
+ result = (
849
+ f"{int(hours)}:{str(int(minutes)).zfill(2)}:{str(int(seconds)).zfill(2)}"
850
+ )
851
+ return result
852
+
853
+ @staticmethod
854
+ def datestr(t):
855
+ if t is None:
856
+ return ""
857
+ if isinstance(t, str):
858
+ return t
859
+ localt = time.localtime(t)
860
+ lyear = localt[0]
861
+ syear = lyear % 100
862
+ lmonth = int(localt[1])
863
+ lday = localt[2]
864
+ lhour = localt[3]
865
+ lminute = localt[4]
866
+ lsecond = localt[5]
867
+ # wx.DateTime has a bug: it does always provide the dateformat
868
+ # string with a month representation one number too high, so
869
+ # wx.DateTime(31,01,1999)
870
+ # Arbitrary but with different figures
871
+ # Alas this is the only simple method to get locale relevant dateformat...
872
+ pattern = None
873
+ try:
874
+ loc = wx.Locale()
875
+ pattern = loc.GetOSInfo(wx.LOCALE_SHORT_DATE_FMT, wx.LOCALE_CAT_DEFAULT)
876
+ except AttributeError:
877
+ # That's not available, so we use the other algorithm instead...
878
+ pass
879
+ if pattern is not None:
880
+ pattern = pattern.replace("%d", "{dd}")
881
+ pattern = pattern.replace("%m", "{mm}")
882
+ pattern = pattern.replace("%y", "{y}")
883
+ pattern = pattern.replace("%Y", "{yy}")
884
+ if pattern is None:
885
+ wxdt = wx.DateTime(31, 7, 2022)
886
+ pattern = wxdt.FormatDate()
887
+ pattern = pattern.replace("2022", "{yy}")
888
+ pattern = pattern.replace("22", "{y}")
889
+ pattern = pattern.replace("31", "{dd}")
890
+ # That would be the right thing, so if the bug is ever fixed, that will work
891
+ pattern = pattern.replace("07", "{mm}")
892
+ pattern = pattern.replace("7", "{mm}")
893
+ # And this is needed to deal with the bug...
894
+ pattern = pattern.replace("08", "{mm}")
895
+ pattern = pattern.replace("8", "{mm}")
896
+ # Deal with years seperately
897
+ pattern = pattern.replace("{y}", str(syear).zfill(2))
898
+ pattern = pattern.replace("{yy}", str(lyear).zfill(2))
899
+ result = pattern.format(
900
+ dd=str(lday).zfill(2),
901
+ mm=str(lmonth).zfill(2),
902
+ )
903
+ # Just to show the bug...
904
+ # result1 = f"{int(lday)}.{str(int(lmonth)).zfill(2)}.{str(int(lyear)).zfill(2)}"
905
+ # wxdt = wx.DateTime(lday, lmonth, lyear, lhour, lminute, lsecond)
906
+ # result2 = wxdt.FormatDate()
907
+ # print(f"res={result}, wxd={result2}, manual={result1}, pattern={pattern}")
908
+ return result
909
+
910
+ def refresh_history(self):
911
+ self.list_job_history.DeleteAllItems()
912
+ self.map_item_key.clear()
913
+ if self.filter_device:
914
+ if self.context.spool_ignore_helper_jobs:
915
+ events = self.context.logging.matching_events(
916
+ "job", device=self.filter_device, important=True
917
+ )
918
+ else:
919
+ events = self.context.logging.matching_events(
920
+ "job", device=self.filter_device
921
+ )
922
+
923
+ else:
924
+ if self.context.spool_ignore_helper_jobs:
925
+ events = self.context.logging.matching_events("job", important=True)
926
+ else:
927
+ events = self.context.logging.matching_events("job")
928
+ has_data = False
929
+ for idx, event_and_key in enumerate(reversed(list(events))):
930
+ has_data = True
931
+ key, info = event_and_key
932
+ list_id = self.list_job_history.InsertItem(
933
+ self.list_job_history.GetItemCount(), f"#{idx}"
934
+ )
935
+ self.map_item_key[list_id] = key
936
+ start_time = info.get("start_time", 0)
937
+ if start_time is None:
938
+ start_time = 0
939
+ duration = info.get("duration", 0)
940
+ if duration is None:
941
+ duration = 0
942
+ self.list_job_history.SetItem(list_id, HC_JOBNAME, str(info.get("label")))
943
+ self.list_job_history.SetItem(
944
+ list_id,
945
+ HC_START,
946
+ f"{self.datestr(start_time)} {self.timestr(start_time, True)}",
947
+ )
948
+
949
+ self.list_job_history.SetItem(
950
+ list_id,
951
+ HC_END,
952
+ self.timestr(start_time + duration, True),
953
+ )
954
+ self.list_job_history.SetItem(
955
+ list_id,
956
+ HC_RUNTIME,
957
+ self.timestr(duration, False),
958
+ )
959
+ nr_loop = info.get("loop")
960
+ nr_total = info.get("total")
961
+ if nr_total is None:
962
+ if nr_loop is None:
963
+ passes_str = "n/a"
964
+ else:
965
+ passes_str = f"{nr_loop}"
966
+ elif isinf(float(nr_total)):
967
+ passes_str = f"{nr_loop}/∞"
968
+ else:
969
+ passes_str = f"{nr_loop}/{nr_total}"
970
+ self.list_job_history.SetItem(
971
+ list_id,
972
+ HC_PASSES,
973
+ passes_str,
974
+ )
975
+ self.list_job_history.SetItem(list_id, HC_DEVICE, str(info.get("device")))
976
+ self.list_job_history.SetItem(
977
+ list_id, HC_STATUS, str(info.get("status", ""))
978
+ )
979
+ self.list_job_history.SetItem(
980
+ list_id, HC_JOBINFO, str(info.get("info", ""))
981
+ )
982
+ self.list_job_history.SetItem(
983
+ list_id,
984
+ HC_ESTIMATE,
985
+ self.timestr(info.get("estimate", 0), False),
986
+ )
987
+ self.list_job_history.SetItem(
988
+ list_id,
989
+ HC_STEPS,
990
+ f"{info.get('steps_done',0)}/{info.get('steps_total',0)}",
991
+ )
992
+ self.list_job_history.SetItemData(list_id, idx)
993
+ if has_data:
994
+ self.list_job_history.Select(0)
995
+
996
+ def before_history_update(self, event):
997
+ list_id = event.GetIndex() # Get the current row
998
+ col_id = event.GetColumn() # Get the current column
999
+ if col_id == HC_JOBINFO:
1000
+ event.Allow()
1001
+ else:
1002
+ event.Veto()
1003
+
1004
+ def on_history_update(self, event):
1005
+ list_id = event.GetIndex() # Get the current row
1006
+ col_id = event.GetColumn() # Get the current column
1007
+ new_data = event.GetLabel() # Get the changed data
1008
+ if list_id >= 0 and col_id == HC_JOBINFO:
1009
+ idx = self.list_job_history.GetItemData(list_id)
1010
+ key = self.map_item_key[idx]
1011
+ self.context.logging.logs[key]["info"] = new_data
1012
+
1013
+ # Set the new data in the listctrl
1014
+ self.list_job_history.SetItem(list_id, col_id, new_data)
1015
+
1016
+ def set_pause_color(self):
1017
+ new_bg_color = None
1018
+ new_fg_color = None
1019
+ new_caption = _("Pause")
1020
+ try:
1021
+ if self.context.device.driver.paused:
1022
+ new_bg_color = self.context.themes.get("pause_bg")
1023
+ new_fg_color = self.context.themes.get("pause_fg")
1024
+ new_caption = _("Resume")
1025
+ except AttributeError:
1026
+ pass
1027
+ self.button_pause.SetBackgroundColour(new_bg_color)
1028
+ self.button_pause.SetForegroundColour(new_fg_color)
1029
+ self.button_pause.SetLabelText(new_caption)
1030
+
1031
+ @signal_listener("pause")
1032
+ def on_device_pause_toggle(self, origin, *args):
1033
+ self.set_pause_color()
1034
+
1035
+ @signal_listener("activate;device")
1036
+ def on_activate_device(self, origin, device):
1037
+ self.available_devices = self.context.kernel.services("device")
1038
+ self.selected_device = self.context.device
1039
+ index = -1
1040
+ for i, s in enumerate(self.available_devices):
1041
+ if s is self.selected_device:
1042
+ index = i + 1
1043
+ break
1044
+ spools = [s.label for s in self.available_devices]
1045
+ spools.insert(0, _("-- All available devices --"))
1046
+ # This might not be relevant if you have a stable device set, but there might always be
1047
+ # changes to add / rename devices etc.
1048
+ if self.combo_device.GetSelection() == 0:
1049
+ # all-devices is a superset of any device, so we can leave it...
1050
+ index = 0
1051
+ self.combo_device.Clear()
1052
+ self.combo_device.SetItems(spools)
1053
+ self.combo_device.SetSelection(index)
1054
+ self.on_combo_device(None)
1055
+ self.set_pause_color()
1056
+
1057
+ @signal_listener("spooler;completed")
1058
+ def on_spooler_completed(self, origin, *args):
1059
+ self.refresh_history()
1060
+
1061
+ @signal_listener("spooler;queue")
1062
+ @signal_listener("spooler;idle")
1063
+ @signal_listener("spooler;realtime")
1064
+ def on_spooler_update(self, origin, value, *args, **kwargs):
1065
+ self.update_spooler = True
1066
+ self.refresh_spooler_list()
1067
+
1068
+ @signal_listener("driver;position")
1069
+ @signal_listener("emulator;position")
1070
+ @signal_listener("pipe;usb_status")
1071
+ def on_device_update(self, origin, *args):
1072
+ doit = True
1073
+ with self.update_lock:
1074
+ # Only update every 2 seconds or so
1075
+ dtime = time.time()
1076
+ if dtime - self._last_invokation < 2:
1077
+ doit = False
1078
+ else:
1079
+ self._last_invokation = dtime
1080
+ if not doit:
1081
+ return
1082
+
1083
+ # Two things (at least) could go wrong:
1084
+ # 1) You are in the wrong queue, i.e. there's a job running in the background a
1085
+ # that provides an update but the user has changed the device so a different
1086
+ # queue is selected
1087
+ # 2) As this is a signal it may come later, i.e. the job has already finished
1088
+ #
1089
+ # The checks here are rather basic and need to be revisited
1090
+ refresh_needed = False
1091
+ try:
1092
+ listctrl = self.list_job_spool
1093
+ except RuntimeError:
1094
+ return
1095
+ for list_id, entry in enumerate(self.queue_entries):
1096
+ spooler = entry[0]
1097
+ qindex = entry[1]
1098
+ if qindex >= len(spooler.queue):
1099
+ # This item is nowhere to be found
1100
+ refresh_needed = True
1101
+ continue
1102
+ spool_obj = spooler.queue[qindex]
1103
+ try:
1104
+ t = spool_obj.elapsed_time()
1105
+ hours, remainder = divmod(t, 3600)
1106
+ minutes, seconds = divmod(remainder, 60)
1107
+ runtime = f"{int(hours)}:{str(int(minutes)).zfill(2)}:{str(int(seconds)).zfill(2)}"
1108
+ if list_id < self.list_job_spool.GetItemCount():
1109
+ self.list_job_spool.SetItem(list_id, JC_RUNTIME, runtime)
1110
+ except (AttributeError, AssertionError):
1111
+ if list_id < self.list_job_spool.GetItemCount():
1112
+ self.list_job_spool.SetItem(list_id, JC_RUNTIME, "-")
1113
+ else:
1114
+ refresh_needed = True
1115
+ except RuntimeError:
1116
+ # Form no longer valid
1117
+ return
1118
+
1119
+ try:
1120
+ if spool_obj.steps_total == 0:
1121
+ spool_obj.calc_steps()
1122
+ info_s = f"{spool_obj.steps_done}/{spool_obj.steps_total}"
1123
+ if hasattr(spooler, "driver"):
1124
+ if hasattr(spooler.driver, "get_internal_queue_status"):
1125
+ internal_current, internal_total = spooler.driver.get_internal_queue_status()
1126
+ if internal_current != 0:
1127
+ info_s += f" ({internal_current}/{internal_total})"
1128
+ except AttributeError:
1129
+ info_s = "-"
1130
+ if list_id >= self.list_job_spool.GetItemCount():
1131
+ refresh_needed = True
1132
+
1133
+ self.list_job_spool.SetItem(list_id, JC_STEPS, info_s)
1134
+ try:
1135
+ loop = spool_obj.loops_executed
1136
+ total = spool_obj.loops
1137
+
1138
+ if isinf(total):
1139
+ total = "∞"
1140
+ pass_str = f"{loop}/{total}"
1141
+ self.list_job_spool.SetItem(list_id, JC_PASSES, pass_str)
1142
+ except AttributeError:
1143
+ if list_id < self.list_job_spool.GetItemCount():
1144
+ self.list_job_spool.SetItem(list_id, JC_PASSES, "-")
1145
+ else:
1146
+ refresh_needed = True
1147
+
1148
+ # Estimate Time
1149
+ try:
1150
+ t = spool_obj.estimate_time()
1151
+ if isinf(t):
1152
+ runtime = "∞"
1153
+ else:
1154
+ hours, remainder = divmod(t, 3600)
1155
+ minutes, seconds = divmod(remainder, 60)
1156
+ runtime = f"{int(hours)}:{str(int(minutes)).zfill(2)}:{str(int(seconds)).zfill(2)}"
1157
+
1158
+ if list_id < self.list_job_spool.GetItemCount():
1159
+ self.list_job_spool.SetItem(list_id, JC_ESTIMATE, runtime)
1160
+ except (AttributeError, AssertionError):
1161
+ if list_id < self.list_job_spool.GetItemCount():
1162
+ self.list_job_spool.SetItem(list_id, JC_ESTIMATE, "-")
1163
+ else:
1164
+ refresh_needed = True
1165
+ if refresh_needed:
1166
+ self.refresh_spooler_list()
1167
+ self.refresh_history()
1168
+
1169
+ def update_queue(self):
1170
+ if self.shown:
1171
+ self.on_device_update(None)
1172
+
1173
+ def pane_show(self):
1174
+ self.list_job_history.load_column_widths()
1175
+ self.list_job_spool.load_column_widths()
1176
+
1177
+ def pane_hide(self):
1178
+ self.list_job_history.save_column_widths()
1179
+ self.list_job_spool.save_column_widths()
1180
+
1181
+
1182
+ class JobSpooler(MWindow):
1183
+ def __init__(self, *args, **kwds):
1184
+ super().__init__(600, 400, *args, **kwds)
1185
+ selected_device = None
1186
+ if len(args) >= 4 and args[3]:
1187
+ selected_device = args[3]
1188
+ self.panel = SpoolerPanel(
1189
+ self, wx.ID_ANY, context=self.context, selected_device=selected_device
1190
+ )
1191
+ self.sizer.Add(self.panel, 1, wx.EXPAND, 0)
1192
+ self.add_module_delegate(self.panel)
1193
+ _icon = wx.NullIcon
1194
+ _icon.CopyFromBitmap(icons8_route.GetBitmap())
1195
+ self.SetIcon(_icon)
1196
+ self.SetTitle(_("Job Spooler"))
1197
+ self.Layout()
1198
+ self.restore_aspect(honor_initial_values=True)
1199
+
1200
+ @staticmethod
1201
+ def sub_register(kernel):
1202
+ kernel.register("wxpane/JobSpooler", register_panel_spooler)
1203
+ kernel.register(
1204
+ "button/control/Spooler",
1205
+ {
1206
+ "label": _("Spooler"),
1207
+ "icon": icons8_route,
1208
+ "tip": _("Opens Spooler Window"),
1209
+ "help": "spooler",
1210
+ "action": lambda v: kernel.console("window toggle JobSpooler\n"),
1211
+ "priority": -1,
1212
+ },
1213
+ )
1214
+
1215
+ def window_open(self):
1216
+ self.panel.pane_show()
1217
+
1218
+ def window_close(self):
1219
+ self.panel.pane_hide()
1220
+
1221
+ @staticmethod
1222
+ def submenu():
1223
+ return "Burning", "Spooler"
1224
+
1225
+ @staticmethod
1226
+ def helptext():
1227
+ return _("Opens the spooler window with all job information")