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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (445) hide show
  1. meerk40t/__init__.py +1 -1
  2. meerk40t/balormk/balor_params.py +167 -167
  3. meerk40t/balormk/clone_loader.py +457 -457
  4. meerk40t/balormk/controller.py +1566 -1512
  5. meerk40t/balormk/cylindermod.py +64 -0
  6. meerk40t/balormk/device.py +966 -1959
  7. meerk40t/balormk/driver.py +778 -591
  8. meerk40t/balormk/galvo_commands.py +1195 -0
  9. meerk40t/balormk/gui/balorconfig.py +237 -111
  10. meerk40t/balormk/gui/balorcontroller.py +191 -184
  11. meerk40t/balormk/gui/baloroperationproperties.py +116 -115
  12. meerk40t/balormk/gui/corscene.py +845 -0
  13. meerk40t/balormk/gui/gui.py +179 -147
  14. meerk40t/balormk/livelightjob.py +466 -382
  15. meerk40t/balormk/mock_connection.py +131 -109
  16. meerk40t/balormk/plugin.py +133 -135
  17. meerk40t/balormk/usb_connection.py +306 -301
  18. meerk40t/camera/__init__.py +1 -1
  19. meerk40t/camera/camera.py +514 -397
  20. meerk40t/camera/gui/camerapanel.py +1241 -1095
  21. meerk40t/camera/gui/gui.py +58 -58
  22. meerk40t/camera/plugin.py +441 -399
  23. meerk40t/ch341/__init__.py +27 -27
  24. meerk40t/ch341/ch341device.py +628 -628
  25. meerk40t/ch341/libusb.py +595 -589
  26. meerk40t/ch341/mock.py +171 -171
  27. meerk40t/ch341/windriver.py +157 -157
  28. meerk40t/constants.py +13 -0
  29. meerk40t/core/__init__.py +1 -1
  30. meerk40t/core/bindalias.py +550 -539
  31. meerk40t/core/core.py +47 -47
  32. meerk40t/core/cutcode/cubiccut.py +73 -73
  33. meerk40t/core/cutcode/cutcode.py +315 -312
  34. meerk40t/core/cutcode/cutgroup.py +141 -137
  35. meerk40t/core/cutcode/cutobject.py +192 -185
  36. meerk40t/core/cutcode/dwellcut.py +37 -37
  37. meerk40t/core/cutcode/gotocut.py +29 -29
  38. meerk40t/core/cutcode/homecut.py +29 -29
  39. meerk40t/core/cutcode/inputcut.py +34 -34
  40. meerk40t/core/cutcode/linecut.py +33 -33
  41. meerk40t/core/cutcode/outputcut.py +34 -34
  42. meerk40t/core/cutcode/plotcut.py +335 -335
  43. meerk40t/core/cutcode/quadcut.py +61 -61
  44. meerk40t/core/cutcode/rastercut.py +168 -148
  45. meerk40t/core/cutcode/waitcut.py +34 -34
  46. meerk40t/core/cutplan.py +1843 -1316
  47. meerk40t/core/drivers.py +330 -329
  48. meerk40t/core/elements/align.py +801 -669
  49. meerk40t/core/elements/branches.py +1844 -1507
  50. meerk40t/core/elements/clipboard.py +229 -219
  51. meerk40t/core/elements/element_treeops.py +4561 -2837
  52. meerk40t/core/elements/element_types.py +125 -105
  53. meerk40t/core/elements/elements.py +4329 -3617
  54. meerk40t/core/elements/files.py +117 -64
  55. meerk40t/core/elements/geometry.py +473 -224
  56. meerk40t/core/elements/grid.py +467 -316
  57. meerk40t/core/elements/materials.py +158 -94
  58. meerk40t/core/elements/notes.py +50 -38
  59. meerk40t/core/elements/offset_clpr.py +933 -912
  60. meerk40t/core/elements/offset_mk.py +963 -955
  61. meerk40t/core/elements/penbox.py +339 -267
  62. meerk40t/core/elements/placements.py +300 -83
  63. meerk40t/core/elements/render.py +785 -687
  64. meerk40t/core/elements/shapes.py +2618 -2092
  65. meerk40t/core/elements/trace.py +651 -563
  66. meerk40t/core/elements/tree_commands.py +415 -409
  67. meerk40t/core/elements/undo_redo.py +116 -58
  68. meerk40t/core/elements/wordlist.py +319 -200
  69. meerk40t/core/exceptions.py +9 -9
  70. meerk40t/core/laserjob.py +220 -220
  71. meerk40t/core/logging.py +63 -63
  72. meerk40t/core/node/blobnode.py +83 -86
  73. meerk40t/core/node/bootstrap.py +105 -103
  74. meerk40t/core/node/branch_elems.py +40 -31
  75. meerk40t/core/node/branch_ops.py +45 -38
  76. meerk40t/core/node/branch_regmark.py +48 -41
  77. meerk40t/core/node/cutnode.py +29 -32
  78. meerk40t/core/node/effect_hatch.py +375 -257
  79. meerk40t/core/node/effect_warp.py +398 -0
  80. meerk40t/core/node/effect_wobble.py +441 -309
  81. meerk40t/core/node/elem_ellipse.py +404 -309
  82. meerk40t/core/node/elem_image.py +1082 -801
  83. meerk40t/core/node/elem_line.py +358 -292
  84. meerk40t/core/node/elem_path.py +259 -201
  85. meerk40t/core/node/elem_point.py +129 -102
  86. meerk40t/core/node/elem_polyline.py +310 -246
  87. meerk40t/core/node/elem_rect.py +376 -286
  88. meerk40t/core/node/elem_text.py +445 -418
  89. meerk40t/core/node/filenode.py +59 -40
  90. meerk40t/core/node/groupnode.py +138 -74
  91. meerk40t/core/node/image_processed.py +777 -766
  92. meerk40t/core/node/image_raster.py +156 -113
  93. meerk40t/core/node/layernode.py +31 -31
  94. meerk40t/core/node/mixins.py +135 -107
  95. meerk40t/core/node/node.py +1427 -1304
  96. meerk40t/core/node/nutils.py +117 -114
  97. meerk40t/core/node/op_cut.py +462 -335
  98. meerk40t/core/node/op_dots.py +296 -251
  99. meerk40t/core/node/op_engrave.py +414 -311
  100. meerk40t/core/node/op_image.py +755 -369
  101. meerk40t/core/node/op_raster.py +787 -522
  102. meerk40t/core/node/place_current.py +37 -40
  103. meerk40t/core/node/place_point.py +329 -126
  104. meerk40t/core/node/refnode.py +58 -47
  105. meerk40t/core/node/rootnode.py +225 -219
  106. meerk40t/core/node/util_console.py +48 -48
  107. meerk40t/core/node/util_goto.py +84 -65
  108. meerk40t/core/node/util_home.py +61 -61
  109. meerk40t/core/node/util_input.py +102 -102
  110. meerk40t/core/node/util_output.py +102 -102
  111. meerk40t/core/node/util_wait.py +65 -65
  112. meerk40t/core/parameters.py +709 -707
  113. meerk40t/core/planner.py +875 -785
  114. meerk40t/core/plotplanner.py +656 -652
  115. meerk40t/core/space.py +120 -113
  116. meerk40t/core/spoolers.py +706 -705
  117. meerk40t/core/svg_io.py +1836 -1549
  118. meerk40t/core/treeop.py +534 -445
  119. meerk40t/core/undos.py +278 -124
  120. meerk40t/core/units.py +784 -680
  121. meerk40t/core/view.py +393 -322
  122. meerk40t/core/webhelp.py +62 -62
  123. meerk40t/core/wordlist.py +513 -504
  124. meerk40t/cylinder/cylinder.py +247 -0
  125. meerk40t/cylinder/gui/cylindersettings.py +41 -0
  126. meerk40t/cylinder/gui/gui.py +24 -0
  127. meerk40t/device/__init__.py +1 -1
  128. meerk40t/device/basedevice.py +322 -123
  129. meerk40t/device/devicechoices.py +50 -0
  130. meerk40t/device/dummydevice.py +163 -128
  131. meerk40t/device/gui/defaultactions.py +618 -602
  132. meerk40t/device/gui/effectspanel.py +114 -0
  133. meerk40t/device/gui/formatterpanel.py +253 -290
  134. meerk40t/device/gui/warningpanel.py +337 -260
  135. meerk40t/device/mixins.py +13 -13
  136. meerk40t/dxf/__init__.py +1 -1
  137. meerk40t/dxf/dxf_io.py +766 -554
  138. meerk40t/dxf/plugin.py +47 -35
  139. meerk40t/external_plugins.py +79 -79
  140. meerk40t/external_plugins_build.py +28 -28
  141. meerk40t/extra/cag.py +112 -116
  142. meerk40t/extra/coolant.py +403 -0
  143. meerk40t/extra/encode_detect.py +198 -0
  144. meerk40t/extra/ezd.py +1165 -1165
  145. meerk40t/extra/hershey.py +835 -340
  146. meerk40t/extra/imageactions.py +322 -316
  147. meerk40t/extra/inkscape.py +630 -622
  148. meerk40t/extra/lbrn.py +424 -424
  149. meerk40t/extra/outerworld.py +284 -0
  150. meerk40t/extra/param_functions.py +1542 -1556
  151. meerk40t/extra/potrace.py +257 -253
  152. meerk40t/extra/serial_exchange.py +118 -0
  153. meerk40t/extra/updater.py +602 -453
  154. meerk40t/extra/vectrace.py +147 -146
  155. meerk40t/extra/winsleep.py +83 -83
  156. meerk40t/extra/xcs_reader.py +597 -0
  157. meerk40t/fill/fills.py +781 -335
  158. meerk40t/fill/patternfill.py +1061 -1061
  159. meerk40t/fill/patterns.py +614 -567
  160. meerk40t/grbl/control.py +87 -87
  161. meerk40t/grbl/controller.py +990 -903
  162. meerk40t/grbl/device.py +1081 -768
  163. meerk40t/grbl/driver.py +989 -771
  164. meerk40t/grbl/emulator.py +532 -497
  165. meerk40t/grbl/gcodejob.py +783 -767
  166. meerk40t/grbl/gui/grblconfiguration.py +373 -298
  167. meerk40t/grbl/gui/grblcontroller.py +485 -271
  168. meerk40t/grbl/gui/grblhardwareconfig.py +269 -153
  169. meerk40t/grbl/gui/grbloperationconfig.py +105 -0
  170. meerk40t/grbl/gui/gui.py +147 -116
  171. meerk40t/grbl/interpreter.py +44 -44
  172. meerk40t/grbl/loader.py +22 -22
  173. meerk40t/grbl/mock_connection.py +56 -56
  174. meerk40t/grbl/plugin.py +294 -264
  175. meerk40t/grbl/serial_connection.py +93 -88
  176. meerk40t/grbl/tcp_connection.py +81 -79
  177. meerk40t/grbl/ws_connection.py +112 -0
  178. meerk40t/gui/__init__.py +1 -1
  179. meerk40t/gui/about.py +2042 -296
  180. meerk40t/gui/alignment.py +1644 -1608
  181. meerk40t/gui/autoexec.py +199 -0
  182. meerk40t/gui/basicops.py +791 -670
  183. meerk40t/gui/bufferview.py +77 -71
  184. meerk40t/gui/busy.py +170 -133
  185. meerk40t/gui/choicepropertypanel.py +1673 -1469
  186. meerk40t/gui/consolepanel.py +706 -542
  187. meerk40t/gui/devicepanel.py +687 -581
  188. meerk40t/gui/dialogoptions.py +110 -107
  189. meerk40t/gui/executejob.py +316 -306
  190. meerk40t/gui/fonts.py +90 -90
  191. meerk40t/gui/functionwrapper.py +252 -0
  192. meerk40t/gui/gui_mixins.py +729 -0
  193. meerk40t/gui/guicolors.py +205 -182
  194. meerk40t/gui/help_assets/help_assets.py +218 -201
  195. meerk40t/gui/helper.py +154 -0
  196. meerk40t/gui/hersheymanager.py +1430 -846
  197. meerk40t/gui/icons.py +3422 -2747
  198. meerk40t/gui/imagesplitter.py +555 -508
  199. meerk40t/gui/keymap.py +354 -344
  200. meerk40t/gui/laserpanel.py +892 -806
  201. meerk40t/gui/laserrender.py +1470 -1232
  202. meerk40t/gui/lasertoolpanel.py +805 -793
  203. meerk40t/gui/magnetoptions.py +436 -0
  204. meerk40t/gui/materialmanager.py +2917 -0
  205. meerk40t/gui/materialtest.py +1722 -1694
  206. meerk40t/gui/mkdebug.py +646 -359
  207. meerk40t/gui/mwindow.py +163 -140
  208. meerk40t/gui/navigationpanels.py +2605 -2467
  209. meerk40t/gui/notes.py +143 -142
  210. meerk40t/gui/opassignment.py +414 -410
  211. meerk40t/gui/operation_info.py +310 -299
  212. meerk40t/gui/plugin.py +494 -328
  213. meerk40t/gui/position.py +714 -669
  214. meerk40t/gui/preferences.py +901 -650
  215. meerk40t/gui/propertypanels/attributes.py +1461 -1131
  216. meerk40t/gui/propertypanels/blobproperty.py +117 -114
  217. meerk40t/gui/propertypanels/consoleproperty.py +83 -80
  218. meerk40t/gui/propertypanels/gotoproperty.py +77 -0
  219. meerk40t/gui/propertypanels/groupproperties.py +223 -217
  220. meerk40t/gui/propertypanels/hatchproperty.py +489 -469
  221. meerk40t/gui/propertypanels/imageproperty.py +2244 -1384
  222. meerk40t/gui/propertypanels/inputproperty.py +59 -58
  223. meerk40t/gui/propertypanels/opbranchproperties.py +82 -80
  224. meerk40t/gui/propertypanels/operationpropertymain.py +1890 -1638
  225. meerk40t/gui/propertypanels/outputproperty.py +59 -58
  226. meerk40t/gui/propertypanels/pathproperty.py +389 -380
  227. meerk40t/gui/propertypanels/placementproperty.py +1214 -383
  228. meerk40t/gui/propertypanels/pointproperty.py +140 -136
  229. meerk40t/gui/propertypanels/propertywindow.py +313 -181
  230. meerk40t/gui/propertypanels/rasterwizardpanels.py +996 -912
  231. meerk40t/gui/propertypanels/regbranchproperties.py +76 -0
  232. meerk40t/gui/propertypanels/textproperty.py +770 -755
  233. meerk40t/gui/propertypanels/waitproperty.py +56 -55
  234. meerk40t/gui/propertypanels/warpproperty.py +121 -0
  235. meerk40t/gui/propertypanels/wobbleproperty.py +255 -204
  236. meerk40t/gui/ribbon.py +2468 -2210
  237. meerk40t/gui/scene/scene.py +1100 -1051
  238. meerk40t/gui/scene/sceneconst.py +22 -22
  239. meerk40t/gui/scene/scenepanel.py +439 -349
  240. meerk40t/gui/scene/scenespacewidget.py +365 -365
  241. meerk40t/gui/scene/widget.py +518 -505
  242. meerk40t/gui/scenewidgets/affinemover.py +215 -215
  243. meerk40t/gui/scenewidgets/attractionwidget.py +315 -309
  244. meerk40t/gui/scenewidgets/bedwidget.py +120 -97
  245. meerk40t/gui/scenewidgets/elementswidget.py +137 -107
  246. meerk40t/gui/scenewidgets/gridwidget.py +785 -745
  247. meerk40t/gui/scenewidgets/guidewidget.py +765 -765
  248. meerk40t/gui/scenewidgets/laserpathwidget.py +66 -66
  249. meerk40t/gui/scenewidgets/machineoriginwidget.py +86 -86
  250. meerk40t/gui/scenewidgets/nodeselector.py +28 -28
  251. meerk40t/gui/scenewidgets/rectselectwidget.py +589 -346
  252. meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
  253. meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
  254. meerk40t/gui/scenewidgets/selectionwidget.py +2952 -2756
  255. meerk40t/gui/simpleui.py +357 -333
  256. meerk40t/gui/simulation.py +2431 -2094
  257. meerk40t/gui/snapoptions.py +208 -203
  258. meerk40t/gui/spoolerpanel.py +1227 -1180
  259. meerk40t/gui/statusbarwidgets/defaultoperations.py +480 -353
  260. meerk40t/gui/statusbarwidgets/infowidget.py +520 -483
  261. meerk40t/gui/statusbarwidgets/opassignwidget.py +356 -355
  262. meerk40t/gui/statusbarwidgets/selectionwidget.py +172 -171
  263. meerk40t/gui/statusbarwidgets/shapepropwidget.py +754 -236
  264. meerk40t/gui/statusbarwidgets/statusbar.py +272 -260
  265. meerk40t/gui/statusbarwidgets/statusbarwidget.py +268 -270
  266. meerk40t/gui/statusbarwidgets/strokewidget.py +267 -251
  267. meerk40t/gui/themes.py +200 -78
  268. meerk40t/gui/tips.py +591 -0
  269. meerk40t/gui/toolwidgets/circlebrush.py +35 -35
  270. meerk40t/gui/toolwidgets/toolcircle.py +248 -242
  271. meerk40t/gui/toolwidgets/toolcontainer.py +82 -77
  272. meerk40t/gui/toolwidgets/tooldraw.py +97 -90
  273. meerk40t/gui/toolwidgets/toolellipse.py +219 -212
  274. meerk40t/gui/toolwidgets/toolimagecut.py +25 -132
  275. meerk40t/gui/toolwidgets/toolline.py +39 -144
  276. meerk40t/gui/toolwidgets/toollinetext.py +79 -236
  277. meerk40t/gui/toolwidgets/toollinetext_inline.py +296 -0
  278. meerk40t/gui/toolwidgets/toolmeasure.py +160 -216
  279. meerk40t/gui/toolwidgets/toolnodeedit.py +2088 -2074
  280. meerk40t/gui/toolwidgets/toolnodemove.py +92 -94
  281. meerk40t/gui/toolwidgets/toolparameter.py +754 -668
  282. meerk40t/gui/toolwidgets/toolplacement.py +108 -108
  283. meerk40t/gui/toolwidgets/toolpoint.py +68 -59
  284. meerk40t/gui/toolwidgets/toolpointlistbuilder.py +294 -0
  285. meerk40t/gui/toolwidgets/toolpointmove.py +183 -0
  286. meerk40t/gui/toolwidgets/toolpolygon.py +288 -403
  287. meerk40t/gui/toolwidgets/toolpolyline.py +38 -196
  288. meerk40t/gui/toolwidgets/toolrect.py +211 -207
  289. meerk40t/gui/toolwidgets/toolrelocate.py +72 -72
  290. meerk40t/gui/toolwidgets/toolribbon.py +598 -113
  291. meerk40t/gui/toolwidgets/tooltabedit.py +546 -0
  292. meerk40t/gui/toolwidgets/tooltext.py +98 -89
  293. meerk40t/gui/toolwidgets/toolvector.py +213 -204
  294. meerk40t/gui/toolwidgets/toolwidget.py +39 -39
  295. meerk40t/gui/usbconnect.py +98 -91
  296. meerk40t/gui/utilitywidgets/buttonwidget.py +18 -18
  297. meerk40t/gui/utilitywidgets/checkboxwidget.py +90 -90
  298. meerk40t/gui/utilitywidgets/controlwidget.py +14 -14
  299. meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +343 -340
  300. meerk40t/gui/utilitywidgets/debugwidgets.py +148 -0
  301. meerk40t/gui/utilitywidgets/handlewidget.py +27 -27
  302. meerk40t/gui/utilitywidgets/harmonograph.py +450 -447
  303. meerk40t/gui/utilitywidgets/openclosewidget.py +40 -40
  304. meerk40t/gui/utilitywidgets/rotationwidget.py +54 -54
  305. meerk40t/gui/utilitywidgets/scalewidget.py +75 -75
  306. meerk40t/gui/utilitywidgets/seekbarwidget.py +183 -183
  307. meerk40t/gui/utilitywidgets/togglewidget.py +142 -142
  308. meerk40t/gui/utilitywidgets/toolbarwidget.py +8 -8
  309. meerk40t/gui/wordlisteditor.py +985 -931
  310. meerk40t/gui/wxmeerk40t.py +1444 -1169
  311. meerk40t/gui/wxmmain.py +5578 -4112
  312. meerk40t/gui/wxmribbon.py +1591 -1076
  313. meerk40t/gui/wxmscene.py +1635 -1453
  314. meerk40t/gui/wxmtree.py +2410 -2089
  315. meerk40t/gui/wxutils.py +1769 -1099
  316. meerk40t/gui/zmatrix.py +102 -102
  317. meerk40t/image/__init__.py +1 -1
  318. meerk40t/image/dither.py +429 -0
  319. meerk40t/image/imagetools.py +2778 -2269
  320. meerk40t/internal_plugins.py +150 -130
  321. meerk40t/kernel/__init__.py +63 -12
  322. meerk40t/kernel/channel.py +259 -212
  323. meerk40t/kernel/context.py +538 -538
  324. meerk40t/kernel/exceptions.py +41 -41
  325. meerk40t/kernel/functions.py +463 -414
  326. meerk40t/kernel/jobs.py +100 -100
  327. meerk40t/kernel/kernel.py +3809 -3571
  328. meerk40t/kernel/lifecycles.py +71 -71
  329. meerk40t/kernel/module.py +49 -49
  330. meerk40t/kernel/service.py +147 -147
  331. meerk40t/kernel/settings.py +383 -343
  332. meerk40t/lihuiyu/controller.py +883 -876
  333. meerk40t/lihuiyu/device.py +1181 -1069
  334. meerk40t/lihuiyu/driver.py +1466 -1372
  335. meerk40t/lihuiyu/gui/gui.py +127 -106
  336. meerk40t/lihuiyu/gui/lhyaccelgui.py +377 -363
  337. meerk40t/lihuiyu/gui/lhycontrollergui.py +741 -651
  338. meerk40t/lihuiyu/gui/lhydrivergui.py +470 -446
  339. meerk40t/lihuiyu/gui/lhyoperationproperties.py +238 -237
  340. meerk40t/lihuiyu/gui/tcpcontroller.py +226 -190
  341. meerk40t/lihuiyu/interpreter.py +53 -53
  342. meerk40t/lihuiyu/laserspeed.py +450 -450
  343. meerk40t/lihuiyu/loader.py +90 -90
  344. meerk40t/lihuiyu/parser.py +404 -404
  345. meerk40t/lihuiyu/plugin.py +101 -102
  346. meerk40t/lihuiyu/tcp_connection.py +111 -109
  347. meerk40t/main.py +231 -165
  348. meerk40t/moshi/builder.py +788 -781
  349. meerk40t/moshi/controller.py +505 -499
  350. meerk40t/moshi/device.py +495 -442
  351. meerk40t/moshi/driver.py +862 -696
  352. meerk40t/moshi/gui/gui.py +78 -76
  353. meerk40t/moshi/gui/moshicontrollergui.py +538 -522
  354. meerk40t/moshi/gui/moshidrivergui.py +87 -75
  355. meerk40t/moshi/plugin.py +43 -43
  356. meerk40t/network/console_server.py +102 -57
  357. meerk40t/network/kernelserver.py +10 -9
  358. meerk40t/network/tcp_server.py +142 -140
  359. meerk40t/network/udp_server.py +103 -77
  360. meerk40t/network/web_server.py +390 -0
  361. meerk40t/newly/controller.py +1158 -1144
  362. meerk40t/newly/device.py +874 -732
  363. meerk40t/newly/driver.py +540 -412
  364. meerk40t/newly/gui/gui.py +219 -188
  365. meerk40t/newly/gui/newlyconfig.py +116 -101
  366. meerk40t/newly/gui/newlycontroller.py +193 -186
  367. meerk40t/newly/gui/operationproperties.py +51 -51
  368. meerk40t/newly/mock_connection.py +82 -82
  369. meerk40t/newly/newly_params.py +56 -56
  370. meerk40t/newly/plugin.py +1214 -1246
  371. meerk40t/newly/usb_connection.py +322 -322
  372. meerk40t/rotary/gui/gui.py +52 -46
  373. meerk40t/rotary/gui/rotarysettings.py +240 -232
  374. meerk40t/rotary/rotary.py +202 -98
  375. meerk40t/ruida/control.py +291 -91
  376. meerk40t/ruida/controller.py +138 -1088
  377. meerk40t/ruida/device.py +672 -231
  378. meerk40t/ruida/driver.py +534 -472
  379. meerk40t/ruida/emulator.py +1494 -1491
  380. meerk40t/ruida/exceptions.py +4 -4
  381. meerk40t/ruida/gui/gui.py +71 -76
  382. meerk40t/ruida/gui/ruidaconfig.py +239 -72
  383. meerk40t/ruida/gui/ruidacontroller.py +187 -184
  384. meerk40t/ruida/gui/ruidaoperationproperties.py +48 -47
  385. meerk40t/ruida/loader.py +54 -52
  386. meerk40t/ruida/mock_connection.py +57 -109
  387. meerk40t/ruida/plugin.py +124 -87
  388. meerk40t/ruida/rdjob.py +2084 -945
  389. meerk40t/ruida/serial_connection.py +116 -0
  390. meerk40t/ruida/tcp_connection.py +146 -0
  391. meerk40t/ruida/udp_connection.py +73 -0
  392. meerk40t/svgelements.py +9671 -9669
  393. meerk40t/tools/driver_to_path.py +584 -579
  394. meerk40t/tools/geomstr.py +5583 -4680
  395. meerk40t/tools/jhfparser.py +357 -292
  396. meerk40t/tools/kerftest.py +904 -890
  397. meerk40t/tools/livinghinges.py +1168 -1033
  398. meerk40t/tools/pathtools.py +987 -949
  399. meerk40t/tools/pmatrix.py +234 -0
  400. meerk40t/tools/pointfinder.py +942 -942
  401. meerk40t/tools/polybool.py +940 -940
  402. meerk40t/tools/rasterplotter.py +1660 -547
  403. meerk40t/tools/shxparser.py +989 -901
  404. meerk40t/tools/ttfparser.py +726 -446
  405. meerk40t/tools/zinglplotter.py +595 -593
  406. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/LICENSE +21 -21
  407. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/METADATA +150 -139
  408. meerk40t-0.9.7010.dist-info/RECORD +445 -0
  409. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/WHEEL +1 -1
  410. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/top_level.txt +0 -1
  411. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/zip-safe +1 -1
  412. meerk40t/balormk/elementlightjob.py +0 -159
  413. meerk40t-0.9.3001.dist-info/RECORD +0 -437
  414. test/bootstrap.py +0 -63
  415. test/test_cli.py +0 -12
  416. test/test_core_cutcode.py +0 -418
  417. test/test_core_elements.py +0 -144
  418. test/test_core_plotplanner.py +0 -397
  419. test/test_core_viewports.py +0 -312
  420. test/test_drivers_grbl.py +0 -108
  421. test/test_drivers_lihuiyu.py +0 -443
  422. test/test_drivers_newly.py +0 -113
  423. test/test_element_degenerate_points.py +0 -43
  424. test/test_elements_classify.py +0 -97
  425. test/test_elements_penbox.py +0 -22
  426. test/test_file_svg.py +0 -176
  427. test/test_fill.py +0 -155
  428. test/test_geomstr.py +0 -1523
  429. test/test_geomstr_nodes.py +0 -18
  430. test/test_imagetools_actualize.py +0 -306
  431. test/test_imagetools_wizard.py +0 -258
  432. test/test_kernel.py +0 -200
  433. test/test_laser_speeds.py +0 -3303
  434. test/test_length.py +0 -57
  435. test/test_lifecycle.py +0 -66
  436. test/test_operations.py +0 -251
  437. test/test_operations_hatch.py +0 -57
  438. test/test_ruida.py +0 -19
  439. test/test_spooler.py +0 -22
  440. test/test_tools_rasterplotter.py +0 -29
  441. test/test_wobble.py +0 -133
  442. test/test_zingl.py +0 -124
  443. {test → meerk40t/cylinder}/__init__.py +0 -0
  444. /meerk40t/{core/element_commands.py → cylinder/gui/__init__.py} +0 -0
  445. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/entry_points.txt +0 -0
