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

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