meerk40t/gui/ribbon.py CHANGED
@@ -1,2210 +1,2468 @@
1
- """
2
- The RibbonBar is a scratch control widget. All the buttons are dynamically generated. The contents of those individual
3
- ribbon panels are defined by implementing classes.
4
-
5
- The primary method of defining a panel is by calling the `set_buttons()` on the panel.
6
-
7
- control_panel.set_buttons(
8
- {
9
- "label": _("Red Dot On"),
10
- "icon": icons8_flash_on,
11
- "tip": _("Turn Redlight On"),
12
- "action": lambda v: service("red on\n"),
13
- "toggle": {
14
- "label": _("Red Dot Off"),
15
- "action": lambda v: service("red off\n"),
16
- "icon": icons8_flash_off,
17
- "signal": "grbl_red_dot",
18
- },
19
- "rule_enabled": lambda v: has_red_dot_enabled(),
20
- }
21
- )
22
-
23
- Would, for example, register a button in the control panel the definitions for label, icon, tip, action are all
24
- standard with regard to buttons.
25
-
26
- The toggle defines an alternative set of values for the toggle state of the button.
27
-
28
- The multi defines a series of alternative states, and creates a hybrid button with a drop-down to select the state
29
- desired.
30
-
31
- Other properties like `rule_enabled` provides a check for whether this button should be enabled or not.
32
-
33
- The `toggle_attr` will permit a toggle to set an attribute on the given `object` which would default to the root
34
- context but could need to set a more local object attribute.
35
-
36
- If a `signal` is assigned as an aspect of multi it triggers that option multi-button option.
37
- If a `signal` is assigned within the toggle it sets the state of the given toggle. These should be compatible with
38
- the signals issued by choice panels.
39
-
40
- The action is a function which is run when the button is pressed.
41
- """
42
-
43
- import copy
44
- import math
45
- import platform
46
- import threading
47
-
48
- import wx
49
-
50
- from meerk40t.gui.icons import STD_ICON_SIZE, PyEmbeddedImage
51
- from meerk40t.kernel import Job
52
- from meerk40t.svgelements import Color
53
-
54
- _ = wx.GetTranslation
55
-
56
- COLOR_MODE_DEFAULT = 0
57
- COLOR_MODE_COLOR = 1
58
- COLOR_MODE_DARK = 2
59
-
60
-
61
- class DropDown:
62
- """
63
- Dropdowns are the triangle click addons that expand the button list to having other functions.
64
-
65
- This primarily stores the position of the given dropdown.
66
- """
67
-
68
- def __init__(self):
69
- self.position = None
70
-
71
- def contains(self, pos):
72
- """
73
- Is this drop down hit by this position.
74
-
75
- @param pos:
76
- @return:
77
- """
78
- if self.position is None:
79
- return False
80
- x, y = pos
81
- return (
82
- self.position[0] < x < self.position[2]
83
- and self.position[1] < y < self.position[3]
84
- )
85
-
86
-
87
- class Button:
88
- """
89
- Buttons store most of the relevant data as to how to display the current aspect of the given button. This
90
- includes things like tool-tip, the drop-down if needed, whether its in the overflow, the pressed and unpressed
91
- aspects of the buttons and enable/disable rules.
92
- """
93
-
94
- def __init__(self, context, parent, button_id, kind, description):
95
- self.context = context
96
- self.parent = parent
97
- self.id = button_id
98
- self.kind = kind
99
- self.button_dict = description
100
- self.enabled = True
101
- self.visible = True
102
- self._aspects = {}
103
- self.key = "original"
104
- self.object = None
105
-
106
- self.position = None
107
- self.toggle = False
108
-
109
- self.label = None
110
- self.icon = None
111
-
112
- self.bitmap = None
113
- self.bitmap_disabled = None
114
-
115
- self.min_size = 15
116
- self.max_size = 150
117
-
118
- self.available_bitmaps = {}
119
- self.available_bitmaps_disabled = {}
120
-
121
- self.tip = None
122
- self.client_data = None
123
- self.state = 0
124
- self.dropdown = None
125
- self.overflow = False
126
-
127
- self.state_pressed = None
128
- self.state_unpressed = None
129
- self.group = None
130
- self.toggle_attr = None
131
- self.identifier = None
132
- self.action = None
133
- self.action_right = None
134
- self.rule_enabled = None
135
- self.rule_visible = None
136
- self.min_width = 0
137
- self.min_height = 0
138
- self.default_width = int(self.max_size / 2)
139
- self.icon_size = self.default_width
140
-
141
- self.set_aspect(**description)
142
- self.apply_enable_rules()
143
-
144
- def set_aspect(
145
- self,
146
- label=None,
147
- icon=None,
148
- tip=None,
149
- group=None,
150
- toggle_attr=None,
151
- identifier=None,
152
- action=None,
153
- action_right=None,
154
- rule_enabled=None,
155
- rule_visible=None,
156
- object=None,
157
- **kwargs,
158
- ):
159
- """
160
- This sets all the different aspects that buttons generally have.
161
-
162
- @param label: button label
163
- @param icon: icon used for this button
164
- @param tip: tool tip for the button
165
- @param group: Group the button exists in for radio-toggles
166
- @param toggle_attr: The attribute that should be changed on toggle.
167
- @param identifier: Identifier in the group or toggle
168
- @param action: Action taken when button is pressed.
169
- @param action_right: Action taken when button is clicked with right mouse button.
170
- @param rule_enabled: Rule by which the button is enabled or disabled
171
- @param rule_visible: Rule by which the button will be hidden or shown
172
- @param object: object which the toggle_attr is an attr applied to
173
- @param kwargs:
174
- @return:
175
- """
176
- self.label = label
177
- resize_param = kwargs.get("size")
178
- if resize_param is None:
179
- self.default_width = int(self.max_size / 2)
180
- else:
181
- self.default_width = resize_param
182
-
183
- # We need to cast the icon explicitly to PyEmbeddedImage
184
- # as otherwise a strange type error is thrown:
185
- # TypeError: GetBitmap() got an unexpected keyword argument 'force_darkmode'
186
- # Well...
187
- from meerk40t.gui.icons import PyEmbeddedImage, VectorIcon
188
-
189
- if not isinstance(icon, VectorIcon):
190
- icon = PyEmbeddedImage(icon.data)
191
- self.icon = icon
192
-
193
- self.available_bitmaps.clear()
194
- self.available_bitmaps_disabled.clear()
195
- self.get_bitmaps(self.default_width)
196
-
197
- self.tip = tip
198
- self.group = group
199
- self.toggle_attr = toggle_attr
200
- self.identifier = identifier
201
- self.action = action
202
- self.action_right = action_right
203
- self.rule_enabled = rule_enabled
204
- self.rule_visible = rule_visible
205
- if object is not None:
206
- self.object = object
207
- else:
208
- self.object = self.context
209
- if self.kind == "hybrid":
210
- self.dropdown = DropDown()
211
- self.modified()
212
-
213
- def get_bitmaps(self, point_size):
214
- top = self.parent.parent.parent
215
- darkm = bool(top.art.color_mode == COLOR_MODE_DARK)
216
- if point_size < self.min_size:
217
- point_size = self.min_size
218
- if point_size > self.max_size:
219
- point_size = self.max_size
220
- self.icon_size = int(point_size)
221
- edge = int(point_size / 25.0) + 1
222
- key = str(self.icon_size)
223
- if key not in self.available_bitmaps:
224
- self.available_bitmaps[key] = self.icon.GetBitmap(
225
- resize=self.icon_size,
226
- noadjustment=True,
227
- force_darkmode=darkm,
228
- buffer=edge,
229
- )
230
- self.available_bitmaps_disabled[key] = self.icon.GetBitmap(
231
- resize=self.icon_size,
232
- color=Color("grey"),
233
- noadjustment=True,
234
- buffer=edge,
235
- )
236
- self.bitmap = self.available_bitmaps[key]
237
- self.bitmap_disabled = self.available_bitmaps_disabled[key]
238
-
239
- def _restore_button_aspect(self, key):
240
- """
241
- Restores a saved button aspect for the given key. Given a key to the alternative aspect we restore the given
242
- aspect.
243
-
244
- @param key: aspect key to set.
245
- @return:
246
- """
247
- try:
248
- alt = self._aspects[key]
249
- except KeyError:
250
- return
251
- self.set_aspect(**alt)
252
- self.key = key
253
-
254
- def _store_button_aspect(self, key, **kwargs):
255
- """
256
- Stores visual aspects of the buttons within the "_aspects" dictionary.
257
-
258
- This stores the various icons, labels, help, and other properties found on the button.
259
-
260
- @param key: aspects to store.
261
- @param kwargs: Additional aspects to implement that are not necessarily currently set on the button.
262
- @return:
263
- """
264
- self._aspects[key] = {
265
- "action": self.action,
266
- "action_right": self.action_right,
267
- "label": self.label,
268
- "tip": self.tip,
269
- "icon": self.icon,
270
- "client_data": self.client_data,
271
- }
272
- self._update_button_aspect(key, **kwargs)
273
-
274
- def _update_button_aspect(self, key, **kwargs):
275
- """
276
- Directly update the button aspects via the kwargs, aspect dictionary *must* exist.
277
-
278
- @param self:
279
- @param key:
280
- @param kwargs:
281
- @return:
282
- """
283
- key_dict = self._aspects[key]
284
- for k in kwargs:
285
- if kwargs[k] is not None:
286
- key_dict[k] = kwargs[k]
287
-
288
- def apply_enable_rules(self):
289
- """
290
- Calls rule_enabled() and returns whether the given rule enables the button.
291
-
292
- @return:
293
- """
294
- if self.rule_enabled is not None:
295
- try:
296
- v = self.rule_enabled(0)
297
- if v != self.enabled:
298
- self.enabled = v
299
- self.modified()
300
- except (AttributeError, TypeError):
301
- pass
302
- if self.rule_visible is not None:
303
- v = self.rule_visible(0)
304
- if v != self.visible:
305
- self.visible = v
306
- if not self.visible:
307
- self.position = None
308
- self.modified()
309
- else:
310
- if not self.visible:
311
- self.visible = True
312
- self.modified()
313
-
314
- def contains(self, pos):
315
- """
316
- Is this button hit by this position.
317
-
318
- @param pos:
319
- @return:
320
- """
321
- if self.position is None:
322
- return False
323
- x, y = pos
324
- return (
325
- self.position[0] < x < self.position[2]
326
- and self.position[1] < y < self.position[3]
327
- )
328
-
329
- def click(self, event=None, recurse=True):
330
- """
331
- Process button click of button at provided button_id
332
-
333
- @return:
334
- """
335
- if self.group:
336
- # Toggle radio buttons
337
- self.toggle = not self.toggle
338
- if self.toggle: # got toggled
339
- button_group = self.parent.group_lookup.get(self.group, [])
340
-
341
- for obutton in button_group:
342
- # Untoggle all other buttons in this group.
343
- if obutton.group == self.group and obutton.id != self.id:
344
- obutton.set_button_toggle(False)
345
- else: # got untoggled...
346
- # so let's activate the first button of the group (implicitly defined as default...)
347
- button_group = self.parent.group_lookup.get(self.group)
348
- if button_group and recurse:
349
- first_button = button_group[0]
350
- first_button.set_button_toggle(True)
351
- first_button.click(recurse=False)
352
- return
353
- if self.action is not None:
354
- # We have an action to call.
355
- self.action(None)
356
-
357
- if self.state_pressed is None:
358
- # Unless button has a pressed state we have finished.
359
- return
360
-
361
- # There is a pressed state which requires that we have a toggle.
362
- self.toggle = not self.toggle
363
- if self.toggle:
364
- # Call the toggle_attr restore the pressed state.
365
- if self.toggle_attr is not None:
366
- setattr(self.object, self.toggle_attr, True)
367
- self.context.signal(self.toggle_attr, True, self.object)
368
- self._restore_button_aspect(self.state_pressed)
369
- else:
370
- # Call the toggle_attr restore the unpressed state.
371
- if self.toggle_attr is not None:
372
- setattr(self.object, self.toggle_attr, False)
373
- self.context.signal(self.toggle_attr, False, self.object)
374
- self._restore_button_aspect(self.state_unpressed)
375
-
376
- def drop_click(self):
377
- """
378
- Drop down of a hybrid button was clicked.
379
-
380
- We make a menu popup and fill it with the data about the multi-button
381
-
382
- @param event:
383
- @return:
384
- """
385
- if self.toggle:
386
- return
387
- top = self.parent.parent.parent
388
- menu = wx.Menu()
389
- item = menu.Append(wx.ID_ANY, "...")
390
- item.Enable(False)
391
- for v in self.button_dict["multi"]:
392
- item = menu.Append(wx.ID_ANY, v.get("label"))
393
- tip = v.get("tip")
394
- if tip:
395
- item.SetHelp(tip)
396
- if v.get("identifier") == self.identifier:
397
- item.SetItemLabel(v.get("label") + "(*)")
398
- icon = v.get("icon")
399
- if icon:
400
- # There seems to be a bug to display icons in a submenu consistently
401
- # print (f"Had a bitmap for {v.get('label')}")
402
- item.SetBitmap(icon.GetBitmap(resize=STD_ICON_SIZE / 2, buffer=2))
403
- top.Bind(wx.EVT_MENU, self.drop_menu_click(v), id=item.GetId())
404
- top.PopupMenu(menu)
405
-
406
- def drop_menu_click(self, v):
407
- """
408
- Creates menu_item_click processors for the various menus created for a drop-click
409
-
410
- @param button:
411
- @param v:
412
- @return:
413
- """
414
-
415
- def menu_item_click(event):
416
- """
417
- Process menu item click.
418
-
419
- @param event:
420
- @return:
421
- """
422
- key_id = v.get("identifier")
423
- try:
424
- setattr(self.object, self.save_id, key_id)
425
- except AttributeError:
426
- pass
427
- self.state_unpressed = key_id
428
- self._restore_button_aspect(key_id)
429
- # self.ensure_realize()
430
-
431
- return menu_item_click
432
-
433
- def _setup_multi_button(self):
434
- """
435
- Store alternative aspects for multi-buttons, load stored previous state.
436
-
437
- @return:
438
- """
439
- multi_aspects = self.button_dict["multi"]
440
- # This is the key used for the multi button.
441
- multi_ident = self.button_dict.get("identifier")
442
- self.save_id = multi_ident
443
- try:
444
- self.object.setting(str, self.save_id, "default")
445
- except AttributeError:
446
- # This is not a context, we tried.
447
- pass
448
- initial_value = getattr(self.object, self.save_id, "default")
449
-
450
- for i, v in enumerate(multi_aspects):
451
- # These are values for the outer identifier
452
- key = v.get("identifier", i)
453
- self._store_button_aspect(key, **v)
454
- if "signal" in v:
455
- self._create_signal_for_multi(key, v["signal"])
456
-
457
- if key == initial_value:
458
- self._restore_button_aspect(key)
459
-
460
- def _create_signal_for_multi(self, key, signal):
461
- """
462
- Creates a signal to restore the state of a multi button.
463
-
464
- @param key:
465
- @param signal:
466
- @return:
467
- """
468
-
469
- def multi_click(origin, set_value):
470
- self._restore_button_aspect(key)
471
-
472
- self.context.listen(signal, multi_click)
473
- self.parent._registered_signals.append((signal, multi_click))
474
-
475
- def _setup_toggle_button(self):
476
- """
477
- Store toggle and original aspects for toggle-buttons
478
-
479
- @param self:
480
- @return:
481
- """
482
- resize_param = self.button_dict.get("size")
483
-
484
- self.state_pressed = "toggle"
485
- self.state_unpressed = "original"
486
- self._store_button_aspect(self.state_unpressed)
487
-
488
- toggle_button_dict = self.button_dict.get("toggle")
489
- key = toggle_button_dict.get("identifier", self.state_pressed)
490
- if "signal" in toggle_button_dict:
491
- self._create_signal_for_toggle(toggle_button_dict.get("signal"))
492
- self._store_button_aspect(key, **toggle_button_dict)
493
-
494
- # Set initial value by identifer and object
495
- if self.toggle_attr is not None and getattr(
496
- self.object, self.toggle_attr, False
497
- ):
498
- self.set_button_toggle(True)
499
- self.modified()
500
-
501
- def _create_signal_for_toggle(self, signal):
502
- """
503
- Creates a signal toggle which will listen for the given signal and set the toggle-state to the given set_value
504
-
505
- E.G. If a toggle has a signal called "tracing" and the context.signal("tracing", True) is called this will
506
- automatically set the toggle state.
507
-
508
- Note: It will not call any of the associated actions, it will simply set the toggle state.
509
-
510
- @param signal:
511
- @return:
512
- """
513
-
514
- def toggle_click(origin, set_value, *args):
515
- self.set_button_toggle(set_value)
516
-
517
- self.context.listen(signal, toggle_click)
518
- self.parent._registered_signals.append((signal, toggle_click))
519
-
520
- def set_button_toggle(self, toggle_state):
521
- """
522
- Set the button's toggle state to the given toggle_state
523
-
524
- @param toggle_state:
525
- @return:
526
- """
527
- self.toggle = toggle_state
528
- if toggle_state:
529
- self._restore_button_aspect(self.state_pressed)
530
- else:
531
- self._restore_button_aspect(self.state_unpressed)
532
-
533
- def modified(self):
534
- """
535
- This button was modified and should be redrawn.
536
- @return:
537
- """
538
- self.parent.modified()
539
-
540
-
541
- class RibbonPanel:
542
- """
543
- Ribbon Panel is a panel of buttons within the page.
544
- """
545
-
546
- def __init__(self, context, parent, id, label, icon):
547
- self.context = context
548
- self.parent = parent
549
- self.id = id
550
- self.label = label
551
- self.icon = icon
552
-
553
- self._registered_signals = list()
554
- self.button_lookup = {}
555
- self.group_lookup = {}
556
-
557
- self.buttons = []
558
- self.position = None
559
- self.available_position = None
560
- self._overflow = list()
561
- self._overflow_position = None
562
-
563
- def visible_buttons(self):
564
- for button in self.buttons:
565
- if button.visible:
566
- yield button
567
-
568
- @property
569
- def visible_button_count(self):
570
- pcount = 0
571
- for button in self.buttons:
572
- if button is not None and button.visible:
573
- pcount += 1
574
- return pcount
575
-
576
- def clear_buttons(self):
577
- self.buttons.clear()
578
- self.parent.modified()
579
- self._overflow = list()
580
- self._overflow_position = None
581
-
582
- def set_buttons(self, new_values):
583
- """
584
- Set buttons is the primary button configuration routine. It is responsible for clearing and recreating buttons.
585
-
586
- * The button definition is a dynamically created and stored dictionary.
587
- * Buttons are sorted by priority.
588
- * Multi buttons get a hybrid type.
589
- * Toggle buttons get a toggle type (Unless they are also multi).
590
- * Created button objects have attributes assigned to them.
591
- * toggle, parent, group, identifier, toggle_identifier, action, right, rule_enabled
592
- * Multi-buttons have an identifier attr which is applied to the root context, or given "object".
593
- * The identifier is used to set the state of the object, the attr-identifier is set to the value-identifier
594
- * Toggle buttons have a toggle_identifier, this is used to set the and retrieve the state of the toggle.
595
-
596
-
597
- @param new_values: dictionary of button values to use.
598
- @return:
599
- """
600
- # print (f"Setbuttons called for {self.label}")
601
- self.modified()
602
- self.clear_buttons()
603
- button_descriptions = []
604
- for desc, name, sname in new_values:
605
- button_descriptions.append(desc)
606
-
607
- # Sort buttons by priority
608
- def sort_priority(elem):
609
- return elem.get("priority", 0)
610
-
611
- button_descriptions.sort(key=sort_priority)
612
-
613
- for desc in button_descriptions:
614
- # Every registered button in the updated lookup gets created.
615
- b = self._create_button(desc)
616
-
617
- # Store newly created button in the various lookups
618
- self.button_lookup[b.id] = b
619
- group = desc.get("group")
620
- if group is not None:
621
- c_group = self.group_lookup.get(group)
622
- if c_group is None:
623
- c_group = []
624
- self.group_lookup[group] = c_group
625
- c_group.append(b)
626
-
627
- def _create_button(self, desc):
628
- """
629
- Creates a button and places it on the button_bar depending on the required definition.
630
-
631
- @param desc:
632
- @return:
633
- """
634
- show_tip = not self.context.disable_tool_tips
635
- # NewIdRef is only available after 4.1
636
- try:
637
- new_id = wx.NewIdRef()
638
- except AttributeError:
639
- new_id = wx.NewId()
640
-
641
- # Create kind of button. Multi buttons are hybrid. Else, regular button or toggle-type
642
- if "multi" in desc:
643
- # Button is a multi-type button
644
- b = Button(
645
- self.context, self, button_id=new_id, kind="hybrid", description=desc
646
- )
647
- self.buttons.append(b)
648
- b._setup_multi_button()
649
- else:
650
- bkind = "normal"
651
- if "group" in desc or "toggle" in desc:
652
- bkind = "toggle"
653
- b = Button(
654
- self.context, self, button_id=new_id, kind=bkind, description=desc
655
- )
656
- self.buttons.append(b)
657
-
658
- if "toggle" in desc:
659
- b._setup_toggle_button()
660
- return b
661
-
662
- def contains(self, pos):
663
- """
664
- Does the given position hit the current panel.
665
-
666
- @param pos:
667
- @return:
668
- """
669
- if self.position is None:
670
- return False
671
- x, y = pos
672
- return (
673
- self.position[0] < x < self.position[2]
674
- and self.position[1] < y < self.position[3]
675
- )
676
-
677
- def modified(self):
678
- """
679
- Modified call parent page.
680
- @return:
681
- """
682
- self.parent.modified()
683
-
684
- def overflow_click(self):
685
- """
686
- Click of overflow. Overflow exists if some icons are not able to be shown.
687
-
688
- We make a menu popup and fill it with the overflow commands.
689
-
690
- @param event:
691
- @return:
692
- """
693
- # print (f"Overflow click called for {self.label}")
694
- menu = wx.Menu()
695
- top = self.parent.parent # .parent
696
- for v in self._overflow:
697
- item = menu.Append(wx.ID_ANY, v.label)
698
- item.Enable(v.enabled)
699
- item.SetHelp(v.tip)
700
- if v.icon:
701
- item.SetBitmap(v.icon.GetBitmap(resize=STD_ICON_SIZE / 2, buffer=2))
702
- top.Bind(wx.EVT_MENU, v.click, id=item.Id)
703
- top.PopupMenu(menu)
704
-
705
-
706
- class RibbonPage:
707
- """
708
- Ribbon Page is a page of buttons this is the series of ribbon panels as triggered by the different tags.
709
- """
710
-
711
- def __init__(self, context, parent, id, label, icon):
712
- self.context = context
713
- self.parent = parent
714
- self.id = id
715
- self.label = label
716
- self.icon = icon
717
- self.panels = []
718
- self.position = None
719
- self.tab_position = None
720
- self.visible = True
721
-
722
- def add_panel(self, panel, ref):
723
- """
724
- Adds a panel to this page.
725
- @param panel:
726
- @param ref:
727
- @return:
728
- """
729
- self.panels.append(panel)
730
- if ref is not None:
731
- # print(f"Setattr in add_panel: {ref} = {panel}")
732
- setattr(self, ref, panel)
733
-
734
- def contains(self, pos):
735
- """
736
- Does this position hit the tab position of this page.
737
- @param pos:
738
- @return:
739
- """
740
- if self.tab_position is None:
741
- return False
742
- x, y = pos
743
- return (
744
- self.tab_position[0] < x < self.tab_position[2]
745
- and self.tab_position[1] < y < self.tab_position[3]
746
- )
747
-
748
- def modified(self):
749
- """
750
- Call modified to parent RibbonBarPanel.
751
-
752
- @return:
753
- """
754
- self.parent.modified()
755
-
756
-
757
- class RibbonBarPanel(wx.Control):
758
- def __init__(self, parent, id, context=None, pane=None, **kwds):
759
- super().__init__(parent, id, **kwds)
760
- self.context = context
761
- self.pages = []
762
- self.pane = pane
763
- jobname = f"realize_ribbon_bar_{self.GetId()}"
764
- # print (f"Requesting job with name: '{jobname}'")
765
- self._redraw_job = Job(
766
- process=self._paint_main_on_buffer,
767
- job_name=jobname,
768
- interval=0.1,
769
- times=1,
770
- run_main=True,
771
- )
772
- # Layout properties.
773
- self.art = Art(self)
774
-
775
- # Define Ribbon.
776
- self._redraw_lock = threading.Lock()
777
- self._paint_dirty = True
778
- self._layout_dirty = True
779
- self._ribbon_buffer = None
780
-
781
- # self._overflow = list()
782
- # self._overflow_position = None
783
-
784
- self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
785
- self.Bind(wx.EVT_ERASE_BACKGROUND, self.on_erase_background)
786
- self.Bind(wx.EVT_ENTER_WINDOW, self.on_mouse_enter)
787
- self.Bind(wx.EVT_LEAVE_WINDOW, self.on_mouse_leave)
788
- self.Bind(wx.EVT_MOTION, self.on_mouse_move)
789
- self.Bind(wx.EVT_PAINT, self.on_paint)
790
- self.Bind(wx.EVT_SIZE, self.on_size)
791
-
792
- self.Bind(wx.EVT_LEFT_DOWN, self.on_click)
793
- self.Bind(wx.EVT_RIGHT_UP, self.on_click_right)
794
-
795
- # Preparation for individual page visibility
796
- def visible_pages(self):
797
- count = 0
798
- for p in self.pages:
799
- if p.visible:
800
- count += 1
801
- return count
802
-
803
- def first_page(self):
804
- # returns the first visible page
805
- for p in self.pages:
806
- if p.visible:
807
- return p
808
- return None
809
-
810
- def modified(self):
811
- """
812
- if modified then we flag the layout and paint as dirty and call for a refresh of the ribbonbar.
813
- @return:
814
- """
815
- # (f"Modified called for RibbonBar with {self.visible_pages()} pages")
816
- self._paint_dirty = True
817
- self._layout_dirty = True
818
- self.context.schedule(self._redraw_job)
819
-
820
- def redrawn(self):
821
- """
822
- if refresh needed then we flag the paint as dirty and call for a refresh of the ribbonbar.
823
- @return:
824
- """
825
- self._paint_dirty = True
826
- self.context.schedule(self._redraw_job)
827
-
828
- def on_size(self, event: wx.SizeEvent):
829
- self._set_buffer()
830
- self.modified()
831
-
832
- def on_erase_background(self, event):
833
- pass
834
-
835
- def on_mouse_enter(self, event: wx.MouseEvent):
836
- pass
837
-
838
- def on_mouse_leave(self, event: wx.MouseEvent):
839
- self.art.hover_tab = None
840
- self.art.hover_button = None
841
- self.art.hover_dropdown = None
842
- self.redrawn()
843
-
844
- def _check_hover_dropdown(self, drop, pos):
845
- if drop is not None and not drop.contains(pos):
846
- drop = None
847
- if drop is not self.art.hover_dropdown:
848
- self.art.hover_dropdown = drop
849
- self.redrawn()
850
-
851
- def _check_hover_button(self, pos):
852
- hover = self._overflow_at_position(pos)
853
- if hover is not None:
854
- self.SetToolTip(_("There is more to see - click to display"))
855
- return
856
- hover = self._button_at_position(pos)
857
- if hover is not None:
858
- self._check_hover_dropdown(hover.dropdown, pos)
859
- if hover is not None and hover is self.art.hover_button:
860
- return
861
- self.art.hover_button = hover
862
- if hover is None:
863
- hover = self._button_at_position(pos, use_all=True)
864
- if hover is not None:
865
- self.SetToolTip(hover.tip)
866
- else:
867
- self.SetToolTip("")
868
-
869
- self.redrawn()
870
-
871
- def _check_hover_tab(self, pos):
872
- hover = self._pagetab_at_position(pos)
873
- if hover is not self.art.hover_tab:
874
- self.art.hover_tab = hover
875
- self.redrawn()
876
-
877
- def on_mouse_move(self, event: wx.MouseEvent):
878
- pos = event.Position
879
- self._check_hover_button(pos)
880
- self._check_hover_tab(pos)
881
-
882
- def on_paint(self, event: wx.PaintEvent):
883
- """
884
- Ribbonbar paint event calls the paints the bitmap self._ribbon_buffer. If self._ribbon_buffer does not exist
885
- initially it is created in the self.scene.update_buffer_ui_thread() call.
886
- """
887
- if self._paint_dirty:
888
- self._paint_main_on_buffer()
889
-
890
- try:
891
- wx.BufferedPaintDC(self, self._ribbon_buffer)
892
- except (RuntimeError, AssertionError, TypeError):
893
- pass
894
-
895
- def _paint_main_on_buffer(self):
896
- """Performs redrawing of the data in the UI thread."""
897
- # print (f"Redraw job started for RibbonBar with {self.visible_pages()} pages")
898
- try:
899
- buf = self._set_buffer()
900
- dc = wx.MemoryDC()
901
- except RuntimeError:
902
- # Shutdown error
903
- return
904
- dc.SelectObject(buf)
905
- if self._redraw_lock.acquire(timeout=0.2):
906
- if self._layout_dirty:
907
- self.art.layout(dc, self)
908
- self._layout_dirty = False
909
- self.art.paint_main(dc, self)
910
- self._redraw_lock.release()
911
- self._paint_dirty = False
912
- dc.SelectObject(wx.NullBitmap)
913
- del dc
914
-
915
- self.Refresh() # Paint buffer on screen.
916
-
917
- def prefer_horizontal(self):
918
- result = None
919
- if self.pane is not None:
920
- try:
921
- pane = self.pane.manager.GetPane(self.pane.name)
922
- if pane.IsDocked():
923
- # if self.pane.name == "tools":
924
- # print (
925
- # f"Pane: {pane.name}: {pane.dock_direction}, State: {pane.IsOk()}/{pane.IsDocked()}/{pane.IsFloating()}"
926
- # )
927
- if pane.dock_direction in (1, 3):
928
- # Horizontal
929
- result = True
930
- elif pane.dock_direction in (2, 4):
931
- # Vertical
932
- result = False
933
- # else:
934
- # if self.pane.name == "tools":
935
- # print (
936
- # f"Pane: {pane.name}: {pane.IsFloating()}"
937
- # )
938
- except (AttributeError, RuntimeError):
939
- # Unknown error occurred
940
- pass
941
-
942
- if result is None:
943
- # Floating...
944
- width, height = self.ClientSize
945
- if width <= 0:
946
- width = 1
947
- if height <= 0:
948
- height = 1
949
- result = bool(width >= height)
950
-
951
- return result
952
-
953
- def _set_buffer(self):
954
- """
955
- Set the value for the self._Buffer bitmap equal to the panel's clientSize.
956
- """
957
- if (
958
- self._ribbon_buffer is None
959
- or self._ribbon_buffer.GetSize() != self.ClientSize
960
- or not self._ribbon_buffer.IsOk()
961
- ):
962
- width, height = self.ClientSize
963
- if width <= 0:
964
- width = 1
965
- if height <= 0:
966
- height = 1
967
- self._ribbon_buffer = wx.Bitmap(width, height)
968
- return self._ribbon_buffer
969
-
970
- def toggle_show_labels(self, v):
971
- self.art.show_labels = v
972
- self.modified()
973
-
974
- def _overflow_at_position(self, pos):
975
- for page in self.pages:
976
- if page is not self.art.current_page or not page.visible:
977
- continue
978
- for panel in page.panels:
979
- x, y = pos
980
- # print (f"Checking: {panel.label}: ({x},{y}) in ({panel._overflow_position})")
981
- if panel._overflow_position is None:
982
- continue
983
- if (
984
- panel._overflow_position[0] < x < panel._overflow_position[2]
985
- and panel._overflow_position[1] < y < panel._overflow_position[3]
986
- ):
987
- # print (f"Found a panel: {panel.label}")
988
- return panel
989
- return None
990
-
991
- def _button_at_position(self, pos, use_all=False):
992
- """
993
- Find the button at the given position, so long as that button is enabled.
994
-
995
- @param pos:
996
- @return:
997
- """
998
- for page in self.pages:
999
- if page is not self.art.current_page or not page.visible:
1000
- continue
1001
- for panel in page.panels:
1002
- for button in panel.visible_buttons():
1003
- if (
1004
- button.contains(pos)
1005
- and (button.enabled or use_all)
1006
- and not button.overflow
1007
- ):
1008
- return button
1009
- return None
1010
-
1011
- def _pagetab_at_position(self, pos):
1012
- """
1013
- Find the page tab at the given position.
1014
-
1015
- @param pos:
1016
- @return:
1017
- """
1018
- for page in self.pages:
1019
- if page.visible and page.contains(pos):
1020
- return page
1021
- return None
1022
-
1023
- def on_click_right(self, event: wx.MouseEvent):
1024
- """
1025
- Handles the ``wx.EVT_RIGHT_DOWN`` event
1026
- :param event: a :class:`MouseEvent` event to be processed.
1027
- """
1028
- pos = event.Position
1029
- button = self._button_at_position(pos)
1030
- if button is not None:
1031
- action = button.action_right
1032
- if action:
1033
- action(event)
1034
- else:
1035
- # Click on background, off menu to edit and set colors
1036
- def set_color(newmode):
1037
- self.context.root.ribbon_color = newmode
1038
- # Force refresh
1039
- self.context.signal("ribbon_recreate", None)
1040
-
1041
- top = self # .parent
1042
- c_mode = self.context.root.setting(int, "ribbon_color", COLOR_MODE_DEFAULT)
1043
- menu = wx.Menu()
1044
- item = menu.Append(wx.ID_ANY, _("Colorscheme"))
1045
- item.Enable(False)
1046
- item = menu.Append(wx.ID_ANY, _("System Default"), "", wx.ITEM_CHECK)
1047
- item.Check(bool(c_mode == COLOR_MODE_DEFAULT))
1048
- top.Bind(
1049
- wx.EVT_MENU, lambda v: set_color(COLOR_MODE_DEFAULT), id=item.GetId()
1050
- )
1051
- item = menu.Append(wx.ID_ANY, _("Colored"), "", wx.ITEM_CHECK)
1052
- item.Check(bool(c_mode == COLOR_MODE_COLOR))
1053
- top.Bind(
1054
- wx.EVT_MENU, lambda v: set_color(COLOR_MODE_COLOR), id=item.GetId()
1055
- )
1056
- item = menu.Append(wx.ID_ANY, _("Black"), "", wx.ITEM_CHECK)
1057
- item.Check(bool(c_mode == COLOR_MODE_DARK))
1058
- top.Bind(wx.EVT_MENU, lambda v: set_color(COLOR_MODE_DARK), id=item.GetId())
1059
- item = menu.AppendSeparator()
1060
- haslabel = self.art.show_labels
1061
- item = menu.Append(wx.ID_ANY, _("Show Labels"), "", wx.ITEM_CHECK)
1062
- if not getattr(self, "allow_labels", True):
1063
- item.Enable(False)
1064
- item.Check(haslabel)
1065
- top.Bind(
1066
- wx.EVT_MENU,
1067
- lambda v: self.toggle_show_labels(not haslabel),
1068
- id=item.GetId(),
1069
- )
1070
- item = menu.AppendSeparator()
1071
- item = menu.Append(wx.ID_ANY, _("Customize Toolbars"))
1072
-
1073
- def show_pref():
1074
- self.context("window open Preferences\n")
1075
- self.context.signal("preferences", "ribbon")
1076
-
1077
- top.Bind(
1078
- wx.EVT_MENU,
1079
- lambda v: show_pref(),
1080
- id=item.GetId(),
1081
- )
1082
- top.PopupMenu(menu)
1083
-
1084
- def on_click(self, event: wx.MouseEvent):
1085
- """
1086
- The ribbon bar was clicked. We check the various parts of the ribbonbar that could have been clicked in the
1087
- preferred click order. Overflow, pagetab, drop-down, button.
1088
- @param event:
1089
- @return:
1090
- """
1091
- pos = event.Position
1092
-
1093
- page = self._pagetab_at_position(pos)
1094
- overflow = self._overflow_at_position(pos)
1095
- if overflow is not None:
1096
- overflow.overflow_click()
1097
- self.modified()
1098
- return
1099
-
1100
- button = self._button_at_position(pos)
1101
- if page is not None and button is None:
1102
- self.art.current_page = page
1103
- self.apply_enable_rules()
1104
- self.modified()
1105
- return
1106
- if button is None:
1107
- return
1108
- drop = button.dropdown
1109
- if drop is not None and drop.contains(pos):
1110
- button.drop_click()
1111
- self.modified()
1112
- return
1113
- button.click()
1114
- self.modified()
1115
-
1116
- def _all_buttons(self):
1117
- """
1118
- Helper to cycle through all buttons in the panels that are currently visible.
1119
- @return:
1120
- """
1121
- for page in self.pages:
1122
- if page is not self.art.current_page or not page.visible:
1123
- continue
1124
- for panel in page.panels:
1125
- for button in panel.buttons:
1126
- yield button
1127
-
1128
- def apply_enable_rules(self):
1129
- """
1130
- Applies all enable rules for all buttons that are currently seen.
1131
- @return:
1132
- """
1133
- for button in self._all_buttons():
1134
- button.apply_enable_rules()
1135
-
1136
- def add_page(self, ref, id, label, icon):
1137
- """
1138
- Add a page to the ribbonbar.
1139
- @param ref:
1140
- @param id:
1141
- @param label:
1142
- @param icon:
1143
- @return:
1144
- """
1145
- page = RibbonPage(
1146
- self.context,
1147
- self,
1148
- id,
1149
- label,
1150
- icon,
1151
- )
1152
- if ref is not None:
1153
- # print(f"Setattr in add_page: {ref} = {page}")
1154
- setattr(self, ref, page)
1155
- if self.art.current_page is None:
1156
- self.art.current_page = page
1157
- self.pages.append(page)
1158
- self._layout_dirty = True
1159
- return page
1160
-
1161
- def remove_page(self, pageid):
1162
- """
1163
- Remove a page from the ribbonbar.
1164
- @param id:
1165
- @return:
1166
- """
1167
- for pidx, page in enumerate(self.pages):
1168
- if page.id == pageid:
1169
- if self.art.current_page is page:
1170
- self.art.current_page = None
1171
- for panel in page.panels:
1172
- panel.clear_buttons()
1173
- del panel
1174
- self.pages.pop(pidx)
1175
- break
1176
-
1177
- self._layout_dirty = True
1178
-
1179
- def validate_current_page(self):
1180
- if self.art.current_page is None or not self.art.current_page.visible:
1181
- self.art.current_page = self.first_page()
1182
-
1183
- def add_panel(self, ref, parent: RibbonPage, id, label, icon):
1184
- """
1185
- Add a panel to the ribbon bar. Parent must be a page.
1186
- @param ref:
1187
- @param parent:
1188
- @param id:
1189
- @param label:
1190
- @param icon:
1191
- @return:
1192
- """
1193
- panel = RibbonPanel(
1194
- self.context,
1195
- parent=parent,
1196
- id=id,
1197
- label=label,
1198
- icon=icon,
1199
- )
1200
- parent.add_panel(panel, ref)
1201
- self._layout_dirty = True
1202
- return panel
1203
-
1204
-
1205
- class Art:
1206
- def __init__(self, parent):
1207
- self.RIBBON_ORIENTATION_AUTO = 0
1208
- self.RIBBON_ORIENTATION_HORIZONTAL = 1
1209
- self.RIBBON_ORIENTATION_VERTICAL = 2
1210
- self.orientation = self.RIBBON_ORIENTATION_AUTO
1211
- self.parent = parent
1212
- self.between_button_buffer = 3
1213
- self.panel_button_buffer = 3
1214
- self.page_panel_buffer = 3
1215
- self.between_panel_buffer = 5
1216
-
1217
- self.tab_width = 70
1218
- self.tab_height = 20
1219
- self.tab_tab_buffer = 10
1220
- self.tab_initial_buffer = 30
1221
- self.tab_text_buffer = 2
1222
- self.edge_page_buffer = 4
1223
- self.rounded_radius = 3
1224
-
1225
- self.bitmap_text_buffer = 5
1226
- self.dropdown_height = 20
1227
- self.overflow_width = 20
1228
- self.text_dropdown_buffer = 7
1229
- self.show_labels = True
1230
-
1231
- self.establish_colors()
1232
-
1233
- self.current_page = None
1234
- self.hover_tab = None
1235
- self.hover_button = None
1236
- self.hover_dropdown = None
1237
-
1238
- def establish_colors(self):
1239
- self.text_color = copy.copy(
1240
- wx.SystemSettings().GetColour(wx.SYS_COLOUR_BTNTEXT)
1241
- )
1242
- self.text_color_inactive = copy.copy(self.text_color).ChangeLightness(50)
1243
- self.text_color_disabled = wx.Colour("Dark Grey")
1244
- self.black_color = copy.copy(
1245
- wx.SystemSettings().GetColour(wx.SYS_COLOUR_BTNTEXT)
1246
- )
1247
-
1248
- self.button_face_hover = copy.copy(
1249
- wx.SystemSettings().GetColour(wx.SYS_COLOUR_HIGHLIGHT)
1250
- ).ChangeLightness(150)
1251
- # self.button_face_hover = copy.copy(
1252
- # wx.SystemSettings().GetColour(wx.SYS_COLOUR_GRADIENTACTIVECAPTION)
1253
- # )
1254
- self.inactive_background = copy.copy(
1255
- wx.SystemSettings().GetColour(wx.SYS_COLOUR_INACTIVECAPTION)
1256
- )
1257
- self.inactive_text = copy.copy(
1258
- wx.SystemSettings().GetColour(wx.SYS_COLOUR_GRAYTEXT)
1259
- )
1260
- self.tooltip_foreground = copy.copy(
1261
- wx.SystemSettings().GetColour(wx.SYS_COLOUR_INFOTEXT)
1262
- )
1263
- self.tooltip_background = copy.copy(
1264
- wx.SystemSettings().GetColour(wx.SYS_COLOUR_INFOBK)
1265
- )
1266
- self.button_face = copy.copy(
1267
- wx.SystemSettings().GetColour(wx.SYS_COLOUR_BTNHILIGHT)
1268
- )
1269
- self.ribbon_background = copy.copy(
1270
- wx.SystemSettings().GetColour(wx.SYS_COLOUR_BTNHILIGHT)
1271
- )
1272
- self.highlight = copy.copy(
1273
- wx.SystemSettings().GetColour(wx.SYS_COLOUR_HOTLIGHT)
1274
- )
1275
-
1276
- # Do we have a setting for the color?
1277
- c_mode = self.parent.context.root.setting(int, "ribbon_color", 0)
1278
- # 0 system default
1279
- # 1 colored background
1280
- # 2 forced dark_mode
1281
- if c_mode < COLOR_MODE_DEFAULT or c_mode > COLOR_MODE_DARK:
1282
- c_mode = COLOR_MODE_DEFAULT
1283
- if (
1284
- c_mode == COLOR_MODE_DEFAULT
1285
- and wx.SystemSettings().GetColour(wx.SYS_COLOUR_WINDOW)[0] < 127
1286
- ):
1287
- c_mode = COLOR_MODE_DARK # dark mode
1288
- self.color_mode = c_mode
1289
-
1290
- if self.color_mode == COLOR_MODE_DARK:
1291
- # This is rather crude, as a dark mode could also
1292
- # be based eg on a dark blue scheme
1293
- self.button_face = wx.BLACK
1294
- self.ribbon_background = wx.BLACK
1295
- self.text_color = wx.WHITE
1296
- self.text_color_inactive = copy.copy(self.text_color)
1297
- self.text_color_disabled = wx.Colour("Light Grey")
1298
- self.black_color = wx.WHITE
1299
- self.inactive_background = wx.BLACK
1300
- OS_NAME = platform.system()
1301
- if OS_NAME == "Windows":
1302
- self.button_face_hover = wx.BLUE
1303
- if self.color_mode == COLOR_MODE_COLOR:
1304
- self.ribbon_background = copy.copy(
1305
- wx.SystemSettings().GetColour(wx.SYS_COLOUR_GRADIENTINACTIVECAPTION)
1306
- )
1307
- self.button_face = copy.copy(
1308
- wx.SystemSettings().GetColour(wx.SYS_COLOUR_GRADIENTACTIVECAPTION)
1309
- )
1310
- self.button_face_hover = wx.Colour("gold").ChangeLightness(150)
1311
- self.highlight = wx.Colour("gold")
1312
-
1313
- # Let's adjust the fontsize for the page headers
1314
- screen_wd, screen_ht = wx.GetDisplaySize()
1315
- ptdefault = 10
1316
- if screen_wd <= 800 or screen_ht <= 600:
1317
- ptdefault = 8
1318
- self.tab_height = 16
1319
- try:
1320
- wxsize = wx.Size(ptdefault, ptdefault)
1321
- dipsize = self.parent.FromDIP(wxsize)
1322
- ptsize = int(dipsize[0])
1323
- except AttributeError:
1324
- ptsize = ptdefault
1325
- self.default_font = wx.Font(
1326
- ptsize, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL
1327
- )
1328
-
1329
- def paint_main(self, dc, ribbon):
1330
- """
1331
- Main paint routine. This should delegate, in paint order, to the things on screen that require painting.
1332
- @return:
1333
- """
1334
- self._paint_background(dc)
1335
- self.parent.validate_current_page()
1336
-
1337
- if ribbon.visible_pages() > 1:
1338
- for page in ribbon.pages:
1339
- if page.visible:
1340
- self._paint_tab(dc, page)
1341
- else:
1342
- self.current_page = ribbon.first_page()
1343
- if self.current_page is not None:
1344
- if self.current_page.position is None:
1345
- # print("Was dirty...")
1346
- self.layout(dc, self.parent)
1347
-
1348
- for page in ribbon.pages:
1349
- if page is not self.current_page or not page.visible:
1350
- continue
1351
-
1352
- dc.SetBrush(wx.Brush(self.ribbon_background))
1353
- x, y, x1, y1 = page.position
1354
- dc.DrawRoundedRectangle(
1355
- int(x), int(y), int(x1 - x), int(y1 - y), self.rounded_radius
1356
- )
1357
- for panel in page.panels:
1358
- # We suppress empty panels
1359
- if panel is None or panel.visible_button_count == 0:
1360
- continue
1361
- self._paint_panel(dc, panel)
1362
- for button in panel.visible_buttons():
1363
- self._paint_button(dc, button)
1364
-
1365
- def _paint_tab(self, dc: wx.DC, page: RibbonPage):
1366
- """
1367
- Paint the individual page tab.
1368
-
1369
- @param dc:
1370
- @param page:
1371
- @return:
1372
- """
1373
- horizontal = self.parent.prefer_horizontal()
1374
- highlight_via_color = False
1375
-
1376
- dc.SetPen(wx.Pen(self.black_color))
1377
- show_rect = True
1378
- if page is not self.current_page:
1379
- dc.SetBrush(wx.Brush(self.button_face))
1380
- dc.SetTextForeground(self.text_color_inactive)
1381
- if not highlight_via_color:
1382
- show_rect = False
1383
- else:
1384
- dc.SetBrush(wx.Brush(self.highlight))
1385
- dc.SetBrush(wx.Brush(self.highlight))
1386
- dc.SetTextForeground(self.text_color)
1387
- if not highlight_via_color:
1388
- dc.SetBrush(wx.Brush(self.button_face))
1389
- if page is self.hover_tab and self.hover_button is None:
1390
- dc.SetBrush(wx.Brush(self.button_face_hover))
1391
- show_rect = True
1392
- x, y, x1, y1 = page.tab_position
1393
- if show_rect:
1394
- dc.DrawRoundedRectangle(
1395
- int(x), int(y), int(x1 - x), int(y1 - y), self.rounded_radius
1396
- )
1397
- dc.SetFont(self.default_font)
1398
- text_width, text_height = dc.GetTextExtent(page.label)
1399
- tpx = int(x + (x1 - x - text_width) / 2)
1400
- tpy = int(y + self.tab_text_buffer)
1401
- if horizontal:
1402
- dc.DrawText(page.label, tpx, tpy)
1403
- else:
1404
- tpx = int(x + self.tab_text_buffer)
1405
- tpy = int(y1 - (y1 - y - text_width) / 2)
1406
- dc.DrawRotatedText(page.label, tpx, tpy, 90)
1407
-
1408
- def _paint_background(self, dc: wx.DC):
1409
- """
1410
- Paint the background of the ribbonbar.
1411
- @param dc:
1412
- @return:
1413
- """
1414
- w, h = dc.Size
1415
- dc.SetBrush(wx.Brush(self.ribbon_background))
1416
- dc.SetPen(wx.TRANSPARENT_PEN)
1417
- dc.DrawRectangle(0, 0, w, h)
1418
-
1419
- def _paint_panel(self, dc: wx.DC, panel: RibbonPanel):
1420
- """
1421
- Paint the ribbonpanel of the given panel.
1422
- @param dc:
1423
- @param panel:
1424
- @return:
1425
- """
1426
- if not panel.position:
1427
- # print(f"Panel position was not set for {panel.label}")
1428
- return
1429
- x, y, x1, y1 = panel.position
1430
- # print(f"Painting panel {panel.label}: {panel.position}")
1431
- dc.SetBrush(wx.Brush(self.ribbon_background))
1432
- dc.SetPen(wx.Pen(self.black_color))
1433
- dc.DrawRoundedRectangle(
1434
- int(x), int(y), int(x1 - x), int(y1 - y), self.rounded_radius
1435
- )
1436
- """
1437
- Paint the overflow of buttons that cannot be stored within the required width.
1438
-
1439
- @param dc:
1440
- @return:
1441
- """
1442
- if not panel._overflow_position:
1443
- return
1444
- x, y, x1, y1 = panel._overflow_position
1445
- dc.SetBrush(wx.Brush(self.highlight))
1446
- dc.SetPen(wx.Pen(self.black_color))
1447
- dc.DrawRoundedRectangle(
1448
- int(x), int(y), int(x1 - x), int(y1 - y), self.rounded_radius
1449
- )
1450
- r = min((y1 - y) / 2, (x1 - x) / 2) - 2
1451
- cx = (x + x1) / 2
1452
- cy = -r / 2 + (y + y1) / 2
1453
- # print (f"area: {x},{y}-{x1},{y1} - center={cx},{cy} r={r}")
1454
- # points = [
1455
- # (
1456
- # int(cx + r * math.cos(math.radians(angle))),
1457
- # int(cy + r * math.sin(math.radians(angle))),
1458
- # )
1459
- # for angle in (0, 90, 180)
1460
- # ]
1461
- lp_x = int(cx - r)
1462
- lp_y = int(cy)
1463
- dp_x = int(cx)
1464
- dp_y = int(cy + r)
1465
- points = [(lp_x, lp_y), (dp_x, dp_y), (2 * dp_x - lp_x, lp_y)]
1466
- dc.SetPen(wx.Pen(self.black_color))
1467
- dc.SetBrush(wx.Brush(self.inactive_background))
1468
- dc.DrawPolygon(points)
1469
-
1470
- def _paint_dropdown(self, dc: wx.DC, dropdown: DropDown):
1471
- """
1472
- Paint the dropdown on the button containing a dropdown.
1473
-
1474
- @param dc:
1475
- @param dropdown:
1476
- @return:
1477
- """
1478
- x, y, x1, y1 = dropdown.position
1479
- if dropdown is self.hover_dropdown:
1480
- dc.SetBrush(wx.Brush(wx.Colour(self.highlight)))
1481
- else:
1482
- dc.SetBrush(wx.TRANSPARENT_BRUSH)
1483
- dc.SetPen(wx.TRANSPARENT_PEN)
1484
-
1485
- dc.DrawRoundedRectangle(
1486
- int(x), int(y), int(x1 - x), int(y1 - y), self.rounded_radius
1487
- )
1488
- r = min((y1 - y) / 2, (x1 - x) / 2) - 2
1489
- cx = (x + x1) / 2
1490
- cy = -r / 2 + (y + y1) / 2
1491
- # print (f"area: {x},{y}-{x1},{y1} - center={cx},{cy} r={r}")
1492
- # points = [
1493
- # (
1494
- # int(cx + r * math.cos(math.radians(angle))),
1495
- # int(cy + r * math.sin(math.radians(angle))),
1496
- # )
1497
- # for angle in (0, 90, 180)
1498
- # ]
1499
- lp_x = int(cx - r)
1500
- lp_y = int(cy)
1501
- dp_x = int(cx)
1502
- dp_y = int(cy + r)
1503
- points = [(lp_x, lp_y), (dp_x, dp_y), (2 * dp_x - lp_x, lp_y)]
1504
- dc.SetPen(wx.Pen(self.black_color))
1505
- dc.SetBrush(wx.Brush(self.inactive_background))
1506
- dc.DrawPolygon(points)
1507
-
1508
- def _paint_button(self, dc: wx.DC, button: Button):
1509
- """
1510
- Paint the given button on the screen.
1511
-
1512
- @param dc:
1513
- @param button:
1514
- @return:
1515
- """
1516
- if button.overflow or not button.visible or button.position is None:
1517
- return
1518
-
1519
- dc.SetBrush(wx.Brush(self.button_face))
1520
- dc.SetPen(wx.TRANSPARENT_PEN)
1521
- dc.SetTextForeground(self.text_color)
1522
- if not button.enabled:
1523
- dc.SetBrush(wx.Brush(self.inactive_background))
1524
- dc.SetPen(wx.TRANSPARENT_PEN)
1525
- dc.SetTextForeground(self.text_color_disabled)
1526
- if button.toggle:
1527
- dc.SetBrush(wx.Brush(self.highlight))
1528
- dc.SetPen(wx.Pen(self.black_color))
1529
- if self.hover_button is button and self.hover_dropdown is None:
1530
- dc.SetBrush(wx.Brush(self.button_face_hover))
1531
- dc.SetPen(wx.Pen(self.black_color))
1532
-
1533
- x, y, x1, y1 = button.position
1534
- w = int(round(x1 - x, 2))
1535
- h = int(round(y1 - y, 2))
1536
- img_h = h
1537
- # do we have text? if yes let's reduce the available space in y
1538
- if self.show_labels: # Regardless whether we have a label or not...
1539
- img_h -= self.bitmap_text_buffer
1540
- ptsize = min(18, int(round(min(w, img_h) / 5.0, 2)) * 2)
1541
- img_h -= int(ptsize * 1.35)
1542
-
1543
- button.get_bitmaps(min(w, img_h))
1544
- if button.enabled:
1545
- bitmap = button.bitmap
1546
- else:
1547
- bitmap = button.bitmap_disabled
1548
-
1549
- # Let's clip the output
1550
- dc.SetClippingRegion(int(x), int(y), int(w), int(h))
1551
-
1552
- dc.DrawRoundedRectangle(int(x), int(y), int(w), int(h), self.rounded_radius)
1553
- bitmap_width, bitmap_height = bitmap.Size
1554
- # if button.label in ("Circle", "Ellipse", "Wordlist", "Property Window"):
1555
- # print (f"N - {button.label}: {bitmap_width}x{bitmap_height} in {w}x{h}")
1556
- bs = min(bitmap_width, bitmap_height)
1557
- ptsize = self.get_font_size(bs)
1558
- font = wx.Font(
1559
- ptsize, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL
1560
- )
1561
-
1562
- dc.DrawBitmap(bitmap, int(x + (w - bitmap_width) / 2), int(y))
1563
- y += bitmap_height
1564
-
1565
- text_edge = self.bitmap_text_buffer
1566
-
1567
- if button.label and self.show_labels:
1568
- show_text = True
1569
- label_text = list(button.label.split(" "))
1570
- # We try to establish whether this would fit properly.
1571
- # We allow a small oversize of 25% to the button,
1572
- # before we try to reduce the fontsize
1573
- wouldfit = False
1574
- while not wouldfit:
1575
- testfont = wx.Font(
1576
- ptsize,
1577
- wx.FONTFAMILY_SWISS,
1578
- wx.FONTSTYLE_NORMAL,
1579
- wx.FONTWEIGHT_NORMAL,
1580
- )
1581
- test_y = y + text_edge
1582
- dc.SetFont(testfont)
1583
- wouldfit = True
1584
- i = 0
1585
- while i < len(label_text):
1586
- # We know by definition that all single words
1587
- # are okay for drawing, now we check whether
1588
- # we can draw multiple in one line
1589
- word = label_text[i]
1590
- cont = True
1591
- while cont:
1592
- cont = False
1593
- if i < len(label_text) - 1:
1594
- nextword = label_text[i + 1]
1595
- test = word + " " + nextword
1596
- tw, th = dc.GetTextExtent(test)
1597
- if tw < w:
1598
- word = test
1599
- i += 1
1600
- cont = True
1601
-
1602
- text_width, text_height = dc.GetTextExtent(word)
1603
- if text_width > w:
1604
- wouldfit = False
1605
- break
1606
- test_y += text_height
1607
- if test_y > y1:
1608
- wouldfit = False
1609
- text_edge = 0
1610
- break
1611
- i += 1
1612
-
1613
- if wouldfit:
1614
- font = testfont
1615
- break
1616
-
1617
- ptsize -= 2
1618
- if ptsize < 6: # too small
1619
- break
1620
- if not wouldfit:
1621
- show_text = False
1622
- label_text = list()
1623
- else:
1624
- show_text = False
1625
- label_text = list()
1626
- if show_text:
1627
- y += text_edge
1628
- dc.SetFont(font)
1629
- i = 0
1630
- while i < len(label_text):
1631
- # We know by definition that all single words
1632
- # are okay for drawing, now we check whether
1633
- # we can draw multiple in one line
1634
- word = label_text[i]
1635
- cont = True
1636
- while cont:
1637
- cont = False
1638
- if i < len(label_text) - 1:
1639
- nextword = label_text[i + 1]
1640
- test = word + " " + nextword
1641
- tw, th = dc.GetTextExtent(test)
1642
- if tw < w:
1643
- word = test
1644
- i += 1
1645
- cont = True
1646
-
1647
- text_width, text_height = dc.GetTextExtent(word)
1648
- dc.DrawText(
1649
- word,
1650
- int(x + (w / 2.0) - (text_width / 2)),
1651
- int(y),
1652
- )
1653
- y += text_height
1654
- i += 1
1655
- if button.dropdown is not None and button.dropdown.position is not None:
1656
- self._paint_dropdown(dc, button.dropdown)
1657
- dc.DestroyClippingRegion()
1658
-
1659
- def layout(self, dc: wx.DC, ribbon):
1660
- """
1661
- Performs the layout of the page. This is determined to be the size of the ribbon minus any edge buffering.
1662
-
1663
- @param dc:
1664
- @param art:
1665
- @return:
1666
- """
1667
- ribbon_width, ribbon_height = dc.Size
1668
- # print(f"ribbon: {dc.Size}")
1669
- horizontal = self.parent.prefer_horizontal()
1670
- xpos = 0
1671
- ypos = 0
1672
- has_page_header = ribbon.visible_pages() > 1
1673
- dc.SetFont(self.default_font)
1674
- for pn, page in enumerate(ribbon.pages):
1675
- if not page.visible:
1676
- continue
1677
- # Set tab positioning.
1678
- # Compute tabwidth according to be displayed label,
1679
- # if bigger than default then extend width
1680
- if has_page_header:
1681
- line_width, line_height = dc.GetTextExtent(page.label)
1682
- if line_height + 4 > self.tab_height:
1683
- self.tab_height = line_height + 4
1684
- for former in range(0, pn):
1685
- former_page = ribbon.pages[former]
1686
- t_x, t_y, t_x1, t_y1 = former_page.tab_position
1687
- if horizontal:
1688
- t_y1 = t_y + self.tab_height * 2
1689
- else:
1690
- t_x1 = t_x + self.tab_height * 2
1691
- former_page.tab_position = (t_x, t_y, t_x1, t_y1)
1692
-
1693
- tabwidth = max(line_width + 2 * self.tab_tab_buffer, self.tab_width)
1694
- if horizontal:
1695
- t_x = pn * self.tab_tab_buffer + xpos + self.tab_initial_buffer
1696
- t_x1 = t_x + tabwidth
1697
- t_y = ypos
1698
- t_y1 = t_y + self.tab_height * 2
1699
- else:
1700
- t_y = pn * self.tab_tab_buffer + ypos + self.tab_initial_buffer
1701
- t_y1 = t_y + tabwidth
1702
- t_x = xpos
1703
- t_x1 = t_x + self.tab_height * 2
1704
- page.tab_position = (t_x, t_y, t_x1, t_y1)
1705
- if horizontal:
1706
- xpos += tabwidth
1707
- else:
1708
- ypos += tabwidth
1709
- else:
1710
- page.tab_position = (0, 0, 0, 0)
1711
- if page is not self.current_page:
1712
- continue
1713
-
1714
- page_width = ribbon_width - self.edge_page_buffer
1715
- page_height = ribbon_height - self.edge_page_buffer
1716
- if horizontal:
1717
- page_width -= self.edge_page_buffer
1718
- x = self.edge_page_buffer
1719
- if has_page_header:
1720
- y = self.tab_height
1721
- page_height -= self.tab_height
1722
- else:
1723
- x = self.edge_page_buffer
1724
- y = 0
1725
- else:
1726
- page_height -= self.edge_page_buffer
1727
- y = self.edge_page_buffer
1728
- if has_page_header:
1729
- x = self.tab_height
1730
- page_width -= self.tab_height
1731
- else:
1732
- y = self.edge_page_buffer
1733
- x = 0
1734
-
1735
- # Page start position.
1736
- if horizontal:
1737
- if has_page_header:
1738
- y = self.tab_height
1739
- page_height += self.edge_page_buffer
1740
- else:
1741
- x = self.edge_page_buffer
1742
- y = 0
1743
- else:
1744
- if has_page_header:
1745
- x = self.tab_height
1746
- page_width += self.edge_page_buffer
1747
- else:
1748
- y = self.edge_page_buffer
1749
- x = 0
1750
- # Set page position.
1751
- page.position = (
1752
- x,
1753
- y,
1754
- x + page_width,
1755
- y + page_height,
1756
- )
1757
-
1758
- # if self.parent.visible_pages() == 1:
1759
- # print(f"page: {page.position}")
1760
- self.page_layout(dc, page)
1761
-
1762
- def preferred_button_size_for_page(self, dc, page):
1763
- x, y, max_x, max_y = page.position
1764
- page_width = max_x - x
1765
- page_height = max_y - y
1766
- horizontal = self.parent.prefer_horizontal()
1767
- is_horizontal = (self.orientation == self.RIBBON_ORIENTATION_HORIZONTAL) or (
1768
- horizontal and self.orientation == self.RIBBON_ORIENTATION_AUTO
1769
- )
1770
- # Count buttons and panels
1771
- total_button_count = 0
1772
- panel_count = 0
1773
- for panel in page.panels:
1774
- plen = panel.visible_button_count
1775
- total_button_count += plen
1776
- if plen > 0:
1777
- panel_count += 1
1778
- # else:
1779
- # print(f"No buttons for {panel.label} found during layout")
1780
- # Calculate h/v counts for panels and buttons
1781
- if is_horizontal:
1782
- all_button_horizontal = max(total_button_count, 1)
1783
- all_button_vertical = 1
1784
-
1785
- all_panel_horizontal = max(panel_count, 1)
1786
- all_panel_vertical = 1
1787
- else:
1788
- all_button_horizontal = 1
1789
- all_button_vertical = max(total_button_count, 1)
1790
-
1791
- all_panel_horizontal = 1
1792
- all_panel_vertical = max(panel_count, 1)
1793
-
1794
- # Calculate optimal width/height for just buttons.
1795
- button_width_across_panels = page_width
1796
- button_width_across_panels -= (
1797
- all_panel_horizontal - 1
1798
- ) * self.between_panel_buffer
1799
- button_width_across_panels -= 2 * self.page_panel_buffer
1800
-
1801
- button_height_across_panels = page_height
1802
- button_height_across_panels -= (
1803
- all_panel_vertical - 1
1804
- ) * self.between_panel_buffer
1805
- button_height_across_panels -= 2 * self.page_panel_buffer
1806
-
1807
- for p, panel in enumerate(page.panels):
1808
- if p == 0:
1809
- # Remove high-and-low perpendicular panel_button_buffer
1810
- if is_horizontal:
1811
- button_height_across_panels -= 2 * self.panel_button_buffer
1812
- else:
1813
- button_width_across_panels -= 2 * self.panel_button_buffer
1814
- for b, button in enumerate(list(panel.visible_buttons())):
1815
- if b == 0:
1816
- # First and last buffers.
1817
- if is_horizontal:
1818
- button_width_across_panels -= 2 * self.panel_button_buffer
1819
- else:
1820
- button_height_across_panels -= 2 * self.panel_button_buffer
1821
- else:
1822
- # Each gap between buttons
1823
- if is_horizontal:
1824
- button_width_across_panels -= self.between_button_buffer
1825
- else:
1826
- button_height_across_panels -= self.between_button_buffer
1827
-
1828
- # Calculate width/height for each button.
1829
- button_width = button_width_across_panels / all_button_horizontal
1830
- button_height = button_height_across_panels / all_button_vertical
1831
-
1832
- return button_width, button_height
1833
-
1834
- def page_layout(self, dc, page):
1835
- """
1836
- Determine the layout of the page. This calls for each panel to be set relative to the number of buttons it
1837
- contains.
1838
-
1839
- @param dc:
1840
- @param art:
1841
- @return:
1842
- """
1843
- x, y, max_x, max_y = page.position
1844
- is_horizontal = (self.orientation == self.RIBBON_ORIENTATION_HORIZONTAL) or (
1845
- self.parent.prefer_horizontal()
1846
- and self.orientation == self.RIBBON_ORIENTATION_AUTO
1847
- )
1848
- button_width, button_height = self.preferred_button_size_for_page(dc, page)
1849
- x += self.page_panel_buffer
1850
- y += self.page_panel_buffer
1851
- """
1852
- THIS ALGORITHM NEEDS STILL TO BE IMPLEMENTED
1853
- --------------------------------------------
1854
- We discuss now the horizontal case, the same
1855
- logic would apply for the vertical case.
1856
- We iterate through the sizes and establish the space
1857
- needed for every panel:
1858
- 1) We calculate the required button dimensions for all
1859
- combinations of tiny/small/regular icons plus with/without labels
1860
- 2) We get the minimum amount of columns required to display
1861
- the buttons (taking the vertical extent ie the amount
1862
- of available rows into account).
1863
- This will provide us with a solution that would need
1864
- the least horizontal space.
1865
- 3) That may lead to a situation where you would still
1866
- have horizontal space available for the panels.
1867
- Hence we do a second pass where we assign additional space
1868
- to all panels that need more than one row of icons.
1869
- As we will do this for all possible size combinations,
1870
- we will chose eventually that solution that has the
1871
- fewest amount of buttons in overflow.
1872
- """
1873
- # 1 Calculate button sizes - this is not required
1874
- # here, already done during button creation
1875
-
1876
- # 2 Loop over all sizes
1877
-
1878
- # Now that we have gathered all information we can assign
1879
- # the space...
1880
- available_space = 0
1881
- for p, panel in enumerate(page.panels):
1882
- if p != 0:
1883
- # Non-first move between panel gap.
1884
- if is_horizontal:
1885
- x += self.between_panel_buffer
1886
- else:
1887
- y += self.between_panel_buffer
1888
-
1889
- if is_horizontal:
1890
- single_panel_horizontal = max(panel.visible_button_count, 1)
1891
- single_panel_vertical = 1
1892
- else:
1893
- single_panel_horizontal = 1
1894
- single_panel_vertical = max(panel.visible_button_count, 1)
1895
-
1896
- panel_width = (
1897
- single_panel_horizontal * button_width
1898
- + (single_panel_horizontal - 1) * self.between_button_buffer
1899
- + 2 * self.panel_button_buffer
1900
- )
1901
- panel_height = (
1902
- single_panel_vertical * button_height
1903
- + (single_panel_vertical - 1) * self.between_button_buffer
1904
- + 2 * self.panel_button_buffer
1905
- )
1906
- if is_horizontal:
1907
- sx = available_space
1908
- sy = 0
1909
- else:
1910
- sx = 0
1911
- sy = available_space
1912
- panel_width += sx
1913
- panel_height += sy
1914
- panel.position = x, y, x + panel_width, y + panel_height
1915
- panel_max_x, panel_max_y = self.panel_layout(dc, panel)
1916
- # Do we have more space than needed?
1917
- available_space = 0
1918
- if panel._overflow_position is None:
1919
- # print (f"({x}, {y}) - ({x + panel_width}, {y+panel_height}), sx={sx}, sy={sy}")
1920
- if is_horizontal:
1921
- available_space = max(
1922
- 0, x + panel_width - panel_max_x - self.panel_button_buffer
1923
- )
1924
- # print (f"x={x + panel_width}, {panel_max_x} will become: {panel_max_x + self.panel_button_buffer}, available={available_space}")
1925
- if available_space != 0:
1926
- panel_width = panel_max_x + self.panel_button_buffer - x
1927
- else:
1928
- available_space = max(
1929
- 0, y + panel_height - panel_max_y - self.panel_button_buffer
1930
- )
1931
- # print (f"y={y + panel_height}, {panel_max_y} will become: {panel_max_y + self.panel_button_buffer}, available={available_space}")
1932
- if available_space != 0:
1933
- panel_height = panel_max_y + self.panel_button_buffer - y
1934
- panel.position = x, y, x + panel_width, y + panel_height
1935
-
1936
- if is_horizontal:
1937
- x += panel_width
1938
- else:
1939
- y += panel_height
1940
-
1941
- def panel_layout(self, dc: wx.DC, panel):
1942
- x, y, max_x, max_y = panel.position
1943
- panel_width = max_x - x
1944
- panel_height = max_y - y
1945
- # print(f"Panel: {panel.label}: {panel.position}")
1946
- horizontal = self.parent.prefer_horizontal()
1947
- is_horizontal = (self.orientation == self.RIBBON_ORIENTATION_HORIZONTAL) or (
1948
- horizontal and self.orientation == self.RIBBON_ORIENTATION_AUTO
1949
- )
1950
- plen = panel.visible_button_count
1951
- # if plen == 0:
1952
- # print(f"layout for panel '{panel.label}' without buttons!")
1953
-
1954
- distribute_evenly = False
1955
- if is_horizontal:
1956
- button_horizontal = max(plen, 1)
1957
- button_vertical = 1
1958
- else:
1959
- button_horizontal = 1
1960
- button_vertical = max(plen, 1)
1961
-
1962
- all_button_width = (
1963
- panel_width
1964
- - (button_horizontal - 1) * self.between_button_buffer
1965
- - 2 * self.panel_button_buffer
1966
- )
1967
- all_button_height = (
1968
- panel_height
1969
- - (button_vertical - 1) * self.between_button_buffer
1970
- - 2 * self.panel_button_buffer
1971
- )
1972
-
1973
- button_width = all_button_width / button_horizontal
1974
- button_height = all_button_height / button_vertical
1975
- # 'tiny'-size of a default 50x50 icon
1976
- minim_size = 15
1977
- button_width = max(minim_size, button_width)
1978
- button_height = max(minim_size, button_height)
1979
-
1980
- x += self.panel_button_buffer
1981
- y += self.panel_button_buffer
1982
- panel._overflow.clear()
1983
- panel._overflow_position = None
1984
- lastbutton = None
1985
- for b, button in enumerate(list(panel.visible_buttons())):
1986
- found = False
1987
- bitmapsize = button.max_size
1988
- while bitmapsize > button.min_size:
1989
- if bitmapsize <= button_height and bitmapsize <= button_width:
1990
- break
1991
- bitmapsize -= 5
1992
- button.get_bitmaps(bitmapsize)
1993
- self.button_calc(dc, button)
1994
- if b == 0:
1995
- max_width = button.min_width
1996
- max_height = button.min_height
1997
- else:
1998
- max_width = max(max_width, button.min_width)
1999
- max_height = max(max_height, button.min_height)
2000
-
2001
- target_height = button_height
2002
- target_width = button_width
2003
- # print(f"Target: {panel.label} - {target_width}x{target_height}")
2004
- for b, button in enumerate(list(panel.visible_buttons())):
2005
- button.overflow = False
2006
- this_width = target_width
2007
- this_height = target_height
2008
- local_width = 1.25 * button.min_width
2009
- local_height = 1.25 * button.min_height
2010
- if not distribute_evenly:
2011
- if button_horizontal > 1 or is_horizontal:
2012
- this_width = min(this_width, local_width)
2013
- if button_vertical > 1 or not is_horizontal:
2014
- this_height = min(this_height, local_height)
2015
- if b != 0:
2016
- # Move across button gap if not first button.
2017
- if is_horizontal:
2018
- x += self.between_button_buffer
2019
- else:
2020
- y += self.between_button_buffer
2021
- button.position = x, y, x + this_width, y + this_height
2022
- if is_horizontal:
2023
- is_overflow = False
2024
- if x + this_width > panel.position[2]:
2025
- is_overflow = True
2026
- # Let's establish whether there is place for another row of icons underneath
2027
- # print(
2028
- # f"Horizontal Overflow: y={y}, b-height={max_height}, new max={y + 2 * max_height + self.panel_button_buffer}, panel: {panel.position[3]}"
2029
- # )
2030
- if (
2031
- y + 2 + max_height + self.panel_button_buffer
2032
- < panel.position[3]
2033
- ):
2034
- is_overflow = False
2035
- target_height = max_height
2036
- # Reset height of all previous buttons:
2037
- for bb, bbutton in enumerate(list(panel.visible_buttons())):
2038
- if bb >= b:
2039
- break
2040
- bbutton.position = (
2041
- bbutton.position[0],
2042
- bbutton.position[1],
2043
- bbutton.position[2],
2044
- bbutton.position[1] + max_height,
2045
- )
2046
- x = panel.position[0] + self.panel_button_buffer
2047
- y += max_height + self.panel_button_buffer
2048
- button.position = x, y, x + this_width, y + max_height
2049
- if is_overflow:
2050
- button.overflow = True
2051
- panel._overflow.append(button)
2052
- if panel._overflow_position is None:
2053
- ppx, ppy, ppx1, ppy1 = panel.position
2054
- panel._overflow_position = (ppx1 - 15, ppy, ppx1, ppy1)
2055
- if (
2056
- lastbutton is not None
2057
- and lastbutton.position[2] >= ppx1 - 15
2058
- ):
2059
- # That overlaps, not good, so add this button
2060
- # to the overflow area too
2061
- lastbutton.overflow = True
2062
- panel._overflow.insert(0, lastbutton)
2063
- else:
2064
- is_overflow = False
2065
- if y + this_height > panel.position[3]:
2066
- is_overflow = True
2067
- # print(
2068
- # f"Vertical Overflow: x={x}, b-width={max_width}, new max={x + 2 * max_width + self.panel_button_buffer}, panel: {panel.position[2]}"
2069
- # )
2070
- # Let's establish whether there is place for another column of icons to the right
2071
- if x + 2 * max_width + self.panel_button_buffer < panel.position[2]:
2072
- is_overflow = False
2073
- target_width = max_width
2074
- # Reset width of all previous buttons:
2075
- for bb, bbutton in enumerate(list(panel.visible_buttons())):
2076
- if bb >= b:
2077
- break
2078
- bbutton.position = (
2079
- bbutton.position[0],
2080
- bbutton.position[1],
2081
- bbutton.position[0] + max_width,
2082
- bbutton.position[3],
2083
- )
2084
- y = panel.position[1] + self.panel_button_buffer
2085
- x += max_width + self.panel_button_buffer
2086
- button.position = x, y, x + max_width, y + this_height
2087
- if is_overflow:
2088
- button.overflow = True
2089
- panel._overflow.append(button)
2090
- if panel._overflow_position is None:
2091
- ppx, ppy, ppx1, ppy1 = panel.position
2092
- panel._overflow_position = (ppx, ppy1 - 15, ppx1, ppy1)
2093
- if (
2094
- lastbutton is not None
2095
- and lastbutton.position[3] >= ppy1 - 15
2096
- ):
2097
- # That overlaps, not good, so add this button
2098
- # to the overflow area too
2099
- lastbutton.overflow = True
2100
- panel._overflow.insert(0, lastbutton)
2101
-
2102
- # print(f"button: {button.position}")
2103
- self.button_layout(dc, button)
2104
-
2105
- if is_horizontal:
2106
- x += this_width
2107
- else:
2108
- y += this_height
2109
- lastbutton = button
2110
- return min(x, panel.position[2]), min(y, panel.position[3])
2111
-
2112
- def button_calc(self, dc: wx.DC, button):
2113
- bitmap = button.bitmap
2114
- ptsize = self.get_font_size(button.icon_size)
2115
- font = wx.Font(
2116
- ptsize, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL
2117
- )
2118
-
2119
- dc.SetFont(font)
2120
- bitmap_width, bitmap_height = bitmap.Size
2121
- bitmap_height = max(bitmap_height, button.icon_size)
2122
- bitmap_width = max(bitmap_width, button.icon_size)
2123
-
2124
- # Calculate text height/width
2125
- text_width = 0
2126
- text_height = 0
2127
- if button.label and self.show_labels:
2128
- label_text = list(button.label.split(" "))
2129
- i = 0
2130
- while i < len(label_text):
2131
- # We know by definition that all single words
2132
- # are okay for drawing, now we check whether
2133
- # we can draw multiple in one line
2134
- word = label_text[i]
2135
- cont = True
2136
- while cont:
2137
- cont = False
2138
- if i < len(label_text) - 1:
2139
- nextword = label_text[i + 1]
2140
- test = word + " " + nextword
2141
- tw, th = dc.GetTextExtent(test)
2142
- if tw < bitmap_width:
2143
- word = test
2144
- i += 1
2145
- cont = True
2146
- line_width, line_height = dc.GetTextExtent(word)
2147
- text_width = max(text_width, line_width)
2148
- text_height += line_height
2149
- i += 1
2150
-
2151
- # Calculate button_width/button_height
2152
- button_width = max(bitmap_width, text_width)
2153
- button_height = bitmap_height
2154
- button_height += 2 * self.panel_button_buffer
2155
- button_width += 2 * self.panel_button_buffer
2156
- if button.label and self.show_labels:
2157
- # button_height += + self.panel_button_buffer
2158
- button_height += self.bitmap_text_buffer + text_height
2159
-
2160
- button.min_width = button_width
2161
- button.min_height = button_height
2162
- # print (f"layout for {button.label} ({button.bitmapsize}): {button.min_width}x{button.min_height}, icon={bitmap_width}x{bitmap_height}")
2163
-
2164
- def button_layout(self, dc: wx.DC, button):
2165
- x, y, max_x, max_y = button.position
2166
- bitmap = button.bitmap
2167
- bitmap_width, bitmap_height = bitmap.Size
2168
- if button.kind == "hybrid" and button.key != "toggle":
2169
- # Calculate text height/width
2170
- # Calculate dropdown
2171
- # Same size regardless of bitmap-size
2172
- sizx = 15
2173
- sizy = 15
2174
- # Let's see whether we have enough room
2175
- extx = (x + max_x) / 2 + bitmap_width / 2 + sizx - 1
2176
- exty = y + bitmap_height + sizy - 1
2177
- extx = max(x - sizx, min(extx, max_x - 1))
2178
- exty = max(y + sizy, min(exty, max_y - 1))
2179
- gap = 5
2180
- button.dropdown.position = (
2181
- extx - sizx,
2182
- exty - sizy - gap,
2183
- extx,
2184
- exty - gap,
2185
- )
2186
- # button.dropdown.position = (
2187
- # x + bitmap_width / 2,
2188
- # y + bitmap_height / 2,
2189
- # x + bitmap_width,
2190
- # y + bitmap_height,
2191
- # )
2192
- # print (
2193
- # f"Required for {button.label}: button: {x},{y} to {max_x},{max_y}," +
2194
- # f"dropd: {extx-sizx},{exty-sizy} to {extx},{exty}"
2195
- # )
2196
-
2197
- def get_font_size(self, imgsize):
2198
- if imgsize <= 20:
2199
- ptsize = 6
2200
- elif imgsize <= 30:
2201
- ptsize = 8
2202
- elif imgsize <= 40:
2203
- ptsize = 10
2204
- elif imgsize <= 60:
2205
- ptsize = 12
2206
- elif imgsize <= 80:
2207
- ptsize = 14
2208
- else:
2209
- ptsize = 16
2210
- return ptsize
1
+ """
2
+ The RibbonBar is a scratch control widget. All the buttons are dynamically generated. The contents of those individual
3
+ ribbon panels are defined by implementing classes.
4
+
5
+ The primary method of defining a panel is by calling the `set_buttons()` on the panel.
6
+
7
+ control_panel.set_buttons(
8
+ {
9
+ "label": _("Red Dot On"),
10
+ "icon": icons8_flash_on,
11
+ "tip": _("Turn Redlight On"),
12
+ "action": lambda v: service("red on\n"),
13
+ "toggle": {
14
+ "label": _("Red Dot Off"),
15
+ "action": lambda v: service("red off\n"),
16
+ "icon": icons8_flash_off,
17
+ "signal": "grbl_red_dot",
18
+ },
19
+ "rule_enabled": lambda v: has_red_dot_enabled(),
20
+ }
21
+ )
22
+
23
+ Would, for example, register a button in the control panel the definitions for label, icon, tip, action are all
24
+ standard with regard to buttons.
25
+
26
+ The toggle defines an alternative set of values for the toggle state of the button.
27
+
28
+ The multi defines a series of alternative states, and creates a hybrid button with a drop-down to select the state
29
+ desired.
30
+
31
+ Other properties like `rule_enabled` provides a check for whether this button should be enabled or not.
32
+
33
+ The `toggle_attr` will permit a toggle to set an attribute on the given `object` which would default to the root
34
+ context but could need to set a more local object attribute.
35
+
36
+ If a `signal` is assigned as an aspect of multi it triggers that option multi-button option.
37
+ If a `signal` is assigned within the toggle it sets the state of the given toggle. These should be compatible with
38
+ the signals issued by choice panels.
39
+
40
+ The action is a function which is run when the button is pressed.
41
+ """
42
+
43
+ import copy
44
+ import platform
45
+ import threading
46
+
47
+ import wx
48
+
49
+ from meerk40t.gui.icons import STD_ICON_SIZE
50
+ from meerk40t.kernel import Job
51
+ from meerk40t.svgelements import Color
52
+
53
+ _ = wx.GetTranslation
54
+
55
+ COLOR_MODE_DEFAULT = 0
56
+ COLOR_MODE_COLOR = 1
57
+ COLOR_MODE_DARK = 2
58
+
59
+
60
+ class DropDown:
61
+ """
62
+ Dropdowns are the triangle click addons that expand the button list to having other functions.
63
+
64
+ This primarily stores the position of the given dropdown.
65
+ """
66
+
67
+ def __init__(self):
68
+ self.position = None
69
+
70
+ def contains(self, pos):
71
+ """
72
+ Is this drop down hit by this position.
73
+
74
+ @param pos:
75
+ @return:
76
+ """
77
+ if self.position is None:
78
+ return False
79
+ x, y = pos
80
+ return (
81
+ self.position[0] < x < self.position[2]
82
+ and self.position[1] < y < self.position[3]
83
+ )
84
+
85
+
86
+ class Button:
87
+ """
88
+ Buttons store most of the relevant data as to how to display the current aspect of the given button. This
89
+ includes things like tool-tip, the drop-down if needed, whether its in the overflow, the pressed and unpressed
90
+ aspects of the buttons and enable/disable rules.
91
+ """
92
+
93
+ def __init__(self, context, parent, button_id, kind, description):
94
+ self.context = context
95
+ self.parent = parent
96
+ self.id = button_id
97
+ self.kind = kind
98
+ self.button_dict = description
99
+ self.enabled = True
100
+ self.visible = True
101
+ self._aspects = {}
102
+ self.key = "original"
103
+ self.object = None
104
+
105
+ self.position = None
106
+ self.toggle = False
107
+
108
+ self.label = None
109
+ self.icon = None
110
+
111
+ self.bitmap = None
112
+ self.bitmap_disabled = None
113
+
114
+ self.min_size = 15
115
+ self.max_size = 150
116
+
117
+ self.available_bitmaps = {}
118
+ self.available_bitmaps_disabled = {}
119
+
120
+ self.tip = None
121
+ self.help = None
122
+ self.client_data = None
123
+ self.state = 0
124
+ self.dropdown = None
125
+ self.overflow = False
126
+
127
+ self.state_pressed = None
128
+ self.state_unpressed = None
129
+ self.group = None
130
+ self.toggle_attr = None
131
+ self.identifier = None
132
+ self.action = None
133
+ self.action_right = None
134
+ self.rule_enabled = None
135
+ self.rule_visible = None
136
+ self.min_width = 0
137
+ self.min_height = 0
138
+ self.default_width = int(self.max_size / 2)
139
+ self.icon_size = self.default_width
140
+
141
+ self.set_aspect(**description)
142
+ self.apply_enable_rules()
143
+
144
+ def set_aspect(
145
+ self,
146
+ label=None,
147
+ icon=None,
148
+ tip=None,
149
+ help=None,
150
+ group=None,
151
+ toggle_attr=None,
152
+ identifier=None,
153
+ action=None,
154
+ action_right=None,
155
+ rule_enabled=None,
156
+ rule_visible=None,
157
+ object=None,
158
+ **kwargs,
159
+ ):
160
+ """
161
+ This sets all the different aspects that buttons generally have.
162
+
163
+ @param label: button label
164
+ @param icon: icon used for this button
165
+ @param tip: tool tip for the button
166
+ @param help: help information for aspect
167
+ @param group: Group the button exists in for radio-toggles
168
+ @param toggle_attr: The attribute that should be changed on toggle.
169
+ @param identifier: Identifier in the group or toggle
170
+ @param action: Action taken when button is pressed.
171
+ @param action_right: Action taken when button is clicked with right mouse button.
172
+ @param rule_enabled: Rule by which the button is enabled or disabled
173
+ @param rule_visible: Rule by which the button will be hidden or shown
174
+ @param object: object which the toggle_attr is an attr applied to
175
+ @param kwargs:
176
+ @return:
177
+ """
178
+ self.label = label
179
+ resize_param = kwargs.get("size")
180
+ if resize_param is None:
181
+ self.default_width = int(self.max_size / 2)
182
+ else:
183
+ self.default_width = resize_param
184
+
185
+ # We need to cast the icon explicitly to PyEmbeddedImage
186
+ # as otherwise a strange type error is thrown:
187
+ # TypeError: GetBitmap() got an unexpected keyword argument 'force_darkmode'
188
+ # Well...
189
+ from meerk40t.gui.icons import PyEmbeddedImage, VectorIcon
190
+
191
+ if not isinstance(icon, VectorIcon):
192
+ icon = PyEmbeddedImage(icon.data)
193
+ self.icon = icon
194
+
195
+ self.available_bitmaps.clear()
196
+ self.available_bitmaps_disabled.clear()
197
+ self.get_bitmaps(self.default_width)
198
+
199
+ self.tip = tip
200
+ self.help = help
201
+ self.group = group
202
+ self.toggle_attr = toggle_attr
203
+ self.identifier = identifier
204
+ self.action = action
205
+ self.action_right = action_right
206
+ self.rule_enabled = rule_enabled
207
+ self.rule_visible = rule_visible
208
+ if object is not None:
209
+ self.object = object
210
+ else:
211
+ self.object = self.context
212
+ if self.kind == "hybrid":
213
+ self.dropdown = DropDown()
214
+ self.modified()
215
+
216
+ def get_bitmaps(self, point_size):
217
+ top = self.parent.parent.parent
218
+ darkm = bool(top.art.color_mode == COLOR_MODE_DARK)
219
+ if point_size < self.min_size:
220
+ point_size = self.min_size
221
+ if point_size > self.max_size:
222
+ point_size = self.max_size
223
+ self.icon_size = int(point_size)
224
+ edge = int(point_size / 25.0) + 1
225
+ key = str(self.icon_size)
226
+ if key not in self.available_bitmaps:
227
+ self.available_bitmaps[key] = self.icon.GetBitmap(
228
+ resize=self.icon_size,
229
+ noadjustment=True,
230
+ force_darkmode=darkm,
231
+ buffer=edge,
232
+ )
233
+ self.available_bitmaps_disabled[key] = self.icon.GetBitmap(
234
+ resize=self.icon_size,
235
+ color=Color("grey"),
236
+ noadjustment=True,
237
+ buffer=edge,
238
+ )
239
+ self.bitmap = self.available_bitmaps[key]
240
+ self.bitmap_disabled = self.available_bitmaps_disabled[key]
241
+
242
+ def _restore_button_aspect(self, key):
243
+ """
244
+ Restores a saved button aspect for the given key. Given a key to the alternative aspect we restore the given
245
+ aspect.
246
+
247
+ @param key: aspect key to set.
248
+ @return:
249
+ """
250
+ try:
251
+ alt = self._aspects[key]
252
+ except KeyError:
253
+ return
254
+ self.set_aspect(**alt)
255
+ self.key = key
256
+
257
+ def _store_button_aspect(self, key, **kwargs):
258
+ """
259
+ Stores visual aspects of the buttons within the "_aspects" dictionary.
260
+
261
+ This stores the various icons, labels, help, and other properties found on the button.
262
+
263
+ @param key: aspects to store.
264
+ @param kwargs: Additional aspects to implement that are not necessarily currently set on the button.
265
+ @return:
266
+ """
267
+ self._aspects[key] = {
268
+ "action": self.action,
269
+ "action_right": self.action_right,
270
+ "label": self.label,
271
+ "tip": self.tip,
272
+ "help": self.help,
273
+ "icon": self.icon,
274
+ "client_data": self.client_data,
275
+ "rule_enabled": self.rule_enabled,
276
+ "rule_visible": self.rule_visible,
277
+ "toggle_attr": self.toggle_attr,
278
+ "object": self.object,
279
+ }
280
+ self._update_button_aspect(key, **kwargs)
281
+
282
+ def _update_button_aspect(self, key, **kwargs):
283
+ """
284
+ Directly update the button aspects via the kwargs, aspect dictionary *must* exist.
285
+
286
+ @param self:
287
+ @param key:
288
+ @param kwargs:
289
+ @return:
290
+ """
291
+ key_dict = self._aspects[key]
292
+ for k in kwargs:
293
+ if kwargs[k] is not None:
294
+ key_dict[k] = kwargs[k]
295
+
296
+ def apply_enable_rules(self):
297
+ """
298
+ Calls rule_enabled() and returns whether the given rule enables the button.
299
+
300
+ @return:
301
+ """
302
+ if self.rule_enabled is not None:
303
+ try:
304
+ v = self.rule_enabled(0)
305
+ if v != self.enabled:
306
+ self.enabled = v
307
+ self.modified()
308
+ except (AttributeError, TypeError):
309
+ pass
310
+ if self.rule_visible is not None:
311
+ v = self.rule_visible(0)
312
+ if v != self.visible:
313
+ self.visible = v
314
+ if not self.visible:
315
+ self.position = None
316
+ self.modified()
317
+ else:
318
+ if not self.visible:
319
+ self.visible = True
320
+ self.modified()
321
+
322
+ def contains(self, pos):
323
+ """
324
+ Is this button hit by this position.
325
+
326
+ @param pos:
327
+ @return:
328
+ """
329
+ if self.position is None:
330
+ return False
331
+ x, y = pos
332
+ return (
333
+ self.position[0] < x < self.position[2]
334
+ and self.position[1] < y < self.position[3]
335
+ )
336
+
337
+ def click(self, event=None, recurse=True):
338
+ """
339
+ Process button click of button at provided button_id
340
+
341
+ @return:
342
+ """
343
+ if self.group:
344
+ # Toggle radio buttons
345
+ if self.state_pressed is None:
346
+ # Regular button
347
+ self.toggle = True
348
+ else:
349
+ # Real toggle button
350
+ self.toggle = not self.toggle
351
+ if self.toggle: # got toggled
352
+ button_group = self.parent.group_lookup.get(self.group, [])
353
+
354
+ for obutton in button_group:
355
+ # Untoggle all other buttons in this group.
356
+ if obutton.group == self.group and obutton.id != self.id:
357
+ obutton.set_button_toggle(False)
358
+ else: # got untoggled...
359
+ # so let's activate the first button of the group (implicitly defined as default...)
360
+ button_group = self.parent.group_lookup.get(self.group)
361
+ if button_group and recurse:
362
+ first_button = button_group[0]
363
+ first_button.set_button_toggle(True)
364
+ first_button.click(recurse=False)
365
+ return
366
+ if self.action is not None:
367
+ # We have an action to call.
368
+ self.action(None)
369
+
370
+ if self.state_pressed is None:
371
+ # Unless button has a pressed state we have finished.
372
+ return
373
+
374
+ # There is a pressed state which requires that we have a toggle.
375
+ self.toggle = not self.toggle
376
+ if self.toggle:
377
+ # Call the toggle_attr restore the pressed state.
378
+ if self.toggle_attr is not None:
379
+ setattr(self.object, self.toggle_attr, True)
380
+ self.context.signal(self.toggle_attr, True, self.object)
381
+ self._restore_button_aspect(self.state_pressed)
382
+ else:
383
+ # Call the toggle_attr restore the unpressed state.
384
+ if self.toggle_attr is not None:
385
+ setattr(self.object, self.toggle_attr, False)
386
+ self.context.signal(self.toggle_attr, False, self.object)
387
+ self._restore_button_aspect(self.state_unpressed)
388
+
389
+ def drop_click(self):
390
+ """
391
+ Drop down of a hybrid button was clicked.
392
+
393
+ We make a menu popup and fill it with the data about the multi-button
394
+ @return:
395
+ """
396
+ if self.toggle:
397
+ return
398
+ top = self.parent.parent.parent
399
+ menu = wx.Menu()
400
+ item = menu.Append(wx.ID_ANY, "...")
401
+ item.Enable(False)
402
+ for v in self.button_dict["multi"]:
403
+ item = menu.Append(wx.ID_ANY, v.get("label"))
404
+ tip = v.get("tip")
405
+ if tip:
406
+ item.SetHelp(tip)
407
+ if v.get("identifier") == self.identifier:
408
+ item.SetItemLabel(v.get("label") + "(*)")
409
+ icon = v.get("icon")
410
+ if icon:
411
+ # There seems to be a bug to display icons in a submenu consistently
412
+ # print (f"Had a bitmap for {v.get('label')}")
413
+ item.SetBitmap(icon.GetBitmap(resize=STD_ICON_SIZE / 2, buffer=2))
414
+ top.Bind(wx.EVT_MENU, self.drop_menu_click(v), id=item.GetId())
415
+ top.PopupMenu(menu)
416
+
417
+ def drop_menu_click(self, v):
418
+ """
419
+ Creates menu_item_click processors for the various menus created for a drop-click
420
+
421
+ @param v:
422
+ @return:
423
+ """
424
+
425
+ def menu_item_click(event):
426
+ """
427
+ Process menu item click.
428
+
429
+ @param event:
430
+ @return:
431
+ """
432
+ key_id = v.get("identifier")
433
+ try:
434
+ setattr(self.object, self.save_id, key_id)
435
+ except AttributeError:
436
+ pass
437
+ self.state_unpressed = key_id
438
+ self._restore_button_aspect(key_id)
439
+ # self.ensure_realize()
440
+ # And now execute it, provided it would be enabled...
441
+ auto_execute = self.context.setting(bool, "button_multi_menu_execute", True)
442
+ if auto_execute:
443
+ is_visible = True
444
+ is_enabled = True
445
+ if self.rule_visible:
446
+ try:
447
+ is_visible = self.rule_visible(0)
448
+ except (AttributeError, TypeError):
449
+ is_visible = False
450
+ if self.rule_enabled:
451
+ try:
452
+ is_enabled = self.rule_enabled(0)
453
+ except (AttributeError, TypeError):
454
+ is_enabled = False
455
+ if is_visible and is_enabled:
456
+ try:
457
+ self.action(None)
458
+ except AttributeError:
459
+ pass
460
+
461
+ return menu_item_click
462
+
463
+ def _setup_multi_button(self):
464
+ """
465
+ Store alternative aspects for multi-buttons, load stored previous state.
466
+
467
+ @return:
468
+ """
469
+ multi_aspects = self.button_dict["multi"]
470
+ # This is the key used for the multi button.
471
+ multi_ident = self.button_dict.get("identifier")
472
+ self.save_id = multi_ident
473
+ try:
474
+ self.object.setting(str, self.save_id, "default")
475
+ except AttributeError:
476
+ # This is not a context, we tried.
477
+ pass
478
+ initial_value = getattr(self.object, self.save_id, "default")
479
+ if "signal" in self.button_dict and "attr" in self.button_dict:
480
+ self._create_generic_signal_for_multi(self.object, self.button_dict.get("attr"), self.button_dict.get("signal"))
481
+
482
+ for i, v in enumerate(multi_aspects):
483
+ # These are values for the outer identifier
484
+ key = v.get("identifier", i)
485
+ self._store_button_aspect(key, **v)
486
+ if "signal" in v:
487
+ self._create_signal_for_multi(key, v["signal"])
488
+
489
+ if key == initial_value:
490
+ self._restore_button_aspect(key)
491
+
492
+ def _create_signal_for_multi(self, key, signal):
493
+ """
494
+ Creates a signal to restore the state of a multi button.
495
+
496
+ @param key:
497
+ @param signal:
498
+ @return:
499
+ """
500
+
501
+ def multi_click(origin, *args):
502
+ self._restore_button_aspect(key)
503
+
504
+ self.context.listen(signal, multi_click)
505
+ self.parent._registered_signals.append((signal, multi_click))
506
+
507
+ def _create_generic_signal_for_multi(self, q_object, q_attr, signal):
508
+ """
509
+ Creates a signal to restore the state of a multi button.
510
+
511
+ @param key:
512
+ @param signal:
513
+ @return:
514
+ """
515
+
516
+ def multi_click(origin, *args):
517
+ try:
518
+ key = getattr(q_object, q_attr)
519
+ except AttributeError:
520
+ return
521
+ self._restore_button_aspect(key)
522
+
523
+ self.context.listen(signal, multi_click)
524
+ self.parent._registered_signals.append((signal, multi_click))
525
+
526
+ def _setup_toggle_button(self):
527
+ """
528
+ Store toggle and original aspects for toggle-buttons
529
+
530
+ @param self:
531
+ @return:
532
+ """
533
+ resize_param = self.button_dict.get("size")
534
+
535
+ self.state_pressed = "toggle"
536
+ self.state_unpressed = "original"
537
+ self._store_button_aspect(self.state_unpressed)
538
+
539
+ toggle_button_dict = self.button_dict.get("toggle")
540
+ key = toggle_button_dict.get("identifier", self.state_pressed)
541
+ if "signal" in toggle_button_dict:
542
+ self._create_signal_for_toggle(toggle_button_dict.get("signal"))
543
+ self._store_button_aspect(key, **toggle_button_dict)
544
+
545
+ # Set initial value by identifer and object
546
+ if self.toggle_attr is not None and getattr(
547
+ self.object, self.toggle_attr, False
548
+ ):
549
+ self.set_button_toggle(True)
550
+ self.modified()
551
+
552
+ def _create_signal_for_toggle(self, signal):
553
+ """
554
+ Creates a signal toggle which will listen for the given signal and set the toggle-state to the given set_value
555
+
556
+ E.G. If a toggle has a signal called "tracing" and the context.signal("tracing", True) is called this will
557
+ automatically set the toggle state.
558
+
559
+ Note: It will not call any of the associated actions, it will simply set the toggle state.
560
+
561
+ @param signal:
562
+ @return:
563
+ """
564
+
565
+ def toggle_click(origin, *args):
566
+ set_value = getattr(self.object, self.toggle_attr) if self.toggle_attr else not self.toggle
567
+ self.set_button_toggle(set_value)
568
+
569
+ self.context.listen(signal, toggle_click)
570
+ self.parent._registered_signals.append((signal, toggle_click))
571
+
572
+ def set_button_toggle(self, toggle_state):
573
+ """
574
+ Set the button's toggle state to the given toggle_state
575
+
576
+ @param toggle_state:
577
+ @return:
578
+ """
579
+ self.toggle = toggle_state
580
+ if toggle_state:
581
+ self._restore_button_aspect(self.state_pressed)
582
+ else:
583
+ self._restore_button_aspect(self.state_unpressed)
584
+
585
+ def modified(self):
586
+ """
587
+ This button was modified and should be redrawn.
588
+ @return:
589
+ """
590
+ self.parent.modified()
591
+
592
+
593
+ class RibbonPanel:
594
+ """
595
+ Ribbon Panel is a panel of buttons within the page.
596
+ """
597
+
598
+ def __init__(self, context, parent, id, label, icon):
599
+ self.context = context
600
+ self.parent = parent
601
+ self.id = id
602
+ self.label = label
603
+ self.icon = icon
604
+
605
+ self._registered_signals = list()
606
+ self.button_lookup = {}
607
+ self.group_lookup = {}
608
+
609
+ self.buttons = []
610
+ self.position = None
611
+ self.available_position = None
612
+ self._overflow = list()
613
+ self._overflow_position = None
614
+
615
+ def visible_buttons(self):
616
+ for button in self.buttons:
617
+ if button.visible:
618
+ yield button
619
+
620
+ @property
621
+ def visible_button_count(self):
622
+ pcount = 0
623
+ for button in self.buttons:
624
+ if button is not None and button.visible:
625
+ pcount += 1
626
+ return pcount
627
+
628
+ def clear_buttons(self):
629
+ self.buttons.clear()
630
+ self.parent.modified()
631
+ self._overflow = list()
632
+ self._overflow_position = None
633
+
634
+ def set_buttons(self, new_values):
635
+ """
636
+ Set buttons is the primary button configuration routine. It is responsible for clearing and recreating buttons.
637
+
638
+ * The button definition is a dynamically created and stored dictionary.
639
+ * Buttons are sorted by priority.
640
+ * Multi buttons get a hybrid type.
641
+ * Toggle buttons get a toggle type (Unless they are also multi).
642
+ * Created button objects have attributes assigned to them.
643
+ * toggle, parent, group, identifier, toggle_identifier, action, right, rule_enabled
644
+ * Multi-buttons have an identifier attr which is applied to the root context, or given "object".
645
+ * The identifier is used to set the state of the object, the attr-identifier is set to the value-identifier
646
+ * Toggle buttons have a toggle_identifier, this is used to set and retrieve the state of the toggle.
647
+
648
+
649
+ @param new_values: dictionary of button values to use.
650
+ @return:
651
+ """
652
+ # print (f"Setbuttons called for {self.label}")
653
+ self.modified()
654
+ self.clear_buttons()
655
+ button_descriptions = []
656
+ for desc, name, sname in new_values:
657
+ button_descriptions.append(desc)
658
+
659
+ # Sort buttons by priority
660
+ def sort_priority(elem):
661
+ return elem.get("priority", 0)
662
+
663
+ button_descriptions.sort(key=sort_priority)
664
+
665
+ for desc in button_descriptions:
666
+ # Every registered button in the updated lookup gets created.
667
+ b = self._create_button(desc)
668
+
669
+ # Store newly created button in the various lookups
670
+ self.button_lookup[b.id] = b
671
+ group = desc.get("group")
672
+ if group is not None:
673
+ c_group = self.group_lookup.get(group)
674
+ if c_group is None:
675
+ c_group = []
676
+ self.group_lookup[group] = c_group
677
+ c_group.append(b)
678
+
679
+ def _create_button(self, desc):
680
+ """
681
+ Creates a button and places it on the button_bar depending on the required definition.
682
+
683
+ @param desc:
684
+ @return:
685
+ """
686
+ show_tip = not self.context.disable_tool_tips
687
+ # NewIdRef is only available after 4.1
688
+ try:
689
+ new_id = wx.NewIdRef()
690
+ except AttributeError:
691
+ new_id = wx.NewId()
692
+
693
+ # Create kind of button. Multi buttons are hybrid. Else, regular button or toggle-type
694
+ if "multi" in desc:
695
+ # Button is a multi-type button
696
+ b = Button(
697
+ self.context, self, button_id=new_id, kind="hybrid", description=desc
698
+ )
699
+ self.buttons.append(b)
700
+ b._setup_multi_button()
701
+ else:
702
+ bkind = "normal"
703
+ if "group" in desc or "toggle" in desc:
704
+ bkind = "toggle"
705
+ b = Button(
706
+ self.context, self, button_id=new_id, kind=bkind, description=desc
707
+ )
708
+ self.buttons.append(b)
709
+
710
+ if "toggle" in desc:
711
+ b._setup_toggle_button()
712
+ return b
713
+
714
+ def contains(self, pos):
715
+ """
716
+ Does the given position hit the current panel.
717
+
718
+ @param pos:
719
+ @return:
720
+ """
721
+ if self.position is None:
722
+ return False
723
+ x, y = pos
724
+ return (
725
+ self.position[0] < x < self.position[2]
726
+ and self.position[1] < y < self.position[3]
727
+ )
728
+
729
+ def modified(self):
730
+ """
731
+ Modified call parent page.
732
+ @return:
733
+ """
734
+ self.parent.modified()
735
+
736
+ def overflow_click(self):
737
+ """
738
+ Click of overflow. Overflow exists if some icons are not able to be shown.
739
+
740
+ We make a menu popup and fill it with the overflow commands.
741
+
742
+ @return:
743
+ """
744
+ # print (f"Overflow click called for {self.label}")
745
+ menu = wx.Menu()
746
+ top = self.parent.parent # .parent
747
+ for v in self._overflow:
748
+ item = menu.Append(wx.ID_ANY, v.label)
749
+ item.Enable(v.enabled)
750
+ if callable(v.tip):
751
+ item.SetHelp(v.tip())
752
+ else:
753
+ item.SetHelp(v.tip)
754
+ if v.icon:
755
+ item.SetBitmap(v.icon.GetBitmap(resize=STD_ICON_SIZE / 2, buffer=2))
756
+ top.Bind(wx.EVT_MENU, v.click, id=item.Id)
757
+ top.PopupMenu(menu)
758
+
759
+
760
+ class RibbonPage:
761
+ """
762
+ Ribbon Page is a page of buttons this is the series of ribbon panels as triggered by the different tags.
763
+ """
764
+
765
+ def __init__(self, context, parent, id, label, icon, reference):
766
+ self.context = context
767
+ self.parent = parent
768
+ self.id = id
769
+ self.label = label
770
+ self.icon = icon
771
+ self.panels = []
772
+ self.position = None
773
+ self.tab_position = None
774
+ self.visible = True
775
+ self.reference = reference
776
+
777
+ def add_panel(self, panel, ref):
778
+ """
779
+ Adds a panel to this page.
780
+ @param panel:
781
+ @param ref:
782
+ @return:
783
+ """
784
+ self.panels.append(panel)
785
+ if ref is not None:
786
+ # print(f"Setattr in add_panel: {ref} = {panel}")
787
+ setattr(self, ref, panel)
788
+
789
+ def contains(self, pos):
790
+ """
791
+ Does this position hit the tab position of this page.
792
+ @param pos:
793
+ @return:
794
+ """
795
+ if self.tab_position is None:
796
+ return False
797
+ x, y = pos
798
+ return (
799
+ self.tab_position[0] < x < self.tab_position[2]
800
+ and self.tab_position[1] < y < self.tab_position[3]
801
+ )
802
+
803
+ def modified(self):
804
+ """
805
+ Call modified to parent RibbonBarPanel.
806
+
807
+ @return:
808
+ """
809
+ self.parent.modified()
810
+
811
+
812
+ class RibbonBarPanel(wx.Control):
813
+ def __init__(self, parent, id, context=None, pane=None, **kwds):
814
+ super().__init__(parent, id, **kwds)
815
+ self.context = context
816
+ self.pages = []
817
+ self.pane = pane
818
+ jobname = f"realize_ribbon_bar_{self.GetId()}"
819
+ # print (f"Requesting job with name: '{jobname}'")
820
+ self._redraw_job = Job(
821
+ process=self._paint_main_on_buffer,
822
+ job_name=jobname,
823
+ interval=0.1,
824
+ times=1,
825
+ run_main=True,
826
+ )
827
+ # Layout properties.
828
+ self.art = Art(self)
829
+
830
+ # Define Ribbon.
831
+ self._redraw_lock = threading.Lock()
832
+ self._paint_dirty = True
833
+ self._layout_dirty = True
834
+ self._ribbon_buffer = None
835
+
836
+ # self._overflow = list()
837
+ # self._overflow_position = None
838
+
839
+ self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
840
+ self.Bind(wx.EVT_ERASE_BACKGROUND, self.on_erase_background)
841
+ self.Bind(wx.EVT_ENTER_WINDOW, self.on_mouse_enter)
842
+ self.Bind(wx.EVT_LEAVE_WINDOW, self.on_mouse_leave)
843
+ self.Bind(wx.EVT_MOTION, self.on_mouse_move)
844
+ self.Bind(wx.EVT_PAINT, self.on_paint)
845
+ self.Bind(wx.EVT_SIZE, self.on_size)
846
+
847
+ self.Bind(wx.EVT_LEFT_DOWN, self.on_click)
848
+ self.Bind(wx.EVT_RIGHT_UP, self.on_click_right)
849
+
850
+ # Tooltip logic - as we do have a single control,
851
+ # this will prevent wxPython from resetting the timer
852
+ # when hovering to a different button
853
+ self._tooltip = ""
854
+ jobname = f"tooltip_ribbon_bar_{self.GetId()}"
855
+ # print (f"Requesting job with name: '{jobname}'")
856
+ tooltip_delay = self.context.setting(int, "tooltip_delay", 100)
857
+ interval = tooltip_delay / 1000.0
858
+ self._tooltip_job = Job(
859
+ process=self._exec_tooltip_job,
860
+ job_name=jobname,
861
+ interval=interval,
862
+ times=1,
863
+ run_main=True,
864
+ )
865
+
866
+ # Preparation for individual page visibility
867
+ def visible_pages(self):
868
+ count = 0
869
+ for p in self.pages:
870
+ if p.visible:
871
+ count += 1
872
+ return count
873
+
874
+ def first_page(self):
875
+ # returns the first visible page
876
+ for p in self.pages:
877
+ if p.visible:
878
+ return p
879
+ return None
880
+
881
+ def modified(self):
882
+ """
883
+ if modified then we flag the layout and paint as dirty and call for a refresh of the ribbonbar.
884
+ @return:
885
+ """
886
+ # (f"Modified called for RibbonBar with {self.visible_pages()} pages")
887
+ self._paint_dirty = True
888
+ self._layout_dirty = True
889
+ self.context.schedule(self._redraw_job)
890
+
891
+ def redrawn(self):
892
+ """
893
+ if refresh needed then we flag the paint as dirty and call for a refresh of the ribbonbar.
894
+ @return:
895
+ """
896
+ self._paint_dirty = True
897
+ self.context.schedule(self._redraw_job)
898
+
899
+ def on_size(self, event: wx.SizeEvent):
900
+ self._set_buffer()
901
+ self.modified()
902
+
903
+ def on_erase_background(self, event):
904
+ pass
905
+
906
+ def on_mouse_enter(self, event: wx.MouseEvent):
907
+ pass
908
+
909
+ def on_mouse_leave(self, event: wx.MouseEvent):
910
+ self.art.hover_tab = None
911
+ self.art.hover_button = None
912
+ self.art.hover_dropdown = None
913
+ self.redrawn()
914
+
915
+ def stop_tooltip_job(self):
916
+ self._tooltip_job.cancel()
917
+
918
+ def start_tooltip_job(self):
919
+ # print (f"Schedule a job with {self._tooltip_job.interval:.2f}sec")
920
+ self.context.schedule(self._tooltip_job)
921
+
922
+ def _exec_tooltip_job(self):
923
+ # print (f"Executed with {self._tooltip}")
924
+ try:
925
+ super().SetToolTip(self._tooltip)
926
+ except RuntimeError:
927
+ # Could happen on a shutdown...
928
+ return
929
+
930
+ def SetToolTip(self, message):
931
+ if callable(message):
932
+ self._tooltip = message()
933
+ else:
934
+ self._tooltip = message
935
+ if message == "":
936
+ self.stop_tooltip_job()
937
+ super().SetToolTip(message)
938
+ else:
939
+ # we restart the job and delete the tooltip in the meantime
940
+ super().SetToolTip("")
941
+ self.start_tooltip_job()
942
+
943
+ def _check_hover_dropdown(self, drop, pos):
944
+ if drop is not None and not drop.contains(pos):
945
+ drop = None
946
+ if drop is not self.art.hover_dropdown:
947
+ self.art.hover_dropdown = drop
948
+ self.redrawn()
949
+
950
+ def _check_hover_button(self, pos):
951
+ hover = self._overflow_at_position(pos)
952
+ if hover is not None:
953
+ self.SetToolTip(_("There is more to see - click to display"))
954
+ self.SetHelpText("")
955
+ return
956
+ hover = self._button_at_position(pos)
957
+ if hover is not None:
958
+ self._check_hover_dropdown(hover.dropdown, pos)
959
+ if hover is not None and hover is self.art.hover_button:
960
+ return
961
+ self.art.hover_button = hover
962
+ if hover is None:
963
+ hover = self._button_at_position(pos, use_all=True)
964
+ if hover is not None:
965
+ self.SetToolTip(hover.tip)
966
+ hhelp = hover.help
967
+ if hhelp is None:
968
+ hhelp = ""
969
+ self.SetHelpText(hhelp)
970
+ else:
971
+ self.SetToolTip("")
972
+ self.SetHelpText("")
973
+
974
+ self.redrawn()
975
+
976
+ def _check_hover_tab(self, pos):
977
+ hover = self._pagetab_at_position(pos)
978
+ if hover is not self.art.hover_tab:
979
+ self.art.hover_tab = hover
980
+ self.redrawn()
981
+
982
+ def on_mouse_move(self, event: wx.MouseEvent):
983
+ pos = event.Position
984
+ self._check_hover_button(pos)
985
+ self._check_hover_tab(pos)
986
+
987
+ def on_paint(self, event: wx.PaintEvent):
988
+ """
989
+ Ribbonbar paint event calls the paints the bitmap self._ribbon_buffer. If self._ribbon_buffer does not exist
990
+ initially it is created in the self.scene.update_buffer_ui_thread() call.
991
+ """
992
+ if self._paint_dirty:
993
+ self._paint_main_on_buffer()
994
+
995
+ try:
996
+ wx.BufferedPaintDC(self, self._ribbon_buffer)
997
+ except (RuntimeError, AssertionError, TypeError):
998
+ pass
999
+
1000
+ def _paint_main_on_buffer(self):
1001
+ """Performs redrawing of the data in the UI thread."""
1002
+ # print (f"Redraw job started for RibbonBar with {self.visible_pages()} pages")
1003
+ if self._redraw_lock.acquire(timeout=0.2):
1004
+ try:
1005
+ buf = self._set_buffer()
1006
+ dc = wx.MemoryDC()
1007
+ dc.SelectObject(buf)
1008
+ if self._layout_dirty:
1009
+ self.art.layout(dc, self)
1010
+ self._layout_dirty = False
1011
+ self.art.paint_main(dc, self)
1012
+ dc.SelectObject(wx.NullBitmap)
1013
+ del dc
1014
+ self._paint_dirty = False
1015
+ except (RuntimeError, AssertionError):
1016
+ pass
1017
+ # Shutdown error
1018
+ finally:
1019
+ self._redraw_lock.release()
1020
+ try:
1021
+ self.Refresh() # Paint buffer on screen.
1022
+ except RuntimeError:
1023
+ # Shutdown error
1024
+ pass
1025
+
1026
+ def prefer_horizontal(self):
1027
+ result = None
1028
+ if self.pane is not None:
1029
+ try:
1030
+ pane = self.pane.manager.GetPane(self.pane.name)
1031
+ if pane.IsDocked():
1032
+ # if self.pane.name == "tools":
1033
+ # print (
1034
+ # f"Pane: {pane.name}: {pane.dock_direction}, State: {pane.IsOk()}/{pane.IsDocked()}/{pane.IsFloating()}"
1035
+ # )
1036
+ if pane.dock_direction in (1, 3):
1037
+ # Horizontal
1038
+ result = True
1039
+ elif pane.dock_direction in (2, 4):
1040
+ # Vertical
1041
+ result = False
1042
+ # else:
1043
+ # if self.pane.name == "tools":
1044
+ # print (
1045
+ # f"Pane: {pane.name}: {pane.IsFloating()}"
1046
+ # )
1047
+ except (AttributeError, RuntimeError):
1048
+ # Unknown error occurred
1049
+ pass
1050
+
1051
+ if result is None:
1052
+ # Floating...
1053
+ width, height = self.ClientSize
1054
+ if width <= 0:
1055
+ width = 1
1056
+ if height <= 0:
1057
+ height = 1
1058
+ result = bool(width >= height)
1059
+
1060
+ return result
1061
+
1062
+ def _set_buffer(self):
1063
+ """
1064
+ Set the value for the self._Buffer bitmap equal to the panel's clientSize.
1065
+ """
1066
+ if (
1067
+ self._ribbon_buffer is None
1068
+ or self._ribbon_buffer.GetSize() != self.ClientSize
1069
+ or not self._ribbon_buffer.IsOk()
1070
+ ):
1071
+ width, height = self.ClientSize
1072
+ if width <= 0:
1073
+ width = 1
1074
+ if height <= 0:
1075
+ height = 1
1076
+ self._ribbon_buffer = wx.Bitmap(width, height)
1077
+ return self._ribbon_buffer
1078
+
1079
+ def toggle_show_labels(self, v):
1080
+ self.art.show_labels = v
1081
+ self.modified()
1082
+
1083
+ def _overflow_at_position(self, pos):
1084
+ for page in self.pages:
1085
+ if page is not self.art.current_page or not page.visible:
1086
+ continue
1087
+ for panel in page.panels:
1088
+ x, y = pos
1089
+ # print (f"Checking: {panel.label}: ({x},{y}) in ({panel._overflow_position})")
1090
+ if panel._overflow_position is None:
1091
+ continue
1092
+ if (
1093
+ panel._overflow_position[0] < x < panel._overflow_position[2]
1094
+ and panel._overflow_position[1] < y < panel._overflow_position[3]
1095
+ ):
1096
+ # print (f"Found a panel: {panel.label}")
1097
+ return panel
1098
+ return None
1099
+
1100
+ def _button_at_position(self, pos, use_all=False):
1101
+ """
1102
+ Find the button at the given position, so long as that button is enabled.
1103
+
1104
+ @param pos:
1105
+ @return:
1106
+ """
1107
+ for page in self.pages:
1108
+ if page is not self.art.current_page or not page.visible:
1109
+ continue
1110
+ for panel in page.panels:
1111
+ for button in panel.visible_buttons():
1112
+ if (
1113
+ button.contains(pos)
1114
+ and (button.enabled or use_all)
1115
+ and not button.overflow
1116
+ ):
1117
+ return button
1118
+ return None
1119
+
1120
+ def _pagetab_at_position(self, pos):
1121
+ """
1122
+ Find the page tab at the given position.
1123
+
1124
+ @param pos:
1125
+ @return:
1126
+ """
1127
+ for page in self.pages:
1128
+ if page.visible and page.contains(pos):
1129
+ return page
1130
+ return None
1131
+
1132
+ def on_click_right(self, event: wx.MouseEvent):
1133
+ """
1134
+ Handles the ``wx.EVT_RIGHT_DOWN`` event
1135
+ :param event: a :class:`MouseEvent` event to be processed.
1136
+ """
1137
+ pos = event.Position
1138
+ button = self._button_at_position(pos)
1139
+ if button is not None:
1140
+ action = button.action_right
1141
+ if action:
1142
+ action(event)
1143
+ else:
1144
+ # Click on background, off menu to edit and set colors
1145
+ def set_color(newmode):
1146
+ self.context.root.ribbon_color = newmode
1147
+ # Force refresh
1148
+ self.context.signal("ribbon_recreate", None)
1149
+
1150
+ top = self # .parent
1151
+ c_mode = self.context.root.setting(int, "ribbon_color", COLOR_MODE_DEFAULT)
1152
+ menu = wx.Menu()
1153
+ item = menu.Append(wx.ID_ANY, _("Colorscheme"))
1154
+ item.Enable(False)
1155
+ item = menu.Append(wx.ID_ANY, _("System Default"), "", wx.ITEM_CHECK)
1156
+ item.Check(bool(c_mode == COLOR_MODE_DEFAULT))
1157
+ top.Bind(
1158
+ wx.EVT_MENU, lambda v: set_color(COLOR_MODE_DEFAULT), id=item.GetId()
1159
+ )
1160
+ item = menu.Append(wx.ID_ANY, _("Colored"), "", wx.ITEM_CHECK)
1161
+ item.Check(bool(c_mode == COLOR_MODE_COLOR))
1162
+ top.Bind(
1163
+ wx.EVT_MENU, lambda v: set_color(COLOR_MODE_COLOR), id=item.GetId()
1164
+ )
1165
+ item = menu.Append(wx.ID_ANY, _("Black"), "", wx.ITEM_CHECK)
1166
+ item.Check(bool(c_mode == COLOR_MODE_DARK))
1167
+ top.Bind(wx.EVT_MENU, lambda v: set_color(COLOR_MODE_DARK), id=item.GetId())
1168
+ item = menu.AppendSeparator()
1169
+ haslabel = self.art.show_labels
1170
+ item = menu.Append(wx.ID_ANY, _("Show Labels"), "", wx.ITEM_CHECK)
1171
+ if not getattr(self, "allow_labels", True):
1172
+ item.Enable(False)
1173
+ item.Check(haslabel)
1174
+ top.Bind(
1175
+ wx.EVT_MENU,
1176
+ lambda v: self.toggle_show_labels(not haslabel),
1177
+ id=item.GetId(),
1178
+ )
1179
+ item = menu.AppendSeparator()
1180
+ item = menu.Append(wx.ID_ANY, _("Customize Toolbars"))
1181
+
1182
+ def show_pref():
1183
+ self.context("window open Preferences\n")
1184
+ self.context.signal("preferences", "ribbon")
1185
+
1186
+ top.Bind(
1187
+ wx.EVT_MENU,
1188
+ lambda v: show_pref(),
1189
+ id=item.GetId(),
1190
+ )
1191
+ top.PopupMenu(menu)
1192
+
1193
+ def on_click(self, event: wx.MouseEvent):
1194
+ """
1195
+ The ribbon bar was clicked. We check the various parts of the ribbonbar that could have been clicked in the
1196
+ preferred click order. Overflow, pagetab, drop-down, button.
1197
+ @param event:
1198
+ @return:
1199
+ """
1200
+ pos = event.Position
1201
+
1202
+ page = self._pagetab_at_position(pos)
1203
+ overflow = self._overflow_at_position(pos)
1204
+ if overflow is not None:
1205
+ overflow.overflow_click()
1206
+ self.modified()
1207
+ return
1208
+
1209
+ button = self._button_at_position(pos)
1210
+ if page is not None and button is None:
1211
+ self.art.current_page = page
1212
+ self.apply_enable_rules()
1213
+ self.modified()
1214
+ return
1215
+ if button is None:
1216
+ return
1217
+ drop = button.dropdown
1218
+ if drop is not None and drop.contains(pos):
1219
+ button.drop_click()
1220
+ self.modified()
1221
+ return
1222
+ button.click()
1223
+ self.modified()
1224
+
1225
+ def _all_buttons(self):
1226
+ """
1227
+ Helper to cycle through all buttons in the panels that are currently visible.
1228
+ @return:
1229
+ """
1230
+ for page in self.pages:
1231
+ if page is not self.art.current_page or not page.visible:
1232
+ continue
1233
+ for panel in page.panels:
1234
+ for button in panel.buttons:
1235
+ yield button
1236
+
1237
+ def apply_enable_rules(self):
1238
+ """
1239
+ Applies all enable rules for all buttons that are currently seen.
1240
+ @return:
1241
+ """
1242
+ for button in self._all_buttons():
1243
+ button.apply_enable_rules()
1244
+
1245
+ def add_page(self, ref, id, label, icon):
1246
+ """
1247
+ Add a page to the ribbonbar.
1248
+ @param ref:
1249
+ @param id:
1250
+ @param label:
1251
+ @param icon:
1252
+ @return:
1253
+ """
1254
+ page = RibbonPage(
1255
+ self.context,
1256
+ self,
1257
+ id,
1258
+ label,
1259
+ icon,
1260
+ ref,
1261
+ )
1262
+ if ref is not None:
1263
+ # print(f"Setattr in add_page: {ref} = {page}")
1264
+ setattr(self, ref, page)
1265
+ if self.art.current_page is None:
1266
+ self.art.current_page = page
1267
+ self.pages.append(page)
1268
+ self._layout_dirty = True
1269
+ return page
1270
+
1271
+ def remove_page(self, pageid):
1272
+ """
1273
+ Remove a page from the ribbonbar.
1274
+ @param pageid:
1275
+ @return:
1276
+ """
1277
+ for pidx, page in enumerate(self.pages):
1278
+ if page.id == pageid:
1279
+ if self.art.current_page is page:
1280
+ self.art.current_page = None
1281
+ for panel in page.panels:
1282
+ panel.clear_buttons()
1283
+ del panel
1284
+ self.pages.pop(pidx)
1285
+ break
1286
+
1287
+ self._layout_dirty = True
1288
+
1289
+ def validate_current_page(self):
1290
+ if self.art.current_page is None or not self.art.current_page.visible:
1291
+ self.art.current_page = self.first_page()
1292
+
1293
+ def add_panel(self, ref, parent: RibbonPage, id, label, icon):
1294
+ """
1295
+ Add a panel to the ribbon bar. Parent must be a page.
1296
+ @param ref:
1297
+ @param parent:
1298
+ @param id:
1299
+ @param label:
1300
+ @param icon:
1301
+ @return:
1302
+ """
1303
+ panel = RibbonPanel(
1304
+ self.context,
1305
+ parent=parent,
1306
+ id=id,
1307
+ label=label,
1308
+ icon=icon,
1309
+ )
1310
+ parent.add_panel(panel, ref)
1311
+ self._layout_dirty = True
1312
+ return panel
1313
+
1314
+
1315
+ class Art:
1316
+ def __init__(self, parent):
1317
+ self.RIBBON_ORIENTATION_AUTO = 0
1318
+ self.RIBBON_ORIENTATION_HORIZONTAL = 1
1319
+ self.RIBBON_ORIENTATION_VERTICAL = 2
1320
+ self.orientation = self.RIBBON_ORIENTATION_AUTO
1321
+ self.parent = parent
1322
+ self.between_button_buffer = 3
1323
+ self.panel_button_buffer = 3
1324
+ self.page_panel_buffer = 3
1325
+ self.between_panel_buffer = 5
1326
+
1327
+ self.tab_width = 70
1328
+ self.tab_height = 20
1329
+ self.tab_tab_buffer = 10
1330
+ self.tab_initial_buffer = 30
1331
+ self.tab_text_buffer = 2
1332
+ self.edge_page_buffer = 4
1333
+ self.rounded_radius = 3
1334
+ self.font_sizes = {}
1335
+
1336
+ self.bitmap_text_buffer = 5
1337
+ self.dropdown_height = 20
1338
+ self.overflow_width = 20
1339
+ self.text_dropdown_buffer = 7
1340
+ self.show_labels = True
1341
+
1342
+ self.establish_colors()
1343
+
1344
+ self.current_page = None
1345
+ self.hover_tab = None
1346
+ self.hover_button = None
1347
+ self.hover_dropdown = None
1348
+
1349
+ def establish_colors(self):
1350
+ self.text_color = copy.copy(
1351
+ wx.SystemSettings().GetColour(wx.SYS_COLOUR_BTNTEXT)
1352
+ )
1353
+ self.text_color_inactive = copy.copy(self.text_color).ChangeLightness(50)
1354
+ self.text_color_disabled = wx.Colour("Dark Grey")
1355
+ self.black_color = copy.copy(
1356
+ wx.SystemSettings().GetColour(wx.SYS_COLOUR_BTNTEXT)
1357
+ )
1358
+
1359
+ self.button_face_hover = copy.copy(
1360
+ wx.SystemSettings().GetColour(wx.SYS_COLOUR_HIGHLIGHT)
1361
+ ).ChangeLightness(150)
1362
+ # self.button_face_hover = copy.copy(
1363
+ # wx.SystemSettings().GetColour(wx.SYS_COLOUR_GRADIENTACTIVECAPTION)
1364
+ # )
1365
+ self.inactive_background = copy.copy(
1366
+ wx.SystemSettings().GetColour(wx.SYS_COLOUR_INACTIVECAPTION)
1367
+ )
1368
+ self.inactive_text = copy.copy(
1369
+ wx.SystemSettings().GetColour(wx.SYS_COLOUR_GRAYTEXT)
1370
+ )
1371
+ self.tooltip_foreground = copy.copy(
1372
+ wx.SystemSettings().GetColour(wx.SYS_COLOUR_INFOTEXT)
1373
+ )
1374
+ self.tooltip_background = copy.copy(
1375
+ wx.SystemSettings().GetColour(wx.SYS_COLOUR_INFOBK)
1376
+ )
1377
+ self.button_face = copy.copy(
1378
+ wx.SystemSettings().GetColour(wx.SYS_COLOUR_BTNHILIGHT)
1379
+ )
1380
+ self.ribbon_background = copy.copy(
1381
+ wx.SystemSettings().GetColour(wx.SYS_COLOUR_BTNHILIGHT)
1382
+ )
1383
+ self.highlight = copy.copy(
1384
+ wx.SystemSettings().GetColour(wx.SYS_COLOUR_HOTLIGHT)
1385
+ )
1386
+
1387
+ # Do we have a setting for the color?
1388
+ c_mode = self.parent.context.root.setting(int, "ribbon_color", 0)
1389
+ # 0 system default
1390
+ # 1 colored background
1391
+ # 2 forced dark_mode
1392
+ if c_mode < COLOR_MODE_DEFAULT or c_mode > COLOR_MODE_DARK:
1393
+ c_mode = COLOR_MODE_DEFAULT
1394
+ if (
1395
+ c_mode == COLOR_MODE_DEFAULT
1396
+ and self.parent.context.themes.dark # wx.SystemSettings().GetColour(wx.SYS_COLOUR_WINDOW)[0] < 127
1397
+ ):
1398
+ c_mode = COLOR_MODE_DARK # dark mode
1399
+ self.color_mode = c_mode
1400
+
1401
+ if self.color_mode == COLOR_MODE_DARK:
1402
+ # This is rather crude, as a dark mode could also
1403
+ # be based e.g. on a dark blue scheme
1404
+ self.button_face = wx.BLACK
1405
+ self.ribbon_background = wx.BLACK
1406
+ self.text_color = wx.WHITE
1407
+ self.text_color_inactive = copy.copy(self.text_color)
1408
+ self.text_color_disabled = wx.Colour("Light Grey")
1409
+ self.black_color = wx.WHITE
1410
+ self.inactive_background = wx.BLACK
1411
+ OS_NAME = platform.system()
1412
+ if OS_NAME == "Windows":
1413
+ self.button_face_hover = wx.BLUE
1414
+ if self.color_mode == COLOR_MODE_COLOR:
1415
+ self.ribbon_background = copy.copy(
1416
+ wx.SystemSettings().GetColour(wx.SYS_COLOUR_GRADIENTINACTIVECAPTION)
1417
+ )
1418
+ self.button_face = copy.copy(
1419
+ wx.SystemSettings().GetColour(wx.SYS_COLOUR_GRADIENTACTIVECAPTION)
1420
+ )
1421
+ self.button_face_hover = wx.Colour("gold").ChangeLightness(150)
1422
+ self.highlight = wx.Colour("gold")
1423
+
1424
+ # Let's adjust the fontsize for the page headers
1425
+ screen_wd, screen_ht = wx.GetDisplaySize()
1426
+ ptdefault = 10
1427
+ if screen_wd <= 800 or screen_ht <= 600:
1428
+ ptdefault = 8
1429
+ self.tab_height = 16
1430
+ try:
1431
+ wxsize = wx.Size(ptdefault, ptdefault)
1432
+ dipsize = self.parent.FromDIP(wxsize)
1433
+ ptsize = int(wxsize[0] + 0.5 * (dipsize[0] - wxsize[0]))
1434
+ # print(ptdefault, wxsize[0], ptsize, dipsize[0])
1435
+ except AttributeError:
1436
+ ptsize = ptdefault
1437
+ self.default_font = wx.Font(
1438
+ ptsize, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL
1439
+ )
1440
+
1441
+ def paint_main(self, dc, ribbon):
1442
+ """
1443
+ Main paint routine. This should delegate, in paint order, to the things on screen that require painting.
1444
+ @return:
1445
+ """
1446
+ self._paint_background(dc)
1447
+ self.parent.validate_current_page()
1448
+
1449
+ if ribbon.visible_pages() > 1:
1450
+ for page in ribbon.pages:
1451
+ if page.visible:
1452
+ self._paint_tab(dc, page)
1453
+ else:
1454
+ self.current_page = ribbon.first_page()
1455
+ if self.current_page is not None:
1456
+ if self.current_page.position is None:
1457
+ # print("Was dirty...")
1458
+ self.layout(dc, self.parent)
1459
+
1460
+ for page in ribbon.pages:
1461
+ if page is not self.current_page or not page.visible:
1462
+ continue
1463
+
1464
+ dc.SetBrush(wx.Brush(self.ribbon_background))
1465
+ x, y, x1, y1 = page.position
1466
+ dc.DrawRoundedRectangle(
1467
+ int(x), int(y), int(x1 - x), int(y1 - y), self.rounded_radius
1468
+ )
1469
+ self.look_at_button_font_sizes(dc, page)
1470
+ for panel in page.panels:
1471
+ # We suppress empty panels
1472
+ if panel is None or panel.visible_button_count == 0:
1473
+ continue
1474
+ self._paint_panel(dc, panel)
1475
+ for button in panel.visible_buttons():
1476
+ self._paint_button(dc, button)
1477
+
1478
+ def look_at_button_font_sizes(self, dc, page):
1479
+ self.font_sizes = {}
1480
+ for panel in page.panels:
1481
+ # We suppress empty panels
1482
+ if panel is None or panel.visible_button_count == 0:
1483
+ continue
1484
+ for button in panel.visible_buttons():
1485
+ x, y, x1, y1 = button.position
1486
+ start_y = y
1487
+ w = int(round(x1 - x, 2))
1488
+ h = int(round(y1 - y, 2))
1489
+ img_h = h
1490
+ # do we have text? if yes let's reduce the available space in y
1491
+ if self.show_labels: # Regardless whether we have a label or not...
1492
+ img_h -= self.bitmap_text_buffer
1493
+ ptsize = min(18, int(round(min(w, img_h) / 5.0, 2)) * 2)
1494
+ img_h -= int(ptsize * 1.35)
1495
+
1496
+ button.get_bitmaps(min(w, img_h))
1497
+ if button.enabled:
1498
+ bitmap = button.bitmap
1499
+ else:
1500
+ bitmap = button.bitmap_disabled
1501
+
1502
+ bitmap_width, bitmap_height = bitmap.Size
1503
+ # if button.label in ("Circle", "Ellipse", "Wordlist", "Property Window"):
1504
+ # print (f"N - {button.label}: {bitmap_width}x{bitmap_height} in {w}x{h}")
1505
+ bs = min(bitmap_width, bitmap_height)
1506
+ ptsize = self.get_font_size(bs)
1507
+ y += bitmap_height
1508
+
1509
+ text_edge = self.bitmap_text_buffer
1510
+ if button.label and self.show_labels:
1511
+ show_text = True
1512
+ label_text = list(button.label.split(" "))
1513
+ # We try to establish whether this would fit properly.
1514
+ # We allow a small oversize of 25% to the button,
1515
+ # before we try to reduce the fontsize
1516
+ wouldfit = False
1517
+ while not wouldfit:
1518
+ total_text_height = 0
1519
+ testfont = wx.Font(
1520
+ ptsize,
1521
+ wx.FONTFAMILY_SWISS,
1522
+ wx.FONTSTYLE_NORMAL,
1523
+ wx.FONTWEIGHT_NORMAL,
1524
+ )
1525
+ test_y = y + text_edge
1526
+ dc.SetFont(testfont)
1527
+ wouldfit = True
1528
+ i = 0
1529
+ while i < len(label_text):
1530
+ # We know by definition that all single words
1531
+ # are okay for drawing, now we check whether
1532
+ # we can draw multiple in one line
1533
+ word = label_text[i]
1534
+ cont = True
1535
+ while cont:
1536
+ cont = False
1537
+ if i < len(label_text) - 1:
1538
+ nextword = label_text[i + 1]
1539
+ test = word + " " + nextword
1540
+ tw, th = dc.GetTextExtent(test)
1541
+ if tw < w:
1542
+ word = test
1543
+ i += 1
1544
+ cont = True
1545
+
1546
+ text_width, text_height = dc.GetTextExtent(word)
1547
+ if text_width > w:
1548
+ wouldfit = False
1549
+ break
1550
+ test_y += text_height
1551
+ total_text_height += text_height
1552
+ if test_y > y1:
1553
+ wouldfit = False
1554
+ text_edge = 0
1555
+ break
1556
+ i += 1
1557
+
1558
+ if wouldfit:
1559
+ # Let's see how much we have...
1560
+ if ptsize in self.font_sizes:
1561
+ self.font_sizes[ptsize] += 1
1562
+ else:
1563
+ self.font_sizes[ptsize] = 1
1564
+ break
1565
+
1566
+ ptsize -= 2
1567
+ if ptsize < 6: # too small
1568
+ break
1569
+
1570
+ def _paint_tab(self, dc: wx.DC, page: RibbonPage):
1571
+ """
1572
+ Paint the individual page tab.
1573
+
1574
+ @param dc:
1575
+ @param page:
1576
+ @return:
1577
+ """
1578
+ horizontal = self.parent.prefer_horizontal()
1579
+ highlight_via_color = False
1580
+
1581
+ dc.SetPen(wx.Pen(self.black_color))
1582
+ show_rect = True
1583
+ if page is not self.current_page:
1584
+ dc.SetBrush(wx.Brush(self.button_face))
1585
+ dc.SetTextForeground(self.text_color_inactive)
1586
+ if not highlight_via_color:
1587
+ show_rect = False
1588
+ else:
1589
+ dc.SetBrush(wx.Brush(self.highlight))
1590
+ dc.SetBrush(wx.Brush(self.highlight))
1591
+ dc.SetTextForeground(self.text_color)
1592
+ if not highlight_via_color:
1593
+ dc.SetBrush(wx.Brush(self.button_face))
1594
+ if page is self.hover_tab and self.hover_button is None:
1595
+ dc.SetBrush(wx.Brush(self.button_face_hover))
1596
+ show_rect = True
1597
+ x, y, x1, y1 = page.tab_position
1598
+ if show_rect:
1599
+ dc.DrawRoundedRectangle(
1600
+ int(x), int(y), int(x1 - x), int(y1 - y), self.rounded_radius
1601
+ )
1602
+ dc.SetFont(self.default_font)
1603
+ text_width, text_height = dc.GetTextExtent(page.label)
1604
+ tpx = int(x + (x1 - x - text_width) / 2)
1605
+ tpy = int(y + self.tab_text_buffer)
1606
+ if horizontal:
1607
+ dc.DrawText(page.label, tpx, tpy)
1608
+ else:
1609
+ tpx = int(x + self.tab_text_buffer)
1610
+ tpy = int(y1 - (y1 - y - text_width) / 2)
1611
+ dc.DrawRotatedText(page.label, tpx, tpy, 90)
1612
+
1613
+ def _paint_background(self, dc: wx.DC):
1614
+ """
1615
+ Paint the background of the ribbonbar.
1616
+ @param dc:
1617
+ @return:
1618
+ """
1619
+ w, h = dc.Size
1620
+ dc.SetBrush(wx.Brush(self.ribbon_background))
1621
+ dc.SetPen(wx.TRANSPARENT_PEN)
1622
+ dc.DrawRectangle(0, 0, w, h)
1623
+
1624
+ def _paint_panel(self, dc: wx.DC, panel: RibbonPanel):
1625
+ """
1626
+ Paint the ribbonpanel of the given panel.
1627
+ @param dc:
1628
+ @param panel:
1629
+ @return:
1630
+ """
1631
+ if not panel.position:
1632
+ # print(f"Panel position was not set for {panel.label}")
1633
+ return
1634
+ x, y, x1, y1 = panel.position
1635
+ # print(f"Painting panel {panel.label}: {panel.position}")
1636
+ dc.SetBrush(wx.Brush(self.ribbon_background))
1637
+ dc.SetPen(wx.Pen(self.black_color))
1638
+ dc.DrawRoundedRectangle(
1639
+ int(x), int(y), int(x1 - x), int(y1 - y), self.rounded_radius
1640
+ )
1641
+ """
1642
+ Paint the overflow of buttons that cannot be stored within the required width.
1643
+
1644
+ @param dc:
1645
+ @return:
1646
+ """
1647
+ if not panel._overflow_position:
1648
+ return
1649
+ x, y, x1, y1 = panel._overflow_position
1650
+ dc.SetBrush(wx.Brush(self.highlight))
1651
+ dc.SetPen(wx.Pen(self.black_color))
1652
+ dc.DrawRoundedRectangle(
1653
+ int(x), int(y), int(x1 - x), int(y1 - y), self.rounded_radius
1654
+ )
1655
+ r = min((y1 - y) / 2, (x1 - x) / 2) - 2
1656
+ cx = (x + x1) / 2
1657
+ cy = -r / 2 + (y + y1) / 2
1658
+ # print (f"area: {x},{y}-{x1},{y1} - center={cx},{cy} r={r}")
1659
+ # points = [
1660
+ # (
1661
+ # int(cx + r * math.cos(math.radians(angle))),
1662
+ # int(cy + r * math.sin(math.radians(angle))),
1663
+ # )
1664
+ # for angle in (0, 90, 180)
1665
+ # ]
1666
+ lx = x + (x1 - x) / 8
1667
+ rx = x1 - (x1 - x) / 8
1668
+ mx = x + (x1 - x) / 2
1669
+ ty = y + (y1 - y) * 2 / 8
1670
+ by = y1 - (y1 - y) * 2 / 8
1671
+ points = [
1672
+ (int(lx), int(ty)),
1673
+ (int(rx), int(ty)),
1674
+ (int(mx), int(by)),
1675
+ (int(lx), int(ty)),
1676
+ ]
1677
+ dc.SetPen(wx.Pen(self.black_color))
1678
+ dc.SetBrush(wx.Brush(self.inactive_background))
1679
+ dc.DrawPolygon(points)
1680
+
1681
+ def _paint_dropdown(self, dc: wx.DC, dropdown: DropDown):
1682
+ """
1683
+ Paint the dropdown on the button containing a dropdown.
1684
+
1685
+ @param dc:
1686
+ @param dropdown:
1687
+ @return:
1688
+ """
1689
+ x, y, x1, y1 = dropdown.position
1690
+ if dropdown is self.hover_dropdown:
1691
+ dc.SetBrush(wx.Brush(wx.Colour(self.highlight)))
1692
+ else:
1693
+ dc.SetBrush(wx.TRANSPARENT_BRUSH)
1694
+ dc.SetPen(wx.TRANSPARENT_PEN)
1695
+
1696
+ dc.DrawRoundedRectangle(
1697
+ int(x), int(y), int(x1 - x), int(y1 - y), self.rounded_radius
1698
+ )
1699
+ lx = x + (x1 - x) / 8
1700
+ rx = x1 - (x1 - x) / 8
1701
+ mx = x + (x1 - x) / 2
1702
+ ty = y + (y1 - y) * 2 / 8
1703
+ by = y1 - (y1 - y) * 2 / 8
1704
+ points = [
1705
+ (int(lx), int(ty)),
1706
+ (int(rx), int(ty)),
1707
+ (int(mx), int(by)),
1708
+ (int(lx), int(ty)),
1709
+ ]
1710
+ dc.SetPen(wx.Pen(self.black_color))
1711
+ dc.SetBrush(wx.Brush(self.inactive_background))
1712
+ dc.DrawPolygon(points)
1713
+
1714
+ def _paint_button(self, dc: wx.DC, button: Button):
1715
+ """
1716
+ Paint the given button on the screen.
1717
+
1718
+ @param dc:
1719
+ @param button:
1720
+ @return:
1721
+ """
1722
+ if button.overflow or not button.visible or button.position is None:
1723
+ return
1724
+
1725
+ dc.SetBrush(wx.Brush(self.button_face))
1726
+ dc.SetPen(wx.TRANSPARENT_PEN)
1727
+ dc.SetTextForeground(self.text_color)
1728
+ if not button.enabled:
1729
+ dc.SetBrush(wx.Brush(self.inactive_background))
1730
+ dc.SetPen(wx.TRANSPARENT_PEN)
1731
+ dc.SetTextForeground(self.text_color_disabled)
1732
+ if button.toggle:
1733
+ dc.SetBrush(wx.Brush(self.highlight))
1734
+ dc.SetPen(wx.Pen(self.black_color))
1735
+ if self.hover_button is button and self.hover_dropdown is None:
1736
+ dc.SetBrush(wx.Brush(self.button_face_hover))
1737
+ dc.SetPen(wx.Pen(self.black_color))
1738
+
1739
+ x, y, x1, y1 = button.position
1740
+ start_y = y
1741
+ w = int(round(x1 - x, 2))
1742
+ h = int(round(y1 - y, 2))
1743
+ img_h = h
1744
+ # do we have text? if yes let's reduce the available space in y
1745
+ if self.show_labels: # Regardless whether we have a label or not...
1746
+ img_h -= self.bitmap_text_buffer
1747
+ ptsize = min(18, int(round(min(w, img_h) / 5.0, 2)) * 2)
1748
+ img_h -= int(ptsize * 1.35)
1749
+
1750
+ button.get_bitmaps(min(w, img_h))
1751
+ if button.enabled:
1752
+ bitmap = button.bitmap
1753
+ else:
1754
+ bitmap = button.bitmap_disabled
1755
+
1756
+ # Let's clip the output
1757
+ dc.SetClippingRegion(int(x), int(y), int(w), int(h))
1758
+
1759
+ dc.DrawRoundedRectangle(int(x), int(y), int(w), int(h), self.rounded_radius)
1760
+ bitmap_width, bitmap_height = bitmap.Size
1761
+ # if button.label in ("Circle", "Ellipse", "Wordlist", "Property Window"):
1762
+ # print (f"N - {button.label}: {bitmap_width}x{bitmap_height} in {w}x{h}")
1763
+ bs = min(bitmap_width, bitmap_height)
1764
+ ptsize = self.get_best_font_size(bs)
1765
+ font = wx.Font(
1766
+ ptsize, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL
1767
+ )
1768
+
1769
+ dc.DrawBitmap(bitmap, int(x + (w - bitmap_width) / 2), int(y))
1770
+
1771
+ # # For debug purposes: draw rectangle around bitmap
1772
+ # dc.SetBrush(wx.TRANSPARENT_BRUSH)
1773
+ # dc.SetPen(wx.RED_PEN)
1774
+ # dc.DrawRectangle(int(x + (w - bitmap_width) / 2), int(y), bitmap_width, bitmap_height)
1775
+
1776
+ y += bitmap_height
1777
+
1778
+ text_edge = self.bitmap_text_buffer
1779
+ if button.label and self.show_labels:
1780
+ show_text = True
1781
+ label_text = list(button.label.split(" "))
1782
+ # We try to establish whether this would fit properly.
1783
+ # We allow a small oversize of 25% to the button,
1784
+ # before we try to reduce the fontsize
1785
+ wouldfit = False
1786
+ while not wouldfit:
1787
+ total_text_height = 0
1788
+ testfont = wx.Font(
1789
+ ptsize,
1790
+ wx.FONTFAMILY_SWISS,
1791
+ wx.FONTSTYLE_NORMAL,
1792
+ wx.FONTWEIGHT_NORMAL,
1793
+ )
1794
+ test_y = y + text_edge
1795
+ dc.SetFont(testfont)
1796
+ wouldfit = True
1797
+ i = 0
1798
+ while i < len(label_text):
1799
+ # We know by definition that all single words
1800
+ # are okay for drawing, now we check whether
1801
+ # we can draw multiple in one line
1802
+ word = label_text[i]
1803
+ cont = True
1804
+ while cont:
1805
+ cont = False
1806
+ if i < len(label_text) - 1:
1807
+ nextword = label_text[i + 1]
1808
+ test = word + " " + nextword
1809
+ tw, th = dc.GetTextExtent(test)
1810
+ if tw < w:
1811
+ word = test
1812
+ i += 1
1813
+ cont = True
1814
+
1815
+ text_width, text_height = dc.GetTextExtent(word)
1816
+ if text_width > w:
1817
+ wouldfit = False
1818
+ break
1819
+ test_y += text_height
1820
+ total_text_height += text_height
1821
+ if test_y > y1:
1822
+ wouldfit = False
1823
+ text_edge = 0
1824
+ break
1825
+ i += 1
1826
+
1827
+ if wouldfit:
1828
+ font = testfont
1829
+ break
1830
+
1831
+ ptsize -= 2
1832
+ if ptsize < 6: # too small
1833
+ break
1834
+ if not wouldfit:
1835
+ show_text = False
1836
+ label_text = list()
1837
+ else:
1838
+ show_text = False
1839
+ label_text = list()
1840
+ if show_text:
1841
+ # if it wasn't a full fit, the new textsize might still be okay to be drawn at the intended position
1842
+ text_edge = min(
1843
+ max(0, start_y + h - y - total_text_height), self.bitmap_text_buffer
1844
+ )
1845
+
1846
+ y += text_edge
1847
+ dc.SetFont(font)
1848
+ i = 0
1849
+ while i < len(label_text):
1850
+ # We know by definition that all single words
1851
+ # are okay for drawing, now we check whether
1852
+ # we can draw multiple in one line
1853
+ word = label_text[i]
1854
+ cont = True
1855
+ while cont:
1856
+ cont = False
1857
+ if i < len(label_text) - 1:
1858
+ nextword = label_text[i + 1]
1859
+ test = word + " " + nextword
1860
+ tw, th = dc.GetTextExtent(test)
1861
+ if tw < w:
1862
+ word = test
1863
+ i += 1
1864
+ cont = True
1865
+
1866
+ text_width, text_height = dc.GetTextExtent(word)
1867
+ dc.DrawText(
1868
+ word,
1869
+ int(x + (w / 2.0) - (text_width / 2)),
1870
+ int(y),
1871
+ )
1872
+ y += text_height
1873
+ i += 1
1874
+ if button.dropdown is not None and button.dropdown.position is not None:
1875
+ self._paint_dropdown(dc, button.dropdown)
1876
+ dc.DestroyClippingRegion()
1877
+
1878
+ def layout(self, dc: wx.DC, ribbon):
1879
+ """
1880
+ Performs the layout of the page. This is determined to be the size of the ribbon minus any edge buffering.
1881
+
1882
+ @param dc:
1883
+ @param ribbon:
1884
+ @return:
1885
+ """
1886
+ ribbon_width, ribbon_height = dc.Size
1887
+ # print(f"ribbon: {dc.Size}")
1888
+ horizontal = self.parent.prefer_horizontal()
1889
+ xpos = 0
1890
+ ypos = 0
1891
+ has_page_header = ribbon.visible_pages() > 1
1892
+ dc.SetFont(self.default_font)
1893
+ for pn, page in enumerate(ribbon.pages):
1894
+ if not page.visible:
1895
+ continue
1896
+ # Set tab positioning.
1897
+ # Compute tabwidth according to be displayed label,
1898
+ # if bigger than default then extend width
1899
+ if has_page_header:
1900
+ line_width, line_height = dc.GetTextExtent(page.label)
1901
+ if line_height + 4 > self.tab_height:
1902
+ self.tab_height = line_height + 4
1903
+ for former in range(0, pn):
1904
+ former_page = ribbon.pages[former]
1905
+ t_x, t_y, t_x1, t_y1 = former_page.tab_position
1906
+ if horizontal:
1907
+ t_y1 = t_y + self.tab_height * 2
1908
+ else:
1909
+ t_x1 = t_x + self.tab_height * 2
1910
+ former_page.tab_position = (t_x, t_y, t_x1, t_y1)
1911
+
1912
+ tabwidth = max(line_width + 2 * self.tab_tab_buffer, self.tab_width)
1913
+ if horizontal:
1914
+ t_x = pn * self.tab_tab_buffer + xpos + self.tab_initial_buffer
1915
+ t_x1 = t_x + tabwidth
1916
+ t_y = ypos
1917
+ t_y1 = t_y + self.tab_height * 2
1918
+ else:
1919
+ t_y = pn * self.tab_tab_buffer + ypos + self.tab_initial_buffer
1920
+ t_y1 = t_y + tabwidth
1921
+ t_x = xpos
1922
+ t_x1 = t_x + self.tab_height * 2
1923
+ page.tab_position = (t_x, t_y, t_x1, t_y1)
1924
+ if horizontal:
1925
+ xpos += tabwidth
1926
+ else:
1927
+ ypos += tabwidth
1928
+ else:
1929
+ page.tab_position = (0, 0, 0, 0)
1930
+ if page is not self.current_page:
1931
+ continue
1932
+
1933
+ page_width = ribbon_width - self.edge_page_buffer
1934
+ page_height = ribbon_height - self.edge_page_buffer
1935
+ if horizontal:
1936
+ page_width -= self.edge_page_buffer
1937
+ x = self.edge_page_buffer
1938
+ if has_page_header:
1939
+ y = self.tab_height
1940
+ page_height -= self.tab_height
1941
+ else:
1942
+ x = self.edge_page_buffer
1943
+ y = 0
1944
+ else:
1945
+ page_height -= self.edge_page_buffer
1946
+ y = self.edge_page_buffer
1947
+ if has_page_header:
1948
+ x = self.tab_height
1949
+ page_width -= self.tab_height
1950
+ else:
1951
+ y = self.edge_page_buffer
1952
+ x = 0
1953
+
1954
+ # Page start position.
1955
+ if horizontal:
1956
+ if has_page_header:
1957
+ y = self.tab_height
1958
+ page_height += self.edge_page_buffer
1959
+ else:
1960
+ x = self.edge_page_buffer
1961
+ y = 0
1962
+ else:
1963
+ if has_page_header:
1964
+ x = self.tab_height
1965
+ page_width += self.edge_page_buffer
1966
+ else:
1967
+ y = self.edge_page_buffer
1968
+ x = 0
1969
+ # Set page position.
1970
+ page.position = (
1971
+ x,
1972
+ y,
1973
+ x + page_width,
1974
+ y + page_height,
1975
+ )
1976
+
1977
+ # if self.parent.visible_pages() == 1:
1978
+ # print(f"page: {page.position}")
1979
+ self.page_layout(dc, page)
1980
+
1981
+ def preferred_button_size_for_page(self, dc, page):
1982
+ x, y, max_x, max_y = page.position
1983
+ page_width = max_x - x
1984
+ page_height = max_y - y
1985
+ horizontal = self.parent.prefer_horizontal()
1986
+ is_horizontal = (self.orientation == self.RIBBON_ORIENTATION_HORIZONTAL) or (
1987
+ horizontal and self.orientation == self.RIBBON_ORIENTATION_AUTO
1988
+ )
1989
+ # Count buttons and panels
1990
+ total_button_count = 0
1991
+ panel_count = 0
1992
+ for panel in page.panels:
1993
+ plen = panel.visible_button_count
1994
+ total_button_count += plen
1995
+ if plen > 0:
1996
+ panel_count += 1
1997
+ # else:
1998
+ # print(f"No buttons for {panel.label} found during layout")
1999
+ # Calculate h/v counts for panels and buttons
2000
+ if is_horizontal:
2001
+ all_button_horizontal = max(total_button_count, 1)
2002
+ all_button_vertical = 1
2003
+
2004
+ all_panel_horizontal = max(panel_count, 1)
2005
+ all_panel_vertical = 1
2006
+ else:
2007
+ all_button_horizontal = 1
2008
+ all_button_vertical = max(total_button_count, 1)
2009
+
2010
+ all_panel_horizontal = 1
2011
+ all_panel_vertical = max(panel_count, 1)
2012
+
2013
+ # Calculate optimal width/height for just buttons.
2014
+ button_width_across_panels = page_width
2015
+ button_width_across_panels -= (
2016
+ all_panel_horizontal - 1
2017
+ ) * self.between_panel_buffer
2018
+ button_width_across_panels -= 2 * self.page_panel_buffer
2019
+
2020
+ button_height_across_panels = page_height
2021
+ button_height_across_panels -= (
2022
+ all_panel_vertical - 1
2023
+ ) * self.between_panel_buffer
2024
+ button_height_across_panels -= 2 * self.page_panel_buffer
2025
+
2026
+ for p, panel in enumerate(page.panels):
2027
+ if p == 0:
2028
+ # Remove high-and-low perpendicular panel_button_buffer
2029
+ if is_horizontal:
2030
+ button_height_across_panels -= 2 * self.panel_button_buffer
2031
+ else:
2032
+ button_width_across_panels -= 2 * self.panel_button_buffer
2033
+ for b, button in enumerate(list(panel.visible_buttons())):
2034
+ if b == 0:
2035
+ # First and last buffers.
2036
+ if is_horizontal:
2037
+ button_width_across_panels -= 2 * self.panel_button_buffer
2038
+ else:
2039
+ button_height_across_panels -= 2 * self.panel_button_buffer
2040
+ else:
2041
+ # Each gap between buttons
2042
+ if is_horizontal:
2043
+ button_width_across_panels -= self.between_button_buffer
2044
+ else:
2045
+ button_height_across_panels -= self.between_button_buffer
2046
+
2047
+ # Calculate width/height for each button.
2048
+ button_width = button_width_across_panels / all_button_horizontal
2049
+ button_height = button_height_across_panels / all_button_vertical
2050
+
2051
+ return button_width, button_height
2052
+
2053
+ def page_layout(self, dc, page):
2054
+ """
2055
+ Determine the layout of the page. This calls for each panel to be set relative to the number of buttons it
2056
+ contains.
2057
+
2058
+ @param dc:
2059
+ @param page:
2060
+ @return:
2061
+ """
2062
+ x, y, max_x, max_y = page.position
2063
+ is_horizontal = (self.orientation == self.RIBBON_ORIENTATION_HORIZONTAL) or (
2064
+ self.parent.prefer_horizontal()
2065
+ and self.orientation == self.RIBBON_ORIENTATION_AUTO
2066
+ )
2067
+ button_width, button_height = self.preferred_button_size_for_page(dc, page)
2068
+ x += self.page_panel_buffer
2069
+ y += self.page_panel_buffer
2070
+ """
2071
+ THIS ALGORITHM NEEDS STILL TO BE IMPLEMENTED
2072
+ --------------------------------------------
2073
+ We discuss now the horizontal case, the same
2074
+ logic would apply for the vertical case.
2075
+ We iterate through the sizes and establish the space
2076
+ needed for every panel:
2077
+ 1) We calculate the required button dimensions for all
2078
+ combinations of tiny/small/regular icons plus with/without labels
2079
+ 2) We get the minimum amount of columns required to display
2080
+ the buttons (taking the vertical extent i.e. the amount
2081
+ of available rows into account).
2082
+ This will provide us with a solution that would need
2083
+ the least horizontal space.
2084
+ 3) That may lead to a situation where you would still
2085
+ have horizontal space available for the panels.
2086
+ Hence we do a second pass where we assign additional space
2087
+ to all panels that need more than one row of icons.
2088
+ As we will do this for all possible size combinations,
2089
+ we will chose eventually that solution that has the
2090
+ fewest amount of buttons in overflow.
2091
+ """
2092
+ # 1 Calculate button sizes - this is not required
2093
+ # here, already done during button creation
2094
+
2095
+ # 2 Loop over all sizes
2096
+
2097
+ # Now that we have gathered all information we can assign
2098
+ # the space...
2099
+ available_space = 0
2100
+ p = -1
2101
+ for panel in page.panels:
2102
+ if panel.visible_button_count == 0:
2103
+ continue
2104
+ p += 1
2105
+ if p != 0:
2106
+ # Non-first move between panel gap.
2107
+ if is_horizontal:
2108
+ x += self.between_panel_buffer
2109
+ else:
2110
+ y += self.between_panel_buffer
2111
+
2112
+ if is_horizontal:
2113
+ single_panel_horizontal = max(panel.visible_button_count, 1)
2114
+ single_panel_vertical = 1
2115
+ else:
2116
+ single_panel_horizontal = 1
2117
+ single_panel_vertical = max(panel.visible_button_count, 1)
2118
+
2119
+ panel_width = (
2120
+ single_panel_horizontal * button_width
2121
+ + (single_panel_horizontal - 1) * self.between_button_buffer
2122
+ + 2 * self.panel_button_buffer
2123
+ )
2124
+ panel_height = (
2125
+ single_panel_vertical * button_height
2126
+ + (single_panel_vertical - 1) * self.between_button_buffer
2127
+ + 2 * self.panel_button_buffer
2128
+ )
2129
+ if is_horizontal:
2130
+ sx = available_space
2131
+ sy = 0
2132
+ else:
2133
+ sx = 0
2134
+ sy = available_space
2135
+ panel_width += sx
2136
+ panel_height += sy
2137
+ # print (f"{panel.label} was {panel.position} will be {x}, {y}, {x + panel_width}, {y + panel_height}")
2138
+ panel.position = x, y, x + panel_width, y + panel_height
2139
+ panel_max_x, panel_max_y = self.panel_layout(dc, panel)
2140
+ # print (f"Max values: {panel_max_x}, {panel_max_y}")
2141
+ # Do we have more space than needed?
2142
+ available_space = 0
2143
+ if panel._overflow_position is None:
2144
+ recalc = False
2145
+ # print (f"({x}, {y}) - ({x + panel_width}, {y+panel_height}), sx={sx}, sy={sy}")
2146
+ if is_horizontal:
2147
+ available_space = max(
2148
+ 0, x + panel_width - panel_max_x - self.panel_button_buffer
2149
+ )
2150
+ # print (f"x={x + panel_width}, {panel_max_x} will become: {panel_max_x + self.panel_button_buffer}, available={available_space}")
2151
+ if available_space != 0:
2152
+ panel_width = panel_max_x + self.panel_button_buffer - x
2153
+ recalc = True
2154
+ else:
2155
+ available_space = max(
2156
+ 0, y + panel_height - panel_max_y - self.panel_button_buffer
2157
+ )
2158
+ # print (f"y={y + panel_height}, {panel_max_y} will become: {panel_max_y + self.panel_button_buffer}, available={available_space}")
2159
+ if available_space != 0:
2160
+ panel_height = panel_max_y + self.panel_button_buffer - y
2161
+ recalc = True
2162
+ if recalc:
2163
+ panel.position = x, y, x + panel_width, y + panel_height
2164
+ self.panel_layout(dc, panel)
2165
+
2166
+ if is_horizontal:
2167
+ x += panel_width
2168
+ else:
2169
+ y += panel_height
2170
+
2171
+ def panel_layout(self, dc: wx.DC, panel):
2172
+ x, y, max_x, max_y = panel.position
2173
+ panel_width = max_x - x
2174
+ panel_height = max_y - y
2175
+ # print(f"Panel: {panel.label}: {panel.position}")
2176
+ horizontal = self.parent.prefer_horizontal()
2177
+ is_horizontal = (self.orientation == self.RIBBON_ORIENTATION_HORIZONTAL) or (
2178
+ horizontal and self.orientation == self.RIBBON_ORIENTATION_AUTO
2179
+ )
2180
+ plen = panel.visible_button_count
2181
+ # if plen == 0:
2182
+ # print(f"layout for panel '{panel.label}' without buttons!")
2183
+
2184
+ distribute_evenly = False
2185
+ if is_horizontal:
2186
+ button_horizontal = max(plen, 1)
2187
+ button_vertical = 1
2188
+ else:
2189
+ button_horizontal = 1
2190
+ button_vertical = max(plen, 1)
2191
+
2192
+ all_button_width = (
2193
+ panel_width
2194
+ - (button_horizontal - 1) * self.between_button_buffer
2195
+ - 2 * self.panel_button_buffer
2196
+ )
2197
+ all_button_height = (
2198
+ panel_height
2199
+ - (button_vertical - 1) * self.between_button_buffer
2200
+ - 2 * self.panel_button_buffer
2201
+ )
2202
+
2203
+ button_width = all_button_width / button_horizontal
2204
+ button_height = all_button_height / button_vertical
2205
+ # 'tiny'-size of a default 50x50 icon
2206
+ minim_size = 15
2207
+ button_width = max(minim_size, button_width)
2208
+ button_height = max(minim_size, button_height)
2209
+
2210
+ x += self.panel_button_buffer
2211
+ y += self.panel_button_buffer
2212
+ panel._overflow.clear()
2213
+ panel._overflow_position = None
2214
+ for b, button in enumerate(list(panel.visible_buttons())):
2215
+ bitmapsize = button.max_size
2216
+ while bitmapsize > button.min_size:
2217
+ if bitmapsize <= button_height and bitmapsize <= button_width:
2218
+ break
2219
+ bitmapsize -= 5
2220
+ button.get_bitmaps(bitmapsize)
2221
+ self.button_calc(dc, button)
2222
+ if b == 0:
2223
+ max_width = button.min_width
2224
+ max_height = button.min_height
2225
+ else:
2226
+ max_width = max(max_width, button.min_width)
2227
+ max_height = max(max_height, button.min_height)
2228
+
2229
+ target_height = button_height
2230
+ target_width = button_width
2231
+ # print(f"Target: {panel.label} - {target_width}x{target_height}")
2232
+ for b, button in enumerate(list(panel.visible_buttons())):
2233
+ this_width = target_width
2234
+ this_height = target_height
2235
+ local_width = 1.25 * button.min_width
2236
+ local_height = 1.25 * button.min_height
2237
+ if not distribute_evenly:
2238
+ if button_horizontal > 1 or is_horizontal:
2239
+ this_width = min(this_width, local_width)
2240
+ if button_vertical > 1 or not is_horizontal:
2241
+ this_height = min(this_height, local_height)
2242
+ if b != 0:
2243
+ # Move across button gap if not first button.
2244
+ if is_horizontal:
2245
+ x += self.between_button_buffer
2246
+ else:
2247
+ y += self.between_button_buffer
2248
+ button.position = x, y, x + this_width, y + this_height
2249
+ if is_horizontal:
2250
+ is_overflow = False
2251
+ if x + this_width > panel.position[2]:
2252
+ is_overflow = True
2253
+ # Let's establish whether there is place for another row of icons underneath
2254
+ # print(
2255
+ # f"Horizontal Overflow: y={y}, b-height={max_height}, new max={y + 2 * max_height + self.panel_button_buffer}, panel: {panel.position[3]}"
2256
+ # )
2257
+ if (
2258
+ y + 2 + max_height + self.panel_button_buffer
2259
+ < panel.position[3]
2260
+ ):
2261
+ is_overflow = False
2262
+ target_height = max_height
2263
+ # Reset height of all previous buttons:
2264
+ for bb, bbutton in enumerate(list(panel.visible_buttons())):
2265
+ if bb >= b:
2266
+ break
2267
+ bbutton.position = (
2268
+ bbutton.position[0],
2269
+ bbutton.position[1],
2270
+ bbutton.position[2],
2271
+ bbutton.position[1] + max_height,
2272
+ )
2273
+ x = panel.position[0] + self.panel_button_buffer
2274
+ y += max_height + self.panel_button_buffer
2275
+ button.position = x, y, x + this_width, y + max_height
2276
+ if is_overflow and panel._overflow_position is None:
2277
+ ppx, ppy, ppx1, ppy1 = panel.position
2278
+ panel._overflow_position = (ppx1 - 15, ppy, ppx1, ppy1)
2279
+ else:
2280
+ is_overflow = False
2281
+ if y + this_height > panel.position[3]:
2282
+ is_overflow = True
2283
+ # print(
2284
+ # f"Vertical Overflow: x={x}, b-width={max_width}, new max={x + 2 * max_width + self.panel_button_buffer}, panel: {panel.position[2]}"
2285
+ # )
2286
+ # Let's establish whether there is place for another column of icons to the right
2287
+ if x + 2 * max_width + self.panel_button_buffer < panel.position[2]:
2288
+ is_overflow = False
2289
+ target_width = max_width
2290
+ # Reset width of all previous buttons:
2291
+ for bb, bbutton in enumerate(list(panel.visible_buttons())):
2292
+ if bb >= b:
2293
+ break
2294
+ bbutton.position = (
2295
+ bbutton.position[0],
2296
+ bbutton.position[1],
2297
+ bbutton.position[0] + max_width,
2298
+ bbutton.position[3],
2299
+ )
2300
+ y = panel.position[1] + self.panel_button_buffer
2301
+ x += max_width + self.panel_button_buffer
2302
+ button.position = x, y, x + max_width, y + this_height
2303
+ if is_overflow and panel._overflow_position is None:
2304
+ ppx, ppy, ppx1, ppy1 = panel.position
2305
+ panel._overflow_position = (ppx, ppy1 - 15, ppx1, ppy1)
2306
+
2307
+ # print(f"button: {button.position}")
2308
+
2309
+ if is_horizontal:
2310
+ x += this_width
2311
+ else:
2312
+ y += this_height
2313
+ x = 0
2314
+ y = 0
2315
+ for button in panel.visible_buttons():
2316
+ button.overflow = False
2317
+ if button.position[2] > panel.position[2]:
2318
+ button.overflow = True
2319
+ elif button.position[3] > panel.position[3]:
2320
+ button.overflow = True
2321
+ elif (
2322
+ is_horizontal
2323
+ and panel._overflow_position is not None
2324
+ and button.position[2] > panel._overflow_position[0]
2325
+ ):
2326
+ button.overflow = True
2327
+ elif (
2328
+ not is_horizontal
2329
+ and panel._overflow_position is not None
2330
+ and button.position[3] > panel._overflow_position[1]
2331
+ ):
2332
+ button.overflow = True
2333
+ # if panel.label == "Create":
2334
+ # print (f"{button.label}: {button.overflow}, {button.position}, {panel.position}, {panel._overflow_position}")
2335
+ if button.overflow:
2336
+ panel._overflow.append(button)
2337
+ else:
2338
+ x = max(x, button.position[2])
2339
+ y = max(y, button.position[3])
2340
+ self.button_layout(dc, button)
2341
+ if panel._overflow_position is not None:
2342
+ x = max(x, panel._overflow_position[2])
2343
+ y = max(y, panel._overflow_position[3])
2344
+
2345
+ return min(x, panel.position[2]), min(y, panel.position[3])
2346
+
2347
+ def button_calc(self, dc: wx.DC, button):
2348
+ bitmap = button.bitmap
2349
+ ptsize = self.get_font_size(button.icon_size)
2350
+ font = wx.Font(
2351
+ ptsize, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL
2352
+ )
2353
+
2354
+ dc.SetFont(font)
2355
+ bitmap_width, bitmap_height = bitmap.Size
2356
+ bitmap_height = max(bitmap_height, button.icon_size)
2357
+ bitmap_width = max(bitmap_width, button.icon_size)
2358
+
2359
+ # Calculate text height/width
2360
+ text_width = 0
2361
+ text_height = 0
2362
+ if button.label and self.show_labels:
2363
+ label_text = list(button.label.split(" "))
2364
+ i = 0
2365
+ while i < len(label_text):
2366
+ # We know by definition that all single words
2367
+ # are okay for drawing, now we check whether
2368
+ # we can draw multiple in one line
2369
+ word = label_text[i]
2370
+ cont = True
2371
+ while cont:
2372
+ cont = False
2373
+ if i < len(label_text) - 1:
2374
+ nextword = label_text[i + 1]
2375
+ test = word + " " + nextword
2376
+ tw, th = dc.GetTextExtent(test)
2377
+ if tw < bitmap_width:
2378
+ word = test
2379
+ i += 1
2380
+ cont = True
2381
+ line_width, line_height = dc.GetTextExtent(word)
2382
+ text_width = max(text_width, line_width)
2383
+ text_height += line_height
2384
+ i += 1
2385
+
2386
+ # Calculate button_width/button_height
2387
+ button_width = max(bitmap_width, text_width)
2388
+ button_height = bitmap_height
2389
+ button_height += 2 * self.panel_button_buffer
2390
+ button_width += 2 * self.panel_button_buffer
2391
+ if button.label and self.show_labels:
2392
+ # button_height += + self.panel_button_buffer
2393
+ button_height += self.bitmap_text_buffer + text_height
2394
+
2395
+ button.min_width = button_width
2396
+ button.min_height = button_height
2397
+ # print (f"layout for {button.label} ({button.bitmapsize}): {button.min_width}x{button.min_height}, icon={bitmap_width}x{bitmap_height}")
2398
+
2399
+ def button_layout(self, dc: wx.DC, button):
2400
+ x, y, max_x, max_y = button.position
2401
+ bitmap = button.bitmap
2402
+ bitmap_width, bitmap_height = bitmap.Size
2403
+ if button.kind == "hybrid" and button.key != "toggle":
2404
+ # Calculate text height/width
2405
+ # Calculate dropdown
2406
+ # Same size regardless of bitmap-size
2407
+ sizx = 15
2408
+ sizy = 15
2409
+ if min(bitmap_width, bitmap_height) > 70:
2410
+ sizx = 20
2411
+ sizy = 20
2412
+ elif min(bitmap_width, bitmap_height) > 100:
2413
+ sizx = 25
2414
+ sizy = 25
2415
+
2416
+ # Let's see whether we have enough room
2417
+ extx = (x + max_x) / 2 + bitmap_width / 2 + sizx - 1
2418
+ exty = y + bitmap_height + sizy - 1
2419
+ extx = max(x - sizx, min(extx, max_x - 1))
2420
+ exty = max(y + sizy, min(exty, max_y - 1))
2421
+ gap = 15
2422
+ if bitmap_height < 30:
2423
+ gap = 3
2424
+
2425
+ # print (f"{bitmap_width}x{bitmap_height} - siz={sizx}, gap={gap}")
2426
+ button.dropdown.position = (
2427
+ extx - sizx,
2428
+ exty - sizy - gap,
2429
+ extx,
2430
+ exty - gap,
2431
+ )
2432
+ # button.dropdown.position = (
2433
+ # x + bitmap_width / 2,
2434
+ # y + bitmap_height / 2,
2435
+ # x + bitmap_width,
2436
+ # y + bitmap_height,
2437
+ # )
2438
+ # print (
2439
+ # f"Required for {button.label}: button: {x},{y} to {max_x},{max_y}," +
2440
+ # f"dropd: {extx-sizx},{exty-sizy} to {extx},{exty}"
2441
+ # )
2442
+
2443
+ def get_font_size(self, imgsize):
2444
+ if imgsize <= 20:
2445
+ ptsize = 6
2446
+ elif imgsize <= 30:
2447
+ ptsize = 8
2448
+ elif imgsize <= 40:
2449
+ ptsize = 10
2450
+ elif imgsize <= 60:
2451
+ ptsize = 12
2452
+ elif imgsize <= 80:
2453
+ ptsize = 14
2454
+ else:
2455
+ ptsize = 16
2456
+ return ptsize
2457
+
2458
+ def get_best_font_size(self, imgsize):
2459
+ sizes = [(pt, amount) for pt, amount in self.font_sizes.items()]
2460
+ sizes.sort(key=lambda e: e[1], reverse=True)
2461
+ best = 32768
2462
+ if len(sizes):
2463
+ # Take the one where we have most...
2464
+ best = sizes[0][0]
2465
+ ptsize = self.get_font_size(imgsize)
2466
+ if ptsize > best:
2467
+ ptsize = best
2468
+ return ptsize