├── .gitignore ├── .pylintrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── checkbox-icons.svg ├── encode_bitmaps.py └── icons_source │ ├── BRUSH_CHECKERBOARD.png │ ├── CHECKBOX_CHECKED.png │ ├── CHECKBOX_FOCUSED.png │ ├── CHECKBOX_UNCHECKED.png │ ├── DROPDOWN_ARROW.png │ └── TEST.png ├── demo.py ├── gswidgetkit ├── __init__.py ├── buttons.py ├── checkbox.py ├── color_picker.py ├── constants.py ├── dropdown.py ├── foldpanelbar.py ├── icons │ ├── __init__.py │ └── icons.py ├── label.py ├── number_field.py ├── panel.py ├── popups.py ├── scrollbar.py ├── textctrl.py ├── tooltip.py ├── utils.py ├── z_matrix.py └── zoom_panel.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Ignore PyPI stuff 3 | build 4 | dist 5 | gswidgetkit.egg-info 6 | 7 | # Ignore vscode 8 | .vscode 9 | 10 | # Ignore python misc. 11 | __pycache__ 12 | 13 | # Ignore various files 14 | buffered_with_controls.py 15 | image_picker.py 16 | env 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | generated-members = wx.* 2 | extension-pkg-whitelist = wx 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to gswidgetkit 2 | 3 | Thanks for taking the time to contribute. We're excited to have you! 4 | 5 | gswidgetkit is a wxPython extension library originally created for use in [Gimel Studio](https://github.com/GimelStudio/GimelStudio) and the same forums, etc can be used to discuss gswidgetkit. 6 | 7 | The main purpose of the gswidgetkit package is to provide widgets and common utilities for use in Gimel Studio. Since Gimel Studio directy relies on this package, some issues in the main Gimel Studio repository will make reference to something that needs to be fixed/implemented here. 8 | 9 | Please see the Gimel Studio [CONTRIBUTING.md](https://github.com/GimelStudio/GimelStudio/blob/master/CONTRIBUTING.md) for overall guidelines and details on contributing. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GimelStudio/gswidgetkit/76b6e6acd5f5fa52506b7296c4fa125687a056c0/README.md -------------------------------------------------------------------------------- /assets/checkbox-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /assets/encode_bitmaps.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # gswidgetkit Copyright 2021-2022 by Noah Rahm and contributors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ---------------------------------------------------------------------------- 16 | 17 | """ 18 | Utility script to generate PyEmbeddedImage bitmap icons and write into 19 | 'icons.py' file from images in the icons_source directory. 20 | 21 | 1. Just run this file in-place and an 'icons.py' file should be 22 | generated. 23 | 2. Place the icons file in the 'gswidgetkit/icons' directory. 24 | """ 25 | 26 | import os 27 | from wx.tools import img2py 28 | 29 | 30 | def PrepareIconCommands(dest_file='icons.py'): 31 | filelist = [] 32 | for file in os.listdir("icons_source/"): 33 | if file.endswith('.png'): 34 | filelist.append(file) 35 | 36 | commandlines = [] 37 | for icon in filelist: 38 | ico_path = "icons_source/{}".format(icon) 39 | ico_name = icon.split('.')[0] 40 | cmd = "-a -n ICON_{} {} {}".format(ico_name, ico_path, dest_file) 41 | commandlines.append(cmd) 42 | return commandlines 43 | 44 | 45 | if __name__ == "__main__": 46 | command_lines = PrepareIconCommands() 47 | for line in command_lines: 48 | args = line.split() 49 | img2py.main(args) 50 | 51 | # Add import statement to top 52 | with open("icons.py", "r+") as file: 53 | file.write("from wx.lib.embeddedimage import PyEmbeddedImage\n#") 54 | print("Done!") 55 | -------------------------------------------------------------------------------- /assets/icons_source/BRUSH_CHECKERBOARD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GimelStudio/gswidgetkit/76b6e6acd5f5fa52506b7296c4fa125687a056c0/assets/icons_source/BRUSH_CHECKERBOARD.png -------------------------------------------------------------------------------- /assets/icons_source/CHECKBOX_CHECKED.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GimelStudio/gswidgetkit/76b6e6acd5f5fa52506b7296c4fa125687a056c0/assets/icons_source/CHECKBOX_CHECKED.png -------------------------------------------------------------------------------- /assets/icons_source/CHECKBOX_FOCUSED.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GimelStudio/gswidgetkit/76b6e6acd5f5fa52506b7296c4fa125687a056c0/assets/icons_source/CHECKBOX_FOCUSED.png -------------------------------------------------------------------------------- /assets/icons_source/CHECKBOX_UNCHECKED.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GimelStudio/gswidgetkit/76b6e6acd5f5fa52506b7296c4fa125687a056c0/assets/icons_source/CHECKBOX_UNCHECKED.png -------------------------------------------------------------------------------- /assets/icons_source/DROPDOWN_ARROW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GimelStudio/gswidgetkit/76b6e6acd5f5fa52506b7296c4fa125687a056c0/assets/icons_source/DROPDOWN_ARROW.png -------------------------------------------------------------------------------- /assets/icons_source/TEST.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GimelStudio/gswidgetkit/76b6e6acd5f5fa52506b7296c4fa125687a056c0/assets/icons_source/TEST.png -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # gswidgetkit Copyright 2021-2022 by Noah Rahm and contributors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ---------------------------------------------------------------------------- 16 | 17 | import wx 18 | 19 | import ctypes 20 | try: 21 | ctypes.windll.shcore.SetProcessDpiAwareness(True) 22 | except Exception: 23 | pass 24 | 25 | from gswidgetkit import (NumberField, EVT_NUMBERFIELD, Label, 26 | EVT_NUMBERFIELD_CHANGE, NativeTextCtrl, 27 | TextCtrl, StyledTextCtrl, ColorPickerButton, 28 | EVT_BUTTON, Button, CheckBox, ToolTip, DropDown, 29 | EVT_DROPDOWN, EVT_COLORPICKER_BUTTON) 30 | from gswidgetkit.icons import ICON_TEST 31 | 32 | 33 | class TestAppFrame(wx.Frame): 34 | def __init__(self, *args, **kwds): 35 | kwds["style"] = wx.DEFAULT_FRAME_STYLE 36 | wx.Frame.__init__(self, *args, **kwds) 37 | self.SetSize((600, 800)) 38 | self.SetBackgroundColour(wx.Colour("#1E2429")) 39 | 40 | sz = wx.BoxSizer(wx.VERTICAL) 41 | 42 | ctrl1 = NumberField(self, default_value=10, label="Resolution", 43 | min_value=0, max_value=25, suffix="px") 44 | ctrl2 = NumberField(self, default_value=98, label="Opacity", 45 | min_value=0, max_value=100, suffix="%", show_p=True) 46 | 47 | ctrl3 = NumberField(self, default_value=0, label="Radius", 48 | min_value=0, max_value=10, suffix="", 49 | show_p=False, disable_precise=True) 50 | 51 | ctrl4 = NumberField(self, default_value=50, label="X:", 52 | min_value=-10, max_value=100, suffix="", show_p=False) 53 | ctrl5 = NumberField(self, default_value=13, label="Y:", 54 | min_value=-10, max_value=100, suffix="", show_p=True) 55 | 56 | ctrl6 = StyledTextCtrl(self, value="", style=wx.BORDER_SIMPLE, 57 | placeholder="", size=(-1, 24)) 58 | ctrl7 = NativeTextCtrl(self, size=(-1, 26)) 59 | 60 | ctrl8 = ColorPickerButton(self, label="Background Color:") 61 | ctrl9 = ColorPickerButton(self, label="Highlight Color:", 62 | default=(0, 54, 78, 215)) 63 | ctrl10 = ColorPickerButton(self, label="Text Color:", 64 | default=(255, 255, 255, 255)) 65 | 66 | ctrl11 = Button(self, label="Contrast", 67 | bmp=(ICON_TEST.GetBitmap(), 'left')) 68 | ToolTip("Contrast", """Adjusts the degree of difference between the lightest 69 | and darkest parts of a picture.""", target=ctrl11, footer="Shortcut: Ctrl+S") 70 | 71 | ctrl12 = Button(self, label="Render Image") 72 | ctrl13 = Button(self, label="Contrast", 73 | bmp=(ICON_TEST.GetBitmap(), 'top')) 74 | ctrl14 = Button(self, label="Choose Layer", 75 | bmp=(ICON_TEST.GetBitmap(), 'left')) 76 | 77 | sz2 = wx.BoxSizer(wx.HORIZONTAL) 78 | 79 | ctrl15 = Button(self, label="", 80 | bmp=(ICON_TEST.GetBitmap(), 'left')) 81 | ctrl16 = Button(self, label="", 82 | bmp=(ICON_TEST.GetBitmap(), 'left')) 83 | ctrl17 = Button(self, label="", 84 | bmp=(ICON_TEST.GetBitmap(), 'left')) 85 | ctrl18 = Button(self, label="", 86 | bmp=(ICON_TEST.GetBitmap(), 'left')) 87 | ctrl18.SetHighlighted(True) 88 | 89 | ctrl19 = CheckBox(self, label="Auto Render") 90 | 91 | ctrl20 = DropDown(self, items=["SCREEN", "ADD", "MULTIPLY"], default="ADD") 92 | 93 | ctrl21 = Label(self, label="This is a label", color="#ccc", font_bold=True) 94 | 95 | sz3 = wx.BoxSizer(wx.HORIZONTAL) 96 | 97 | ctrl22 = TextCtrl(self, default="hello", icon=ICON_TEST.GetBitmap(), 98 | size=(-1, 28)) 99 | 100 | sz2.Add(ctrl15, flag=wx.EXPAND | wx.ALL, border=6) 101 | sz2.Add(ctrl16, flag=wx.EXPAND | wx.ALL, border=6) 102 | sz2.Add(ctrl17, flag=wx.EXPAND | wx.ALL, border=6) 103 | sz2.Add(ctrl18, flag=wx.EXPAND | wx.ALL, border=6) 104 | sz2.Add(ctrl19, flag=wx.EXPAND | wx.ALL, border=6) 105 | sz2.Add(ctrl20, flag=wx.EXPAND | wx.ALL, border=6) 106 | sz2.Add(ctrl21, flag=wx.EXPAND | wx.ALL, border=6) 107 | 108 | sz3.Add(ctrl22, flag=wx.EXPAND | wx.ALL, border=6) 109 | 110 | sz.Add(ctrl1, flag=wx.EXPAND | wx.ALL, border=6) 111 | sz.Add(ctrl2, flag=wx.EXPAND | wx.ALL, border=6) 112 | sz.Add(ctrl3, flag=wx.EXPAND | wx.ALL, border=6) 113 | sz.Add(ctrl4, flag=wx.EXPAND | wx.ALL, border=6) 114 | sz.Add(ctrl5, flag=wx.EXPAND | wx.ALL, border=6) 115 | sz.Add(ctrl6, flag=wx.EXPAND | wx.ALL, border=6) 116 | sz.Add(ctrl7, flag=wx.EXPAND | wx.ALL, border=6) 117 | sz.Add(ctrl8, flag=wx.EXPAND | wx.ALL, border=6) 118 | sz.Add(ctrl9, flag=wx.EXPAND | wx.ALL, border=6) 119 | sz.Add(ctrl10, flag=wx.EXPAND | wx.ALL, border=6) 120 | 121 | sz.Add(ctrl11, flag=wx.EXPAND | wx.ALL, border=6) 122 | sz.Add(ctrl12, flag=wx.EXPAND | wx.ALL, border=6) 123 | sz.Add(ctrl13, flag=wx.EXPAND | wx.ALL, border=6) 124 | sz.Add(ctrl14, flag=wx.EXPAND | wx.ALL, border=6) 125 | sz.Add(sz2, border=20) 126 | sz.Add(sz3, border=20) 127 | 128 | self.SetSizer(sz) 129 | 130 | # Events 131 | self.Bind(EVT_NUMBERFIELD_CHANGE, self.OnFieldChange, ctrl1) 132 | self.Bind(EVT_NUMBERFIELD, self.OnField, ctrl1) 133 | self.Bind(EVT_COLORPICKER_BUTTON, self.OnColorChosen, ctrl8) 134 | self.Bind(EVT_COLORPICKER_BUTTON, self.OnColorChosen, ctrl9) 135 | self.Bind(EVT_COLORPICKER_BUTTON, self.OnColorChosen, ctrl10) 136 | self.Bind(EVT_BUTTON, self.OnButtonClick, ctrl11) 137 | self.Bind(EVT_BUTTON, self.OnButtonClick, ctrl12) 138 | self.Bind(EVT_BUTTON, self.OnButtonClick, ctrl13) 139 | self.Bind(EVT_BUTTON, self.OnButtonClick, ctrl14) 140 | self.Bind(EVT_DROPDOWN, self.OnDropdown, ctrl20) 141 | 142 | 143 | def OnFieldChange(self, event): 144 | print("->", event.value) 145 | 146 | def OnField(self, event): 147 | print("->", event.value) 148 | 149 | def OnColorChosen(self, event): 150 | print("Color selected: ", event.value) 151 | 152 | def OnButtonClick(self, event): 153 | print("Button clicked: ", event.GetId()) 154 | 155 | def OnDropdown(self, event): 156 | print("->", event.value) 157 | 158 | 159 | if __name__ == "__main__": 160 | app = wx.App(False) 161 | frame = TestAppFrame(None, wx.ID_ANY, "gswidgetkit Demo") 162 | app.SetTopWindow(frame) 163 | frame.Show() 164 | app.MainLoop() 165 | -------------------------------------------------------------------------------- /gswidgetkit/__init__.py: -------------------------------------------------------------------------------- 1 | from .number_field import (NumberField, EVT_NUMBERFIELD, 2 | EVT_NUMBERFIELD_CHANGE) 3 | from .buttons import Button, EVT_BUTTON 4 | from .color_picker import ColorPickerButton, EVT_COLORPICKER_BUTTON 5 | from .textctrl import TextCtrl, NativeTextCtrl, StyledTextCtrl 6 | from .checkbox import CheckBox 7 | from .tooltip import ToolTip 8 | from .dropdown import DropDown, EVT_DROPDOWN 9 | from .label import Label 10 | from .utils import GetTextExtent 11 | from .zoom_panel import ZoomPanel 12 | from .foldpanelbar import FoldPanelBar 13 | from .z_matrix import ZMatrix 14 | -------------------------------------------------------------------------------- /gswidgetkit/buttons.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # gswidgetkit Copyright 2021-2022 by Noah Rahm and contributors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ---------------------------------------------------------------------------- 16 | 17 | import wx 18 | from wx.lib.newevent import NewCommandEvent 19 | 20 | from .constants import TEXT_COLOR, ACCENT_COLOR, BUTTON_BG_COLOR 21 | 22 | button_cmd_event, EVT_BUTTON = NewCommandEvent() 23 | 24 | 25 | class Button(wx.Control): 26 | """ 27 | Button with support for the following combinations: 28 | 1. text 29 | 2. icon + text 30 | 3. icon 31 | 32 | :param wx.Window `parent`: parent window. Must not be ``None``. 33 | :param integer `id`: window identifier. A value of -1 indicates a default value. 34 | :param string `label`: the displayed button label. 35 | :param tuple `bmp`: the button icon as (wx.Bitmap, pos). pos can be one of 'top', 'left', 'right', 'bottom'. 36 | :param bool `center`: if True the contents of the button will be centered rather than left-aligned. 37 | :param bool `flat`: if True, the background will take on the color of the parent window. 38 | """ 39 | def __init__(self, parent, id=wx.ID_ANY, label="", bmp=None, center=True, 40 | flat=False, pos=wx.DefaultPosition, size=wx.DefaultSize, 41 | style=wx.NO_BORDER, *args, **kwargs): 42 | wx.Control.__init__(self, parent, id, pos, size, style, *args, **kwargs) 43 | 44 | self.parent = parent 45 | 46 | # Add spaces around a button with text 47 | if label != "": 48 | self.label = " {} ".format(label) 49 | self.padding = (10, 20, 10, 20) 50 | else: 51 | # Icon button 52 | self.label = label 53 | self.padding = (5, 6, 5, 6) 54 | 55 | self.buffer = None 56 | self.center = center 57 | self.flat = flat 58 | self.outer_padding = 4 59 | self.size = None 60 | self.bmp = bmp 61 | 62 | self.mouse_in = False 63 | self.mouse_down = False 64 | self.focused = False 65 | self.highlighted = False 66 | 67 | self.Bind(wx.EVT_PAINT, self.OnPaint) 68 | self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None) 69 | self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) 70 | self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) 71 | self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave) 72 | self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnter) 73 | self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown) 74 | self.Bind(wx.EVT_LEFT_UP, self.OnMouseUp) 75 | self.Bind(wx.EVT_SIZE, self.OnSize) 76 | 77 | def OnPaint(self, event): 78 | wx.BufferedPaintDC(self, self.buffer) 79 | 80 | def OnSize(self, event): 81 | size = self.GetClientSize() 82 | 83 | # Make sure size is at least 1px to avoid 84 | # strange "invalid bitmap size" errors. 85 | if size[0] < 1: 86 | size = (1, 1) 87 | self.buffer = wx.Bitmap(*size) 88 | self.UpdateDrawing() 89 | 90 | def UpdateDrawing(self): 91 | dc = wx.MemoryDC() 92 | dc.SelectObject(self.buffer) 93 | dc = wx.GCDC(dc) 94 | self.OnDrawBackground(dc) 95 | self.OnDrawWidget(dc) 96 | del dc # need to get rid of the MemoryDC before Update() is called. 97 | self.Refresh() 98 | self.Update() 99 | 100 | def OnDrawBackground(self, dc): 101 | dc.SetBackground(wx.Brush(self.parent.GetBackgroundColour())) 102 | dc.Clear() 103 | 104 | def OnDrawWidget(self, dc): 105 | fnt = self.parent.GetFont() 106 | dc.SetFont(fnt) 107 | dc.SetPen(wx.TRANSPARENT_PEN) 108 | 109 | w, h = self.GetSize() 110 | 111 | if self.mouse_down or self.highlighted: 112 | dc.SetBrush(wx.Brush(wx.Colour(ACCENT_COLOR))) 113 | 114 | elif self.mouse_in: 115 | if self.flat is True: 116 | color = self.parent.GetBackgroundColour().ChangeLightness(110) 117 | else: 118 | color = wx.Colour(BUTTON_BG_COLOR) 119 | dc.SetBrush(wx.Brush(color)) 120 | 121 | else: 122 | if self.flat is True: 123 | color = self.parent.GetBackgroundColour() 124 | else: 125 | color = wx.Colour(BUTTON_BG_COLOR).ChangeLightness(85) 126 | dc.SetBrush(wx.Brush(color)) 127 | 128 | dc.DrawRoundedRectangle(0, 0, w, h, 4) 129 | 130 | txt_w, txt_h = dc.GetTextExtent(self.label) 131 | 132 | if self.bmp is not None: 133 | bmp = self.bmp 134 | bmp_w, bmp_h = bmp[0].GetSize() 135 | position = bmp[1] 136 | else: 137 | bmp = False 138 | 139 | if bmp: 140 | 141 | if position == "left": 142 | if self.center: 143 | bmp_x = (w - txt_w - bmp_w) / 2 144 | bmp_y = (h - bmp_h) / 2 145 | 146 | txt_x = (w - txt_w - bmp_w) / 2 + bmp_w 147 | txt_y = (h - txt_h) / 2 148 | else: 149 | bmp_x = self.padding[3] 150 | bmp_y = self.padding[0] 151 | 152 | txt_x = self.padding[3] + bmp_w 153 | if bmp_h > txt_h: 154 | txt_y = (bmp_h - txt_h) / 2 + self.padding[0] 155 | else: 156 | txt_y = self.padding[0] 157 | 158 | if position == "right": 159 | if self.center: 160 | bmp_x = (w - txt_w - bmp_w) / 2 + txt_w 161 | bmp_y = (h - bmp_h) / 2 162 | 163 | txt_x = (w - txt_w - bmp_w) / 2 164 | txt_y = (h - txt_h) / 2 165 | else: 166 | bmp_x = self.padding[3] + txt_w 167 | bmp_y = self.padding[0] 168 | 169 | txt_x = self.padding[3] 170 | if bmp_h > txt_h: 171 | txt_y = (bmp_h - txt_h) / 2 + self.padding[0] 172 | else: 173 | txt_y = self.padding[0] 174 | 175 | elif position == "top": 176 | if self.center: 177 | bmp_x = (w - bmp_w) / 2 178 | bmp_y = (h - bmp_h - txt_h) / 2 179 | 180 | txt_x = (w - txt_w) / 2 181 | txt_y = (h - bmp_h - txt_h) / 2 + bmp_h 182 | else: 183 | if bmp_w > txt_w: 184 | bmp_x = self.padding[3] 185 | bmp_y = self.padding[0] 186 | 187 | txt_x = (bmp_w - txt_w) / 2 + self.padding[3] 188 | txt_y = self.padding[0] + bmp_h 189 | else: 190 | bmp_x = (txt_w - bmp_w) / 2 + self.padding[3] 191 | bmp_y = self.padding[0] 192 | 193 | txt_x = self.padding[3] 194 | txt_y = self.padding[0] + bmp_h 195 | 196 | elif position == "bottom": 197 | if self.center: 198 | bmp_x = (w - bmp_w) / 2 199 | bmp_y = (h - txt_h - bmp_h) / 2 + txt_h 200 | 201 | txt_x = (w - txt_w) / 2 202 | txt_y = (h - txt_h - bmp_h) / 2 203 | else: 204 | if bmp_w > txt_w: 205 | bmp_x = self.padding[3] 206 | bmp_y = self.padding[0] + txt_h 207 | 208 | txt_x = (bmp_w - txt_w) / 2 + self.padding[3] 209 | txt_y = self.padding[0] 210 | else: 211 | bmp_x = (txt_w - bmp_w) / 2 + self.padding[3] 212 | bmp_y = self.padding[0] + txt_h 213 | 214 | txt_x = self.padding[3] 215 | txt_y = self.padding[0] 216 | 217 | dc.DrawBitmap(bmp[0], int(bmp_x), int(bmp_y)) 218 | else: 219 | if self.center: 220 | txt_x = (w - txt_w) / 2 221 | txt_y = (h - txt_h) / 2 222 | else: 223 | txt_x = self.padding[3] 224 | txt_y = self.padding[0] 225 | 226 | # Text color 227 | if self.mouse_down or self.focused or self.mouse_in: 228 | color = wx.Colour(TEXT_COLOR).ChangeLightness(120) 229 | else: 230 | color = wx.Colour(TEXT_COLOR) 231 | 232 | dc.SetTextForeground(color) 233 | 234 | # Draw text 235 | dc.DrawText(self.label, int(txt_x), int(txt_y)) 236 | 237 | def OnSetFocus(self, event): 238 | self.focused = True 239 | self.Refresh() 240 | 241 | def OnKillFocus(self, event): 242 | self.focused = False 243 | self.Refresh() 244 | 245 | def OnMouseEnter(self, event): 246 | self.mouse_in = True 247 | self.UpdateDrawing() 248 | 249 | def OnMouseLeave(self, event): 250 | self.mouse_in = False 251 | self.UpdateDrawing() 252 | 253 | def OnMouseDown(self, event): 254 | self.mouse_down = True 255 | self.SetFocus() 256 | self.UpdateDrawing() 257 | 258 | def OnMouseUp(self, event): 259 | self.mouse_down = False 260 | self.SendButtonEvent() 261 | self.UpdateDrawing() 262 | 263 | def SendButtonEvent(self): 264 | wx.PostEvent(self, button_cmd_event(id=self.GetId(), value=0)) 265 | 266 | def SetHighlighted(self, highlighted=True): 267 | self.highlighted = highlighted 268 | try: 269 | self.UpdateDrawing() 270 | except Exception: 271 | pass 272 | 273 | def DoGetBestSize(self): 274 | font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) 275 | 276 | dc = wx.ClientDC(self) 277 | dc.SetFont(font) 278 | 279 | txt_w, txt_h = dc.GetTextExtent(self.label) 280 | 281 | if self.bmp is not None: 282 | bmp = self.bmp 283 | bmp_w, bmp_h = bmp[0].GetSize() 284 | position = bmp[1] 285 | else: 286 | bmp = False 287 | 288 | if bmp: 289 | if position == "left" or position == "right": 290 | if bmp_h > txt_h: 291 | size = (self.padding[3] + bmp_w + txt_w + self.padding[1], 292 | self.padding[0] + bmp_h + self.padding[2]) 293 | else: 294 | size = (self.padding[3] + bmp_w + txt_w + self.padding[1], 295 | self.padding[0] + txt_h + self.padding[2]) 296 | else: 297 | if bmp_w > txt_w: 298 | size = (self.padding[3] + bmp_w + self.padding[1], 299 | self.padding[0] + bmp_h + txt_h + self.padding[2]) 300 | else: 301 | size = (self.padding[3] + txt_w + self.padding[1], 302 | self.padding[0] + bmp_h + txt_h + self.padding[2]) 303 | else: 304 | size = (self.padding[3] + txt_w + self.padding[1], 305 | self.padding[0] + txt_h + self.padding[2]) 306 | 307 | return wx.Size(size) 308 | -------------------------------------------------------------------------------- /gswidgetkit/checkbox.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # gswidgetkit Copyright 2021-2022 by Noah Rahm and contributors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # This file is edited code from wx.lib.checkbox 17 | # (c) 2020 by Total Control Software 18 | # 19 | # ---------------------------------------------------------------------------- 20 | 21 | import wx 22 | from .icons import (ICON_CHECKBOX_CHECKED, ICON_CHECKBOX_UNCHECKED, 23 | ICON_CHECKBOX_FOCUSED) 24 | 25 | 26 | class CheckBox(wx.Control): 27 | """ 28 | Checkbox widget for selecting boolean values. 29 | 30 | :param wx.Window `parent`: parent window. Must not be ``None``. 31 | :param integer `id`: window identifier. A value of -1 indicates a default value. 32 | :param string `label`: the displayed checkbox label. 33 | """ 34 | def __init__(self, parent, id=wx.ID_ANY, label="", pos=wx.DefaultPosition, 35 | size=wx.DefaultSize, style=wx.NO_BORDER, validator=wx.DefaultValidator, 36 | name="CheckBox"): 37 | wx.Control.__init__(self, parent, id, pos, size, style, validator, name) 38 | 39 | self.SYS_DEFAULT_GUI_FONT = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) 40 | 41 | # Initialize our cool bitmaps. 42 | self.InitializeBitmaps() 43 | 44 | # Initialize the focus pen colour/dashes, for faster drawing later. 45 | self.InitializeColours() 46 | 47 | # Set default colors 48 | self.SetBackgroundColour(parent.GetBackgroundColour()) 49 | self.SetForegroundColour(wx.Colour("#e9e9e9")) 50 | 51 | # By default, we start unchecked. 52 | self._checked = False 53 | 54 | # Set the spacing between the check bitmap and the label to 3 by default. 55 | # This can be changed using SetSpacing later. 56 | self._spacing = 6 57 | self._hasFocus = False 58 | 59 | # Ok, set the wx.PyControl label, its initial size (formerly known an 60 | # SetBestFittingSize), and inherit the attributes from the standard 61 | # wx.CheckBox . 62 | self.SetLabel(label) 63 | self.SetInitialSize(size) 64 | self.InheritAttributes() 65 | 66 | # Bind the events related to our control: first of all, we use a 67 | # combination of wx.BufferedPaintDC and an empty handler for 68 | # wx.EVT_ERASE_BACKGROUND (see later) to reduce flicker. 69 | self.Bind(wx.EVT_PAINT, self.OnPaint) 70 | # Since the paint event draws the whole widget, we will use 71 | # SetBackgroundStyle(wx.BG_STYLE_PAINT) and then 72 | # implementing an erase-background handler is not neccessary. 73 | self.SetBackgroundStyle(wx.BG_STYLE_PAINT) 74 | # Add a size handler to refresh so the paint wont smear when resizing. 75 | self.Bind(wx.EVT_SIZE, self.OnSize) 76 | 77 | # Then we want to monitor user clicks, so that we can switch our 78 | # state between checked and unchecked. 79 | self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseClick) 80 | if wx.Platform == '__WXMSW__': 81 | # MSW Sometimes does strange things... 82 | self.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseClick) 83 | 84 | # We want also to react to keyboard keys, namely the space bar that can 85 | # toggle our checked state. Whether key-up or key-down is used is based 86 | # on platform. 87 | if 'wxMSW' in wx.PlatformInfo: 88 | self.Bind(wx.EVT_KEY_UP, self.OnKeyEvent) 89 | else: 90 | self.Bind(wx.EVT_KEY_DOWN, self.OnKeyEvent) 91 | 92 | # Then, we react to focus event, because we want to draw a small 93 | # dotted rectangle around the text if we have focus. 94 | # This might be improved!!! 95 | self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) 96 | self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) 97 | 98 | 99 | def InitializeBitmaps(self): 100 | """ Initializes the check bitmaps. """ 101 | 102 | # We keep 4 bitmaps for GenCheckBox, depending on the 103 | # checking state (Checked/UnChecked) and the control 104 | # state (Enabled/Disabled). 105 | 106 | self._bitmaps = { 107 | "CheckedEnable": _GetCheckedBitmap(self), 108 | "FocusedEnable": _GetFocusedBitmap(self), 109 | "UnCheckedEnable": _GetNotCheckedBitmap(self), 110 | "CheckedDisable": _GetCheckedImage(self).ConvertToDisabled().ConvertToBitmap(), 111 | "FocusedDisable": _GetFocusedImage(self).ConvertToDisabled().ConvertToBitmap(), 112 | "UnCheckedDisable": _GetNotCheckedImage(self).ConvertToDisabled().ConvertToBitmap() 113 | } 114 | 115 | def InitializeColours(self): 116 | """ Initializes the focus indicator pen. """ 117 | 118 | textClr = self.GetForegroundColour() 119 | self.SYS_COLOUR_GRAYTEXT = wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT) 120 | 121 | def GetBitmap(self): 122 | """ 123 | Returns the appropriated bitmap depending on the checking state 124 | (Checked/UnChecked) and the control state (Enabled/Disabled). 125 | """ 126 | 127 | if self.IsEnabled(): 128 | # So we are Enabled. 129 | if self.IsChecked(): 130 | # We are Checked. 131 | return self._bitmaps["CheckedEnable"] 132 | elif self.HasFocus(): 133 | return self._bitmaps["FocusedEnable"] 134 | else: 135 | # We are UnChecked. 136 | return self._bitmaps["UnCheckedEnable"] 137 | else: 138 | # Poor GenCheckBox, Disabled and ignored! 139 | if self.IsChecked(): 140 | return self._bitmaps["CheckedDisable"] 141 | elif self.HasFocus(): 142 | return self._bitmaps["FocusedDisable"] 143 | else: 144 | return self._bitmaps["UnCheckedDisable"] 145 | 146 | def SetLabel(self, label): 147 | """ 148 | Sets the :class:`GenCheckBox` text label and updates the control's 149 | size to exactly fit the label plus the bitmap. 150 | 151 | :param `label`: Text to be displayed next to the checkbox. 152 | :type `label`: str 153 | """ 154 | 155 | wx.Control.SetLabel(self, label) 156 | 157 | # The text label has changed, so we must recalculate our best size 158 | # and refresh ourselves. 159 | self.InvalidateBestSize() 160 | self.Refresh() 161 | 162 | def SetFont(self, font): 163 | """ 164 | Sets the :class:`GenCheckBox` text font and updates the control's 165 | size to exactly fit the label plus the bitmap. 166 | 167 | :param `font`: Font to be used to render the checkboxs label. 168 | :type `font`: `wx.Font` 169 | """ 170 | 171 | wx.Control.SetFont(self, font) 172 | 173 | # The font for text label has changed, so we must recalculate our best 174 | # size and refresh ourselves. 175 | self.InvalidateBestSize() 176 | self.Refresh() 177 | 178 | def DoGetBestSize(self): 179 | """ 180 | Overridden base class virtual. Determines the best size of the control 181 | based on the label size, the bitmap size and the current font. 182 | """ 183 | 184 | # Retrieve our properties: the text label, the font and the check 185 | # bitmap. 186 | label = self.GetLabel() 187 | font = self.GetFont() 188 | bitmap = self.GetBitmap() 189 | 190 | if not font: 191 | # No font defined? So use the default GUI font provided by the system. 192 | font = self.SYS_DEFAULT_GUI_FONT 193 | 194 | # Set up a wx.ClientDC. When you don't have a dc available (almost 195 | # always you don't have it if you are not inside a wx.EVT_PAINT event), 196 | # use a wx.ClientDC (or a wx.MemoryDC) to measure text extents. 197 | dc = wx.ClientDC(self) 198 | dc.SetFont(font) 199 | 200 | # Measure our label. 201 | textWidth, textHeight = dc.GetTextExtent(label) 202 | 203 | # Retrieve the check bitmap dimensions. 204 | bitmapWidth, bitmapHeight = bitmap.GetWidth(), bitmap.GetHeight() 205 | 206 | # Get the spacing between the check bitmap and the text. 207 | spacing = self.GetSpacing() 208 | 209 | # Ok, we're almost done: the total width of the control is simply 210 | # the sum of the bitmap width, the spacing and the text width, 211 | # while the height is the maximum value between the text width and 212 | # the bitmap width. 213 | totalWidth = bitmapWidth + spacing + textWidth 214 | totalHeight = max(textHeight, bitmapHeight) 215 | 216 | best = wx.Size(totalWidth, totalHeight) 217 | 218 | # Cache the best size so it doesn't need to be calculated again, 219 | # at least until some properties of the window change. 220 | self.CacheBestSize(best) 221 | 222 | return best 223 | 224 | def AcceptsFocusFromKeyboard(self): 225 | """ Overridden base class virtual. """ 226 | 227 | # We can accept focus from keyboard, obviously. 228 | return True 229 | 230 | def AcceptsFocus(self): 231 | """ Overridden base class virtual. """ 232 | 233 | # If it seems that wx.CheckBox does not accept focus with mouse, It does. 234 | # You just can't see the focus rectangle until there's 235 | # another keypress or navigation event (at least on some platforms.) 236 | return True # This will draw focus rectangle always on mouse click. 237 | 238 | def HasFocus(self): 239 | """ Returns whether or not we have the focus. """ 240 | 241 | # We just returns the _hasFocus property that has been set in the 242 | # wx.EVT_SET_FOCUS and wx.EVT_KILL_FOCUS event handlers. 243 | return self._hasFocus 244 | 245 | def SetForegroundColour(self, colour): 246 | """ 247 | Overridden base class virtual. 248 | 249 | :param `colour`: Set the foreground colour of the checkboxs label. 250 | :type `colour`: `wx.Colour` 251 | """ 252 | 253 | wx.Control.SetForegroundColour(self, colour) 254 | 255 | # We have to re-initialize the focus indicator per colour as it should 256 | # always be the same as the foreground colour. 257 | self.InitializeColours() 258 | self.Refresh() 259 | 260 | def SetBackgroundColour(self, colour): 261 | """ 262 | Overridden base class virtual. 263 | 264 | :param `colour`: Set the background colour of the checkbox. 265 | :type `colour`: `wx.Colour` 266 | """ 267 | 268 | wx.Control.SetBackgroundColour(self, colour) 269 | 270 | # We have to refresh ourselves. 271 | self.Refresh() 272 | 273 | def Enable(self, enable=True): 274 | """ 275 | Enables/Disables :class:`GenCheckBox`. 276 | 277 | :param `enable`: Set the enabled state of the checkbox. 278 | :type `enable`: bool 279 | """ 280 | 281 | wx.Control.Enable(self, enable) 282 | 283 | # We have to refresh ourselves, as our state changed. 284 | self.Refresh() 285 | 286 | def GetDefaultAttributes(self): 287 | """ 288 | Overridden base class virtual. By default we should use 289 | the same font/colour attributes as the native wx.CheckBox. 290 | """ 291 | 292 | return wx.CheckBox.GetClassDefaultAttributes() 293 | 294 | def ShouldInheritColours(self): 295 | """ 296 | Overridden base class virtual. If the parent has non-default 297 | colours then we want this control to inherit them. 298 | """ 299 | 300 | return True 301 | 302 | def SetSpacing(self, spacing): 303 | """ 304 | Sets a new spacing between the check bitmap and the text. 305 | 306 | :param `spacing`: Set the amount of space between the checkboxs bitmap and text. 307 | :type `spacing`: int 308 | """ 309 | 310 | self._spacing = spacing 311 | 312 | # The spacing between the check bitmap and the text has changed, 313 | # so we must recalculate our best size and refresh ourselves. 314 | self.InvalidateBestSize() 315 | self.Refresh() 316 | 317 | def GetSpacing(self): 318 | """ Returns the spacing between the check bitmap and the text. """ 319 | 320 | return self._spacing 321 | 322 | def GetValue(self): 323 | """ 324 | Returns the state of :class:`GenCheckBox`, True if checked, False 325 | otherwise. 326 | """ 327 | 328 | return self._checked 329 | 330 | def IsChecked(self): 331 | """ 332 | This is just a maybe more readable synonym for GetValue: just as the 333 | latter, it returns True if the :class:`GenCheckBox` is checked and False 334 | otherwise. 335 | """ 336 | 337 | return self._checked 338 | 339 | def SetValue(self, state): 340 | """ 341 | Sets the :class:`GenCheckBox` to the given state. This does not cause a 342 | ``wx.wxEVT_COMMAND_CHECKBOX_CLICKED`` event to get emitted. 343 | 344 | :param `state`: Set the value of the checkbox. True or False. 345 | :type `state`: bool 346 | """ 347 | 348 | self._checked = state 349 | 350 | # Refresh ourselves: the bitmap has changed. 351 | self.Refresh() 352 | 353 | def OnKeyEvent(self, event): 354 | """ 355 | Handles the ``wx.EVT_KEY_UP`` or ``wx.EVT_KEY_UP`` event (depending on 356 | platform) for :class:`GenCheckBox`. 357 | 358 | :param `event`: A `wx.KeyEvent` to be processed. 359 | :type `event`: `wx.KeyEvent` 360 | """ 361 | 362 | if event.GetKeyCode() == wx.WXK_SPACE: 363 | # The spacebar has been pressed: toggle our state. 364 | self.SendCheckBoxEvent() 365 | else: 366 | event.Skip() 367 | 368 | def OnSetFocus(self, event): 369 | """ 370 | Handles the ``wx.EVT_SET_FOCUS`` event for :class:`GenCheckBox`. 371 | 372 | :param `event`: A `wx.FocusEvent` to be processed. 373 | :type `event`: `wx.FocusEvent` 374 | """ 375 | 376 | self._hasFocus = True 377 | 378 | # We got focus, and we want a dotted rectangle to be painted 379 | # around the checkbox label, so we refresh ourselves. 380 | self.Refresh() 381 | 382 | def OnKillFocus(self, event): 383 | """ 384 | Handles the ``wx.EVT_KILL_FOCUS`` event for :class:`GenCheckBox`. 385 | 386 | :param `event`: A `wx.FocusEvent` to be processed. 387 | :type `event`: `wx.FocusEvent` 388 | """ 389 | 390 | self._hasFocus = False 391 | 392 | # We lost focus, and we want a dotted rectangle to be cleared 393 | # around the checkbox label, so we refresh ourselves. 394 | self.Refresh() 395 | 396 | def OnSize(self, event): 397 | """ 398 | Handles the ``wx.EVT_SIZE`` event for :class:`GenCheckBox`. 399 | 400 | :param `event`: A `wx.SizeEvent` to be processed. 401 | :type `event`: `wx.SizeEvent` 402 | """ 403 | self.Refresh() 404 | 405 | def OnPaint(self, event): 406 | """ 407 | Handles the ``wx.EVT_PAINT`` event for :class:`GenCheckBox`. 408 | 409 | :param `event`: A `wx.PaintEvent` to be processed. 410 | :type `event`: `wx.PaintEvent` 411 | """ 412 | 413 | # If you want to reduce flicker, a good starting point is to 414 | # use wx.BufferedPaintDC . 415 | # wx.AutoBufferedPaintDC would be marginally better. 416 | dc = wx.AutoBufferedPaintDC(self) 417 | 418 | # Is is advisable that you don't overcrowd the OnPaint event 419 | # (or any other event) with a lot of code, so let's do the 420 | # actual drawing in the Draw() method, passing the newly 421 | # initialized wx.AutoBufferedPaintDC . 422 | self.Draw(dc) 423 | 424 | def Draw(self, dc): 425 | """ 426 | Actually performs the drawing operations, for the bitmap and 427 | for the text, positioning them centered vertically. 428 | 429 | :param `dc`: device context to use. 430 | :type `dc`: `wx.DC` 431 | """ 432 | 433 | # Get the actual client size of ourselves. 434 | width, height = self.GetClientSize() 435 | 436 | if not width or not height: 437 | # Nothing to do, we still don't have dimensions! 438 | return 439 | 440 | # Initialize the wx.BufferedPaintDC, assigning a background 441 | # colour and a foreground colour (to draw the text). 442 | backColour = self.GetBackgroundColour() 443 | backBrush = wx.Brush(backColour, wx.SOLID) 444 | dc.SetBackground(backBrush) 445 | dc.Clear() 446 | 447 | if self.IsEnabled(): 448 | dc.SetTextForeground(self.GetForegroundColour()) 449 | else: 450 | dc.SetTextForeground(self.SYS_COLOUR_GRAYTEXT) 451 | 452 | dc.SetFont(self.GetFont()) 453 | 454 | # Get the text label for the checkbox, the associated check bitmap 455 | # and the spacing between the check bitmap and the text. 456 | label = self.GetLabel() 457 | bitmap = self.GetBitmap() 458 | spacing = self.GetSpacing() 459 | 460 | # Measure the text extent and get the check bitmap dimensions. 461 | textWidth, textHeight = dc.GetTextExtent(label) 462 | bitmapWidth, bitmapHeight = bitmap.GetWidth(), bitmap.GetHeight() 463 | 464 | # Position the bitmap centered vertically. 465 | bitmapXpos = 0 466 | bitmapYpos = (height - bitmapHeight) // 2 467 | 468 | # Position the text centered vertically. 469 | textXpos = bitmapWidth + spacing 470 | textYpos = (height - textHeight) // 2 471 | 472 | # Draw the bitmap on the DC. 473 | try: 474 | dc.DrawBitmap(bitmap, bitmapXpos, bitmapYpos, True) 475 | except Exception as exc: # bitmap might be image and need converted. Ex: if disabled. 476 | dc.DrawBitmap(bitmap.ConvertToBitmap(), bitmapXpos, bitmapYpos, True) 477 | 478 | # Draw the text 479 | dc.DrawText(label, textXpos, textYpos) 480 | 481 | # Let's see if we have keyboard focus and, if this is the case, 482 | # we draw it a little lighter. 483 | if self.HasFocus(): 484 | dc.DrawBitmap(bitmap, bitmapXpos, bitmapYpos, True) 485 | 486 | def OnMouseClick(self, event): 487 | """ 488 | Handles the ``wx.EVT_LEFT_DOWN`` event for :class:`GenCheckBox`. 489 | 490 | :param `event`: A `wx.MouseEvent` to be processed. 491 | :type `event`: `wx.MouseEvent` 492 | """ 493 | 494 | if not self.IsEnabled(): 495 | # Nothing to do, we are disabled. 496 | return 497 | 498 | self.SendCheckBoxEvent() 499 | event.Skip() 500 | 501 | def SendCheckBoxEvent(self): 502 | """ Actually sends the wx.wxEVT_COMMAND_CHECKBOX_CLICKED event. """ 503 | 504 | # This part of the code may be reduced to a 3-liner code 505 | # but it is kept for better understanding the event handling. 506 | # If you can, however, avoid code duplication; in this case, 507 | # I could have done: 508 | # 509 | # self._checked = not self.IsChecked() 510 | # checkEvent = wx.CommandEvent(wx.wxEVT_COMMAND_CHECKBOX_CLICKED, 511 | # self.GetId()) 512 | # checkEvent.SetInt(int(self._checked)) 513 | if self.IsChecked(): 514 | 515 | # We were checked, so we should become unchecked. 516 | self._checked = False 517 | 518 | # Fire a wx.CommandEvent: this generates a 519 | # wx.wxEVT_COMMAND_CHECKBOX_CLICKED event that can be caught by the 520 | # developer by doing something like: 521 | # MyCheckBox.Bind(wx.EVT_CHECKBOX, self.OnCheckBox) 522 | checkEvent = wx.CommandEvent(wx.wxEVT_COMMAND_CHECKBOX_CLICKED, 523 | self.GetId()) 524 | 525 | # Set the integer event value to 0 (we are switching to unchecked state). 526 | checkEvent.SetInt(0) 527 | 528 | else: 529 | 530 | # We were unchecked, so we should become checked. 531 | self._checked = True 532 | 533 | checkEvent = wx.CommandEvent(wx.wxEVT_COMMAND_CHECKBOX_CLICKED, 534 | self.GetId()) 535 | 536 | # Set the integer event value to 1 (we are switching to checked state). 537 | checkEvent.SetInt(1) 538 | 539 | # Set the originating object for the event (ourselves). 540 | checkEvent.SetEventObject(self) 541 | 542 | # Watch for a possible listener of this event that will catch it and 543 | # eventually process it. 544 | self.GetEventHandler().ProcessEvent(checkEvent) 545 | 546 | # Refresh ourselves: the bitmap has changed. 547 | self.Refresh() 548 | 549 | 550 | def _GetCheckedBitmap(self): 551 | return ICON_CHECKBOX_CHECKED.GetBitmap() 552 | 553 | def _GetCheckedImage(self): 554 | return _GetCheckedBitmap(self).ConvertToImage() 555 | 556 | def _GetFocusedBitmap(self): 557 | return ICON_CHECKBOX_FOCUSED.GetBitmap() 558 | 559 | def _GetFocusedImage(self): 560 | return _GetFocusedBitmap(self).ConvertToImage() 561 | 562 | def _GetNotCheckedBitmap(self): 563 | return ICON_CHECKBOX_UNCHECKED.GetBitmap() 564 | 565 | def _GetNotCheckedImage(self): 566 | return _GetNotCheckedBitmap(self).ConvertToImage() 567 | 568 | 569 | def _GrayOut(anImage): 570 | """ 571 | Convert the given image (in place) to a grayed-out version, 572 | appropriate for a 'disabled' appearance. 573 | 574 | :param `anImage`: A `wx.Image` to gray out. 575 | :type `anImage`: `wx.Image` 576 | :rtype: `wx.Bitmap` 577 | """ 578 | 579 | factor = 0.7 # 0 < f < 1. Higher Is Grayer 580 | 581 | if anImage.HasMask(): 582 | maskColor = (anImage.GetMaskRed(), anImage.GetMaskGreen(), anImage.GetMaskBlue()) 583 | else: 584 | maskColor = None 585 | 586 | data = map(ord, list(anImage.GetData())) 587 | 588 | for i in range(0, len(data), 3): 589 | pixel = (data[i], data[i + 1], data[i + 2]) 590 | pixel = _MakeGray(pixel, factor, maskColor) 591 | 592 | for x in range(3): 593 | data[i + x] = pixel[x] 594 | 595 | anImage.SetData(''.join(map(chr, data))) 596 | 597 | return anImage.ConvertToBitmap() 598 | 599 | 600 | def _MakeGray(rgbTuple, factor, maskColor): 601 | """ 602 | Make a pixel grayed-out. If the pixel matches the maskcolor, it won't be 603 | changed. 604 | 605 | :type `rgbTuple`: red, green, blue 3-tuple 606 | :type `factor`: float 607 | :type `maskColor`: red, green, blue 3-tuple 608 | """ 609 | r, g, b = rgbTuple 610 | if (r, g, b) != maskColor: 611 | return map(lambda x: int((230 - x) * factor) + x, (r, g, b)) 612 | else: 613 | return (r, g, b) 614 | -------------------------------------------------------------------------------- /gswidgetkit/color_picker.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # gswidgetkit Copyright 2021-2022 by Noah Rahm and contributors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ---------------------------------------------------------------------------- 16 | 17 | import wx 18 | from wx.lib.newevent import NewCommandEvent 19 | import wx.lib.agw.cubecolourdialog as colordialog 20 | 21 | from .constants import TEXT_COLOR 22 | from .icons import ICON_BRUSH_CHECKERBOARD 23 | 24 | button_cmd_event, EVT_COLORPICKER_BUTTON = NewCommandEvent() 25 | 26 | 27 | class ColorPickerButton(wx.Control): 28 | """ 29 | Color picker widget for selecting an RGBA color. 30 | 31 | :param wx.Window `parent`: parent window. Must not be ``None``. 32 | :param integer `id`: window identifier. A value of -1 indicates a default value. 33 | :param string `label`: the label displayed beside the color select button. 34 | :param tuple `default`: tuple of the default RGBA color. 35 | """ 36 | def __init__(self, parent, id=wx.ID_ANY, label="", default=(213, 219, 213, 177), 37 | pos=wx.DefaultPosition, size=wx.Size(400, -1), style=wx.NO_BORDER, 38 | *args, **kwargs): 39 | wx.Control.__init__(self, parent, id, pos, size, style, *args, **kwargs) 40 | 41 | self.parent = parent 42 | 43 | self.cur_color = default 44 | 45 | self.label = label 46 | self.padding = (5, 10, 5, 10) 47 | 48 | self.buffer = None 49 | self.size = None 50 | 51 | self.mouse_in = False 52 | self.mouse_down = False 53 | self.focused = False 54 | 55 | self.Bind(wx.EVT_PAINT, self.OnPaint) 56 | self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None) 57 | self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) 58 | self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) 59 | self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave) 60 | self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnter) 61 | self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown) 62 | self.Bind(wx.EVT_LEFT_UP, self.OnMouseUp) 63 | self.Bind(wx.EVT_SIZE, self.OnSize) 64 | 65 | def OnPaint(self, event): 66 | wx.BufferedPaintDC(self, self.buffer) 67 | 68 | def OnSize(self, event): 69 | size = self.GetClientSize() 70 | 71 | # Make sure size is at least 1px to avoid 72 | # strange "invalid bitmap size" errors. 73 | if size[0] < 1: 74 | size = (1, 1) 75 | self.buffer = wx.Bitmap(*size) 76 | self.UpdateDrawing() 77 | 78 | def UpdateDrawing(self): 79 | dc = wx.MemoryDC() 80 | dc.SelectObject(self.buffer) 81 | dc = wx.GCDC(dc) 82 | self.OnDrawBackground(dc) 83 | self.OnDrawWidget(dc) 84 | del dc # need to get rid of the MemoryDC before Update() is called. 85 | self.Refresh() 86 | self.Update() 87 | 88 | def OnDrawBackground(self, dc): 89 | dc.SetBackground(wx.Brush(self.parent.GetBackgroundColour())) 90 | dc.Clear() 91 | 92 | def OnDrawWidget(self, dc): 93 | fnt = self.parent.GetFont() 94 | dc.SetFont(fnt) 95 | dc.SetPen(wx.TRANSPARENT_PEN) 96 | 97 | w, h = self.GetSize() 98 | 99 | txt_w, txt_h = dc.GetTextExtent(self.label) 100 | 101 | txt_x = self.padding[3] 102 | txt_y = self.padding[0] 103 | 104 | txt_w = txt_w + self.padding[1] + self.padding[3] 105 | 106 | dc.SetBrush(wx.Brush(ICON_BRUSH_CHECKERBOARD.GetBitmap())) 107 | dc.DrawRoundedRectangle(txt_w, 0, w-txt_w, h, 4) 108 | 109 | dc.SetBrush(wx.Brush(wx.Colour(self.cur_color))) 110 | dc.DrawRoundedRectangle(txt_w, 0, w-txt_w, h, 4) 111 | 112 | # Draw text 113 | if self.mouse_down or self.focused or self.mouse_in: 114 | color = wx.Colour(TEXT_COLOR).ChangeLightness(120) 115 | else: 116 | color = wx.Colour(TEXT_COLOR) 117 | dc.SetTextForeground(color) 118 | 119 | dc.DrawText(self.label, int(txt_x), int(txt_y)) 120 | 121 | def OnSetFocus(self, event): 122 | self.focused = True 123 | self.Refresh() 124 | 125 | def OnKillFocus(self, event): 126 | self.focused = False 127 | self.Refresh() 128 | 129 | def OnMouseEnter(self, event): 130 | self.mouse_in = True 131 | self.UpdateDrawing() 132 | 133 | def OnMouseLeave(self, event): 134 | self.mouse_in = False 135 | self.UpdateDrawing() 136 | 137 | def OnMouseDown(self, event): 138 | self.mouse_down = True 139 | self.SetFocus() 140 | self.UpdateDrawing() 141 | 142 | def OnMouseUp(self, event): 143 | self.mouse_down = False 144 | self.ShowDialog() 145 | self.SendButtonEvent() 146 | self.UpdateDrawing() 147 | 148 | def SendButtonEvent(self): 149 | wx.PostEvent(self, button_cmd_event(id=self.GetId(), value=self.cur_color)) 150 | 151 | def ShowDialog(self): 152 | self.color_data = wx.ColourData() 153 | self.color_data.SetColour(self.cur_color) 154 | self.color_dialog = colordialog.CubeColourDialog(None, self.color_data) 155 | if self.color_dialog.ShowModal() == wx.ID_OK: 156 | self.color_data = self.color_dialog.GetColourData() 157 | self.cur_color = self.color_data.GetColour() 158 | self.color_dialog.Destroy() 159 | 160 | def DoGetBestSize(self): 161 | font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) 162 | 163 | dc = wx.ClientDC(self) 164 | dc.SetFont(font) 165 | 166 | txt_w, txt_h = dc.GetTextExtent(self.label) 167 | 168 | size = (self.padding[3] + txt_w + self.padding[1], 169 | self.padding[0] + txt_h + self.padding[2]) 170 | 171 | return wx.Size(size) 172 | -------------------------------------------------------------------------------- /gswidgetkit/constants.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # gswidgetkit Copyright 2021-2022 by Noah Rahm and contributors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ---------------------------------------------------------------------------- 16 | 17 | 18 | TEXT_COLOR = "#dfdfdf" 19 | ACCENT_COLOR = "#3D7DC5" 20 | 21 | DROPDOWN_BG_COLOR = "#1B2226" 22 | NUMBERFIELD_BG_COLOR = "#1B2226" 23 | BUTTON_BG_COLOR = "#1B2226" 24 | TEXTCTRL_BG_COLOR = "#1B2226" 25 | TEXTCTRL_BORDER_COLOR = "#111517" 26 | FOLDPANEL_BG_COLOR = "#1E2429" 27 | -------------------------------------------------------------------------------- /gswidgetkit/dropdown.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # gswidgetkit Copyright 2021-2022 by Noah Rahm and contributors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ---------------------------------------------------------------------------- 16 | 17 | import wx 18 | import wx.lib.agw.flatmenu as flatmenu 19 | from wx.lib.newevent import NewCommandEvent 20 | 21 | from .constants import ACCENT_COLOR, TEXT_COLOR, DROPDOWN_BG_COLOR 22 | from .utils import GetTextExtent 23 | from .icons import ICON_DROPDOWN_ARROW 24 | 25 | # Max number of items that can be added to the menu is 100 26 | DROPDOWNMENU_ITEM_IDS = wx.NewIdRef(100) 27 | 28 | # Create new event for selecting dropdown item 29 | dropdown_cmd_event, EVT_DROPDOWN = NewCommandEvent() 30 | 31 | 32 | class DropDown(wx.Control): 33 | """ 34 | Dropdown widget for selecting a value from a list of choices. 35 | 36 | :param wx.Window `parent`: parent window. Must not be ``None``. 37 | :param integer `id`: window identifier. A value of -1 indicates a default value. 38 | :param list `items`: the list of items in the dropdown. 39 | :param `default`: the default selected item in the dropdown. Must exist in `items`. 40 | """ 41 | def __init__(self, parent, items, default, id=wx.ID_ANY, 42 | pos=wx.DefaultPosition, size=wx.DefaultSize, 43 | style=wx.NO_BORDER, *args,**kwargs): 44 | wx.Control.__init__(self, parent, id, pos, size, style, *args, **kwargs) 45 | self.parent = parent 46 | self.items = items 47 | self.default = default 48 | self.value = default 49 | self.longest_str = max(items, key=len) # Longest string in the choices 50 | 51 | self.buffer = None 52 | self.padding_x = 20 53 | self.padding_y = 10 54 | 55 | self.mouse_in = False 56 | self.mouse_down = False 57 | self.focused = False 58 | 59 | self.menuidmapping = {} 60 | 61 | # Init menu 62 | self.CreateMenu() 63 | 64 | self.Bind(wx.EVT_PAINT, self.OnPaint) 65 | self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None) 66 | self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) 67 | self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) 68 | self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave) 69 | self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnter) 70 | self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown) 71 | self.Bind(wx.EVT_LEFT_UP, self.OnMouseUp) 72 | self.Bind(wx.EVT_SIZE, self.OnSize) 73 | 74 | def OnPaint(self, event): 75 | wx.BufferedPaintDC(self, self.buffer) 76 | 77 | def OnSize(self, event): 78 | size = self.GetClientSize() 79 | 80 | # Make sure size is at least 1px to avoid 81 | # strange "invalid bitmap size" errors. 82 | if size[0] < 1: 83 | size = (1, 1) 84 | self.buffer = wx.Bitmap(*size) 85 | self.UpdateDrawing() 86 | 87 | def UpdateDrawing(self): 88 | dc = wx.MemoryDC() 89 | dc.SelectObject(self.buffer) 90 | dc = wx.GCDC(dc) 91 | self.OnDrawBackground(dc) 92 | self.OnDrawWidget(dc) 93 | del dc # need to get rid of the MemoryDC before Update() is called. 94 | self.Refresh() 95 | self.Update() 96 | 97 | def OnDrawBackground(self, dc): 98 | dc.SetBackground(wx.Brush(self.parent.GetBackgroundColour())) 99 | dc.Clear() 100 | 101 | def OnDrawWidget(self, dc): 102 | fnt = self.parent.GetFont() 103 | dc.SetFont(fnt) 104 | dc.SetTextForeground(TEXT_COLOR) 105 | dc.SetPen(wx.TRANSPARENT_PEN) 106 | 107 | w, h = self.GetSize() 108 | lbl_w, lbl_h = GetTextExtent(self.GetValue()) 109 | 110 | if self.mouse_down: 111 | bg_color = wx.Colour(ACCENT_COLOR) 112 | elif self.mouse_in: 113 | bg_color = wx.Colour(DROPDOWN_BG_COLOR) 114 | else: 115 | bg_color = wx.Colour(DROPDOWN_BG_COLOR).ChangeLightness(85) 116 | 117 | dc.SetBrush(wx.Brush(bg_color)) 118 | dc.DrawRoundedRectangle(0, 0, w, h, 4) 119 | dc.DrawText(self.GetValue(), self.padding_x, int((h/2) - (lbl_h/2))) 120 | dc.DrawBitmap(ICON_DROPDOWN_ARROW.GetBitmap(), (w-28), int((h/2) - (lbl_h/2) - 2)) 121 | 122 | def OnSetFocus(self, event): 123 | self.focused = True 124 | self.Refresh() 125 | 126 | def OnKillFocus(self, event): 127 | self.focused = False 128 | self.Refresh() 129 | 130 | def OnMouseEnter(self, event): 131 | self.mouse_in = True 132 | self.UpdateDrawing() 133 | 134 | def OnMouseLeave(self, event): 135 | self.mouse_in = False 136 | self.UpdateDrawing() 137 | 138 | def OnMouseDown(self, event): 139 | self.mouse_down = True 140 | self.SetFocus() 141 | self.UpdateDrawing() 142 | 143 | def OnMouseUp(self, event): 144 | self.mouse_down = False 145 | self.OnClick() 146 | self.UpdateDrawing() 147 | 148 | def DoGetBestSize(self): 149 | """ 150 | Overridden base class virtual. Determines the best size of the control 151 | based on the label size, the bitmap size and the current font. 152 | """ 153 | font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) 154 | dc = wx.ClientDC(self) 155 | dc.SetFont(font) 156 | 157 | val_text_w, val_text_h = dc.GetTextExtent(self.longest_str) 158 | 159 | totalwidth = val_text_w + self.padding_x + 65 160 | 161 | totalheight = val_text_h + self.padding_y 162 | 163 | best = wx.Size(totalwidth, totalheight) 164 | 165 | # Cache the best size so it doesn't need to be calculated again, 166 | # at least until some properties of the window change 167 | self.CacheBestSize(best) 168 | 169 | return best 170 | 171 | def SetValue(self, value): 172 | self.value = value 173 | 174 | def GetValue(self): 175 | return self.value 176 | 177 | def ComputeMenuPos(self): 178 | y = self.GetSize()[1] + self.GetScreenPosition()[1] + 1 179 | x = self.GetScreenPosition()[0] 180 | return wx.Point(x, y) 181 | 182 | def OnClick(self): 183 | # Set the size of the menu 184 | w, h = self.GetSize() 185 | self.dropdown_menu._menuWidth = w 186 | h = (self.dropdown_menu._itemHeight)*(len(self.items))+4 187 | self.dropdown_menu.SetSize(wx.Size(w, h)) 188 | 189 | # Popup the menu 190 | pos = self.ComputeMenuPos() 191 | self.dropdown_menu.Popup(pos, self) 192 | 193 | def SendDropdownSelectEvent(self): 194 | wx.PostEvent(self, dropdown_cmd_event(id=self.GetId(), value=self.GetValue())) 195 | 196 | def CreateMenu(self): 197 | self.dropdown_menu = flatmenu.FlatMenu() 198 | 199 | # Make the margin width to be the same as in the dropdown button 200 | # and the margin height to be consistent with standards. 201 | self.dropdown_menu._marginWidth = 10 202 | self.dropdown_menu._marginHeight = 26 203 | 204 | i = 0 205 | for item in self.items: 206 | menu_item = flatmenu.FlatMenuItem(self.dropdown_menu, 207 | DROPDOWNMENU_ITEM_IDS[i], 208 | item, "", wx.ITEM_NORMAL) 209 | self.dropdown_menu.AppendItem(menu_item) 210 | self.menuidmapping[DROPDOWNMENU_ITEM_IDS[i]] = item 211 | 212 | self.Bind(wx.EVT_MENU, self.OnSelectMenuItem, id=DROPDOWNMENU_ITEM_IDS[i]) 213 | i += 1 214 | 215 | def OnSelectMenuItem(self, event): 216 | self.SetValue(self.menuidmapping[event.GetId()]) 217 | self.SendDropdownSelectEvent() 218 | self.UpdateDrawing() 219 | -------------------------------------------------------------------------------- /gswidgetkit/icons/__init__.py: -------------------------------------------------------------------------------- 1 | from .icons import * 2 | -------------------------------------------------------------------------------- /gswidgetkit/icons/icons.py: -------------------------------------------------------------------------------- 1 | from wx.lib.embeddedimage import PyEmbeddedImage 2 | #-------------------- 3 | ICON_BRUSH_CHECKERBOARD = PyEmbeddedImage( 4 | b'iVBORw0KGgoAAAANSUhEUgAAAGQAAABkAQMAAABKLAcXAAAABlBMVEVfYWQnLjUoH6AuAAAA' 5 | b'KElEQVQ4y2NgsP/A/B9GDDDv/wH+PzCCYWB5o+EyGi6j4TIaLrTiAQDYaoSnfkBkDwAAAABJ' 6 | b'RU5ErkJggg==') 7 | 8 | #---------------------------------------------------------------------- 9 | ICON_CHECKBOX_CHECKED = PyEmbeddedImage( 10 | b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAVJJ' 11 | b'REFUOI2d0M8rg3EcwPH38x3b2hDRgyKmZn49y8pBmZLkJiQrSubiIJGd2fMc3Z3HUVHyD4jk' 12 | b'DzA5uNly3IgpPGabw9Z4ahvb5/rp9f7+kOa1S78EKtBJeRMBtoQArQJM1mRUkYGOCnBupEFR' 13 | b'CXM0vFNrTgFQdmDc08LuxiSBBU/5Aa8iszrlJPais3dyZwzYrVUl8ahbZn3WRTyho+6HeXr9' 14 | b'/AksTjhQ/W5slsKR4b4m1qa7iSd0gqFrHhN6ficAkl9pOprt7CwrWM0mAx5yNbI518Pj6yfB' 15 | b'0HX+ZEPg+CLK6dUDXa01bC8pWKpFHgd8vUUxgKl/bEUDuL1/RggJryLjbKvjQ09lcUJHO7gp' 16 | b'iAEMjz46j2ISEjPedgYc9cRejB/2ZwDg8CwCwIgiF73275F82mWm0CKdfENU20piACFBtODi' 17 | b'HxgIizRoxSKlJme0b6+FcOtiAVh9AAAAAElFTkSuQmCC') 18 | 19 | #---------------------------------------------------------------------- 20 | ICON_CHECKBOX_FOCUSED = PyEmbeddedImage( 21 | b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADC3pUWHRSYXcgcHJvZmlsZSB0' 22 | b'eXBlIGV4aWYAAHja7ZZbkuQoDEX/WUUvAUkIieXwMBGzg1l+X/yqyqyq7o6p/ppIEwZSxheh' 23 | b'I3CG7d9/ZviBi4qlkNQ8l5wjrlRS4YqOx+Oqe00x7fX5I16dB3u4HzBMglaOn57P8Zed4oMS' 24 | b'VfT0nZD380F7fFDSqe9PQnw0sjxa/XEKlVNI+HhAp0A9lhVzcXu/hLYd7bhW4scdVpX80e0P' 25 | b'vw3RG4p5hHkTkoiahQ8HZN0UpOIBoY7ia6DI3j/qa6kIyGdxiu+8Cs9U7h59YX+CIvmwBxge' 26 | b'g5nv9lM76ZP9FAx7iN/NLP2e+cFu6Z7iIcjrnnN4mHM7VldTRkjzuahrKXsPAxtCLvtrGcVw' 27 | b'K/q2l4LiAdnbgXzEHhtKp0IMLJMSDao0advbTh0uJt7Y0DJ3gFo2F+PCXWIAsbQKTTYpMsRB' 28 | b'rgOvwMq3L7TPW/bpOjkmHoSRTBCjlQphVX+jfCk050p5ouh3rOAXrySEG4vcqjEKQGheeaR7' 29 | b'gK/yfC2uAoK6h9mxwBrbIdGUztxaeSQ7aMFARXvsNbJxCiBEmFvhDHZAophJlDJFYzYixNHB' 30 | b'p0LIWRI3ICBVHvCSk0gGHOwYzI13jPaxrHyYcWYBhEoWA5oiFazWwYb8seTIoaqiSVWzmroW' 31 | b'rVlyyppztrwOv2piydSymbkVqy6eXD27uQcvXgsXweGoJRcrXkqpFZNWKFe8XTGg1sZNWmra' 32 | b'crPmrbTakT49de25W/fQS6+DhwycEyMPGz7KqBttSKUtbbrlzTbfylYnUm3KTFNnnjZ9lllv' 33 | b'ahQOrB/Kn1OjixrvpNZAu6nhVbNLgtZxoosZiHEiELdFAAnNi1l0SonDQreYxbLONGV4qQvO' 34 | b'oEUMBNNGrJNudm/kHriFlL7FjS9yYaH7G+TCQvcFuY/cPqE21temRwk7obUNV1CjYPt1+OJ1' 35 | b'fc9aUkr4ACBgqxejft2G3w34ZVveZgrpW0pv7UvoJfQSegm9hF5C/x+htn9p16fy2y6F//Yi' 36 | b'/qnPUcJPiaVBoB5++ksAAAGGaUNDUElDQyBwcm9maWxlAAB4nH2RO0jDUBSG/6YWXxUHO4g4' 37 | b'ZKjiYEFUxFGqWAQLpa3QqoPJTV/QpCFJcXEUXAsOPharDi7Oujq4CoLgA8TNzUnRRUo8Nym0' 38 | b'iPHA5X789/w/954LCPUyU82OCUDVLCMZi4qZ7KrY+YpedCMAH8YkZurx1GIanvV1T31UdxGe' 39 | b'5d33Z/UpOZMBPpF4jumGRbxBPLNp6Zz3iUOsKCnE58TjBl2Q+JHrsstvnAsOCzwzZKST88Qh' 40 | b'YrHQxnIbs6KhEk8ThxVVo3wh47LCeYuzWq6y5j35C4M5bSXFdVrDiGEJcSQgQkYVJZRhIUK7' 41 | b'RoqJJJ1HPfxDjj9BLplcJTByLKACFZLjB/+D37M181OTblIwCgRebPtjBOjcBRo12/4+tu3G' 42 | b'CeB/Bq60lr9SB2Y/Sa+1tPAR0L8NXFy3NHkPuNwBBp90yZAcyU9LyOeB9zP6piwwcAv0rLlz' 43 | b'a57j9AFI06yWb4CDQ2C0QNnrHu/uap/bvz3N+f0AMZVyjczf1CsAAA+caVRYdFhNTDpjb20u' 44 | b'YWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJl' 45 | b'U3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4Onht' 46 | b'cHRrPSJYTVAgQ29yZSA0LjQuMC1FeGl2MiI+CiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6' 47 | b'Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogIDxyZGY6RGVzY3Jp' 48 | b'cHRpb24gcmRmOmFib3V0PSIiCiAgICB4bWxuczppcHRjRXh0PSJodHRwOi8vaXB0Yy5vcmcv' 49 | b'c3RkL0lwdGM0eG1wRXh0LzIwMDgtMDItMjkvIgogICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9u' 50 | b'cy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFk' 51 | b'b2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgeG1sbnM6cGx1cz0i' 52 | b'aHR0cDovL25zLnVzZXBsdXMub3JnL2xkZi94bXAvMS4wLyIKICAgIHhtbG5zOkdJTVA9Imh0' 53 | b'dHA6Ly93d3cuZ2ltcC5vcmcveG1wLyIKICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcv' 54 | b'ZGMvZWxlbWVudHMvMS4xLyIKICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20v' 55 | b'dGlmZi8xLjAvIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAv' 56 | b'IgogICB4bXBNTTpEb2N1bWVudElEPSJnaW1wOmRvY2lkOmdpbXA6ZTg4MzMxNTYtMzZiYy00' 57 | b'ZTczLWFiMGYtODJiM2E1MzI1NmFlIgogICB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmU5' 58 | b'MmZmYWQ4LThhMzctNGQ0MC1hZTYyLWJjNjYwZTM1YWM2YSIKICAgeG1wTU06T3JpZ2luYWxE' 59 | b'b2N1bWVudElEPSJ4bXAuZGlkOjYyYzNjYTYyLTE1YTYtNDhiYy1hNGY0LTY2OGRhMDczNzdm' 60 | b'ZCIKICAgR0lNUDpBUEk9IjIuMCIKICAgR0lNUDpQbGF0Zm9ybT0iV2luZG93cyIKICAgR0lN' 61 | b'UDpUaW1lU3RhbXA9IjE2MzY0MzQxODc5NTk3NzQiCiAgIEdJTVA6VmVyc2lvbj0iMi4xMC4y' 62 | b'MiIKICAgZGM6Rm9ybWF0PSJpbWFnZS9wbmciCiAgIHRpZmY6T3JpZW50YXRpb249IjEiCiAg' 63 | b'IHhtcDpDcmVhdG9yVG9vbD0iR0lNUCAyLjEwIj4KICAgPGlwdGNFeHQ6TG9jYXRpb25DcmVh' 64 | b'dGVkPgogICAgPHJkZjpCYWcvPgogICA8L2lwdGNFeHQ6TG9jYXRpb25DcmVhdGVkPgogICA8' 65 | b'aXB0Y0V4dDpMb2NhdGlvblNob3duPgogICAgPHJkZjpCYWcvPgogICA8L2lwdGNFeHQ6TG9j' 66 | b'YXRpb25TaG93bj4KICAgPGlwdGNFeHQ6QXJ0d29ya09yT2JqZWN0PgogICAgPHJkZjpCYWcv' 67 | b'PgogICA8L2lwdGNFeHQ6QXJ0d29ya09yT2JqZWN0PgogICA8aXB0Y0V4dDpSZWdpc3RyeUlk' 68 | b'PgogICAgPHJkZjpCYWcvPgogICA8L2lwdGNFeHQ6UmVnaXN0cnlJZD4KICAgPHhtcE1NOkhp' 69 | b'c3Rvcnk+CiAgICA8cmRmOlNlcT4KICAgICA8cmRmOmxpCiAgICAgIHN0RXZ0OmFjdGlvbj0i' 70 | b'c2F2ZWQiCiAgICAgIHN0RXZ0OmNoYW5nZWQ9Ii8iCiAgICAgIHN0RXZ0Omluc3RhbmNlSUQ9' 71 | b'InhtcC5paWQ6MDAyOWEyODItNTllYy00NWMzLThiNjMtMTE2MmUwOWVhMjA0IgogICAgICBz' 72 | b'dEV2dDpzb2Z0d2FyZUFnZW50PSJHaW1wIDIuMTAgKFdpbmRvd3MpIgogICAgICBzdEV2dDp3' 73 | b'aGVuPSIyMDIxLTExLTA4VDIzOjAzOjA3Ii8+CiAgICA8L3JkZjpTZXE+CiAgIDwveG1wTU06' 74 | b'SGlzdG9yeT4KICAgPHBsdXM6SW1hZ2VTdXBwbGllcj4KICAgIDxyZGY6U2VxLz4KICAgPC9w' 75 | b'bHVzOkltYWdlU3VwcGxpZXI+CiAgIDxwbHVzOkltYWdlQ3JlYXRvcj4KICAgIDxyZGY6U2Vx' 76 | b'Lz4KICAgPC9wbHVzOkltYWdlQ3JlYXRvcj4KICAgPHBsdXM6Q29weXJpZ2h0T3duZXI+CiAg' 77 | b'ICA8cmRmOlNlcS8+CiAgIDwvcGx1czpDb3B5cmlnaHRPd25lcj4KICAgPHBsdXM6TGljZW5z' 78 | b'b3I+CiAgICA8cmRmOlNlcS8+CiAgIDwvcGx1czpMaWNlbnNvcj4KICA8L3JkZjpEZXNjcmlw' 79 | b'dGlvbj4KIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAg' 80 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 81 | b'ICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 82 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 83 | b'ICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 84 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 85 | b'ICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 86 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 87 | b'ICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 88 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAg' 89 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 90 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAg' 91 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 92 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAg' 93 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 94 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAg' 95 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 96 | b'ICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 97 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 98 | b'ICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 99 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 100 | b'ICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 101 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 102 | b'CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 103 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAg' 104 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 105 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAg' 106 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 107 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAg' 108 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 109 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAg' 110 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 111 | b'ICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 112 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 113 | b'ICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 114 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 115 | b'ICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' 116 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAog' 117 | b'ICAgICAgICAgICAgICAgICAgICAgICAgICAKPD94cGFja2V0IGVuZD0idyI/PsmlmdUAAAAG' 118 | b'YktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQflCwkFAwczB1qB' 119 | b'AAAAU0lEQVQ4y+3NsRWAIBAE0R2efWgJlkROZAVci3YgjdyZGJgiiQE/31lKKdndK7CpQ0Rc' 120 | b'wLFEhAGrOj2HNUnqHr/sSYNmYAb+E2gD+zMB9jHSJNkNEGQSt/B1TqkAAAAASUVORK5CYII=') 121 | 122 | #---------------------------------------------------------------------- 123 | ICON_CHECKBOX_UNCHECKED = PyEmbeddedImage( 124 | b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAFpJ' 125 | b'REFUOI3tzDERgDAQBdH9wQAOoKS6wVBmsIACIgkrOCBGcrS0IQ1Ftn+rGONWSjkkzVTk7rek' 126 | b'fTCzsxYDSBqBJQBTLX61hgYMQB/0wX8GucFfQVL6OMlAegBdmw/j71Be5wAAAABJRU5ErkJg' 127 | b'gg==') 128 | 129 | #---------------------------------------------------------------------- 130 | ICON_DROPDOWN_ARROW = PyEmbeddedImage( 131 | b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAHJJ' 132 | b'REFUSIntkEENg1AQBYc6+Q7AQaUgBSdIQAJIwMG0DuqgHNqE088C6anZOe5udl4eJEny56iT' 133 | b'2gY3vTrU9rfA8QTmmkTtgRF4RGGjhO/vs3D+E4k6qq+oPoDmjIRPHQvQAfdSyno1dFWirkeS' 134 | b'J0myswFVsUqvSX87GAAAAABJRU5ErkJggg==') 135 | 136 | #---------------------------------------------------------------------- 137 | ICON_TEST = PyEmbeddedImage( 138 | b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAQJJ' 139 | b'REFUSIntlNFRw0AMRF+owCVkK4g7wB1gOnAJKYEOYiogHQAdmAriDpYOsDvg4y4zmQGfIPAF' 140 | b'eT+euZO1llYyXPjzrKIA2xVwDdT56FnS+CsCtjtgB1TAS35ugAG4lTRFAldB8gfgXtJKUiOp' 141 | b'BpQ/7JCr+z62K9tvtreFmNH2Psq1VEELzJL6wrt3wM25AmsgMnIAKtt1KWjRA5KhJY73r+cI' 142 | b'jMAmMLEhtbE4SZ8KSHoCZlKfP5CFd0v3oUCmBTrbj6d9tt0CB1KLhkggWrQa6EmbfGTOZxOp' 143 | b'gqa02eGvIgutSZM1nSbLy9hHIj/Cdmd7WhrXkgdfQtIe2JIqvPAfeQcbwWKpPHAccgAAAABJ' 144 | b'RU5ErkJggg==') 145 | 146 | -------------------------------------------------------------------------------- /gswidgetkit/label.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # gswidgetkit Copyright 2021-2022 by Noah Rahm and contributors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ---------------------------------------------------------------------------- 16 | 17 | import wx 18 | 19 | 20 | class Label(wx.StaticText): 21 | """ 22 | Label widget wrapper created to abstract away needing to have extra code for 23 | changing the label's colors and font. 24 | 25 | :param wx.Window `parent`: parent window. Must not be ``None``. 26 | :param integer `id`: window identifier. A value of -1 indicates a default value. 27 | :param string `label`: the label text. 28 | :param `color`: the label text color. 29 | :param `bg_color`: the label background color. 30 | :param bool `font_bold`: if true, the label text will be bolded. 31 | """ 32 | def __init__(self, parent, id=wx.ID_ANY, label="", color="#fff", 33 | bg_color=None, font_bold=False, style=0): 34 | wx.StaticText.__init__(self, parent, id=id, label=label, style=style) 35 | 36 | if bg_color is None: 37 | self.SetBackgroundColour(parent.GetBackgroundColour()) 38 | else: 39 | self.SetBackgroundColour(bg_color) 40 | if font_bold is True: 41 | self.SetFont(self.GetFont().Bold()) 42 | self.SetForegroundColour(color) 43 | -------------------------------------------------------------------------------- /gswidgetkit/number_field.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # gswidgetkit Copyright 2021-2022 by Noah Rahm and contributors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ---------------------------------------------------------------------------- 16 | 17 | import wx 18 | from wx.lib.newevent import NewCommandEvent 19 | 20 | from .constants import ACCENT_COLOR, NUMBERFIELD_BG_COLOR, TEXT_COLOR 21 | from .textctrl import StyledTextCtrl 22 | from .utils import GetTextExtent 23 | 24 | numberfield_cmd_event, EVT_NUMBERFIELD = NewCommandEvent() 25 | numberfield_change_cmd_event, EVT_NUMBERFIELD_CHANGE = NewCommandEvent() 26 | 27 | 28 | class NumberField(wx.Control): 29 | """ 30 | Number field widget to select an integer. Supports both precise and imprecise 31 | editing (via dragging the cursor). 32 | 33 | :param wx.Window `parent`: parent window. Must not be ``None``. 34 | :param integer `id`: window identifier. A value of -1 indicates a default value. 35 | :param string `label`: the displayed text on the Numberfield label. 36 | :param string `type_`: the type of number field ("INTEGER" or "FLOAT") 37 | :param integer/float `default_value`: the default value. 38 | :param integer/float `min_value`: the minimum value the control can be set to. 39 | :param integer/float `max_value`: the maximum value the control can be set to. 40 | :param integer/float `step_size`: how much the value increments/decrements when mouse drags on this widget 41 | :param string `suffix`: the label text shown directly after the value. 42 | :param bool `show_p`: if True, show the progress in the background of the control. 43 | :param bool `disable_precise`: if True, disable the ability to edit the value via typing in a precise value. 44 | :param bool `scroll_horz`: if True, the user can scroll horizontally with the mouse. 45 | """ 46 | def __init__(self, parent, id=wx.ID_ANY, label="", type_="INTEGER", default_value=0, min_value=0, 47 | max_value=100, step_size=1, suffix="px", show_p=True, disable_precise=False, 48 | scroll_horz=True, size=wx.DefaultSize): 49 | wx.Control.__init__(self, parent, id, pos=wx.DefaultPosition, 50 | size=size, style=wx.NO_BORDER) 51 | 52 | self.parent = parent 53 | self.focused = False 54 | self.mouse_in = False 55 | self.control_size = wx.DefaultSize 56 | self.show_p = show_p 57 | self.disable_precise = disable_precise 58 | self.buffer = None 59 | 60 | if scroll_horz is True: 61 | self.scroll_dir = 0 62 | else: 63 | self.scroll_dir = 1 64 | 65 | self.type_ = type_ 66 | self.cur_value = default_value 67 | self.min_value = min_value 68 | self.max_value = max_value 69 | self.change_rate = .5 70 | self.change_value = 0 71 | self.step_size = step_size 72 | self.suffix = suffix 73 | 74 | self.value_range = [min_value, max_value] 75 | 76 | self.label = label 77 | 78 | self.padding_x = 20 79 | self.padding_y = 10 80 | 81 | # Flag that is true if a drag is happening after a left click 82 | self.changing_value = False 83 | 84 | # Keep track of last sent event 85 | self.last_sent_event = None 86 | 87 | # The point in which the cursor gets anchored to during the drag event 88 | self.anchor_point = (0, 0) 89 | 90 | # Text ctrl 91 | self.textctrl = StyledTextCtrl(self, value=str(self.cur_value), 92 | bg_color=NUMBERFIELD_BG_COLOR, 93 | style=wx.BORDER_NONE, pos=(0, 0), 94 | size=(10, 24)) 95 | self.textctrl.Hide() 96 | self.textctrl.Bind(wx.EVT_KILL_FOCUS, self.OnHideTextCtrl) 97 | 98 | self.Bind(wx.EVT_PAINT, self.OnPaint) 99 | self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None) 100 | self.Bind(wx.EVT_CHAR_HOOK, self.OnKey) 101 | self.Bind(wx.EVT_MOTION, self.OnMouseMotion) 102 | self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown, self) 103 | self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp, self) 104 | self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) 105 | self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) 106 | self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave) 107 | self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnter) 108 | self.Bind(wx.EVT_LEFT_DCLICK, self.OnShowTextCtrl) 109 | self.Bind(wx.EVT_SIZE, self.OnSize) 110 | 111 | def OnPaint(self, event): 112 | wx.BufferedPaintDC(self, self.buffer) 113 | 114 | def OnSize(self, event): 115 | size = self.GetClientSize() 116 | 117 | # Make sure size is at least 1px to avoid 118 | # strange "invalid bitmap size" errors. 119 | if size[0] < 1: 120 | size = (1, 1) 121 | self.buffer = wx.Bitmap(*size) 122 | self.UpdateDrawing() 123 | 124 | def UpdateDrawing(self): 125 | dc = wx.MemoryDC() 126 | dc.SelectObject(self.buffer) 127 | dc = wx.GCDC(dc) 128 | self.OnDrawBackground(dc) 129 | self.OnDrawWidget(dc) 130 | del dc # need to get rid of the MemoryDC before Update() is called. 131 | self.Refresh() 132 | self.Update() 133 | 134 | def OnDrawBackground(self, dc): 135 | dc.SetBackground(wx.Brush(self.parent.GetBackgroundColour())) 136 | dc.Clear() 137 | 138 | def OnDrawWidget(self, dc): 139 | fnt = self.parent.GetFont() 140 | dc.SetFont(fnt) 141 | dc.SetPen(wx.TRANSPARENT_PEN) 142 | 143 | full_val_lbl = str(self.cur_value)+self.suffix 144 | 145 | width = self.Size[0] 146 | height = self.Size[1] 147 | one_val = (width) / (self.max_value + abs(self.min_value)) 148 | self.p_val = round(((self.cur_value + abs(self.min_value))*one_val)) 149 | 150 | if self.mouse_in: 151 | p_color = wx.Colour(ACCENT_COLOR) 152 | bg_color = wx.Colour(NUMBERFIELD_BG_COLOR) 153 | else: 154 | p_color = wx.Colour(ACCENT_COLOR).ChangeLightness(90) 155 | bg_color = wx.Colour(NUMBERFIELD_BG_COLOR).ChangeLightness(85) 156 | dc.SetTextForeground(wx.Colour(TEXT_COLOR)) 157 | dc.SetBrush(wx.Brush(bg_color)) 158 | dc.DrawRoundedRectangle(0, 0, width, height, 4) 159 | 160 | if self.show_p is True: 161 | dc.SetBrush(wx.Brush(p_color)) 162 | dc.DrawRoundedRectangle(0, 0, self.p_val, height, 4) 163 | 164 | if self.p_val < width-4 and self.p_val > (self.min_value+4): 165 | dc.DrawRectangle((self.p_val)-4, 0, 4, height) 166 | 167 | lbl_w, lbl_h = GetTextExtent(self.label) 168 | val_w, val_h = GetTextExtent(full_val_lbl) 169 | 170 | dc.DrawText(self.label, self.padding_x, int((height/2) - (lbl_h/2))) 171 | dc.DrawText(full_val_lbl, (width-self.padding_x) - (val_w), int((height/2) - (val_h/2))) 172 | 173 | # Update position of textctrl 174 | self.textctrl.SetPosition((5, (int(self.Size[1]/2) - 10))) 175 | self.textctrl.SetSize((int(self.Size[0]-10), 24)) 176 | 177 | def OnKey(self, event): 178 | key = event.GetKeyCode() 179 | if key == wx.WXK_RETURN: 180 | self.mouse_in = False 181 | self.focused = False 182 | self.OnHideTextCtrl(None) 183 | self.UpdateDrawing() 184 | else: 185 | event.Skip() 186 | 187 | def OnMouseMotion(self, event): 188 | """ 189 | When the mouse moves, it check to see if it is a drag, or if left down had happened. 190 | If neither of those cases are true then it will cancel the action. 191 | If they are true then it calculates the change in position of the mouse, then changes 192 | the position of the cursor back to where the left click event happened. 193 | """ 194 | # Changes the cursor 195 | if self.changing_value: 196 | self.SetCursor(wx.Cursor(wx.CURSOR_BLANK)) 197 | else: 198 | self.SetCursor(wx.Cursor(wx.CURSOR_SIZEWE)) 199 | 200 | # Calculate the change in mouse position 201 | cur_point = event.GetPosition() 202 | self.delta = cur_point[self.scroll_dir] - self.anchor_point[self.scroll_dir] 203 | 204 | # If the cursor is being moved and dragged left or right 205 | if self.delta != 0 and event.Dragging() and self.changing_value: 206 | self.UpdateWidget() 207 | self.UpdateDrawing() 208 | 209 | if event.Dragging() and self.changing_value: 210 | self.SetCursor(wx.Cursor(wx.CURSOR_BLANK)) 211 | # Set the cursor back to the original point so it doesn't run away 212 | if "wxMac" not in wx.PlatformInfo: 213 | self.WarpPointer(int(self.anchor_point[0]), int(self.anchor_point[1])) 214 | 215 | # Case where the mouse is moving over the control, but has no 216 | # intent to actually change the value 217 | if self.changing_value and not event.Dragging(): 218 | self.changing_value = False 219 | self.parent.SetDoubleBuffered(False) 220 | 221 | event.Skip() 222 | 223 | def OnHideTextCtrl(self, event): 224 | value = self.textctrl.GetValue() 225 | if value != " ": 226 | if (self.type_ == "INTEGER"): 227 | new_value = int(value) 228 | elif (self.type_ == "FLOAT"): 229 | new_value = float(value) 230 | else: 231 | new_value = int(value) 232 | 233 | if new_value >= self.min_value and new_value <= self.max_value: 234 | self.cur_value = new_value 235 | 236 | self.textctrl.Hide() 237 | self.SendChangeEvent() 238 | self.SendSliderEvent() 239 | self.UpdateDrawing() 240 | 241 | def OnShowTextCtrl(self, event): 242 | if self.show_p is False and self.disable_precise is False: 243 | self.textctrl.SetValue(str(self.cur_value)) 244 | self.textctrl.Show() 245 | self.textctrl.SetFocus() 246 | event.Skip() 247 | self.textctrl.SetCurrentPos(len(str(self.cur_value+1))) 248 | 249 | def SendSliderEvent(self): 250 | wx.PostEvent(self, numberfield_cmd_event(id=self.GetId(), value=self.cur_value)) 251 | 252 | def SendChangeEvent(self): 253 | # Implement a debounce system where only one event is 254 | # sent only if the value actually changed. 255 | if self.cur_value != self.last_sent_event: 256 | wx.PostEvent(self, numberfield_change_cmd_event( 257 | id=self.GetId(), value=self.cur_value)) 258 | self.last_sent_event = self.cur_value 259 | 260 | def Increasing(self): 261 | if self.delta > 0: 262 | return True 263 | else: 264 | return False 265 | 266 | def Decreasing(self): 267 | if self.delta < 0: 268 | return True 269 | else: 270 | return False 271 | 272 | def OnLeftUp(self, event): 273 | """ 274 | Cancels the changing event, and turns off the optimization buffering 275 | """ 276 | self.changing_value = False 277 | self.parent.SetDoubleBuffered(False) 278 | self.SetCursor(wx.Cursor(wx.CURSOR_SIZEWE)) 279 | self.SendSliderEvent() 280 | 281 | def OnLeftDown(self, event): 282 | """ 283 | Sets the anchor point that the cursor will go back to the original position. 284 | Also turns on the doublebuffering which eliminates the flickering when rapidly changing values. 285 | """ 286 | pos = event.GetPosition() 287 | self.anchor_point = (pos[0], pos[1]) 288 | self.changing_value = True 289 | self.parent.SetDoubleBuffered(True) 290 | self.UpdateDrawing() 291 | 292 | def OnSetFocus(self, event): 293 | self.focused = True 294 | self.Refresh() 295 | 296 | def OnKillFocus(self, event): 297 | self.focused = False 298 | self.Refresh() 299 | 300 | def OnMouseEnter(self, event): 301 | self.mouse_in = True 302 | self.Refresh() 303 | self.UpdateDrawing() 304 | 305 | def OnMouseLeave(self, event): 306 | """ 307 | In the event that the mouse is moved fast enough to leave the bounds of the label, this 308 | will be triggered, warping the cursor back to where the left click event originally 309 | happened 310 | """ 311 | if self.changing_value: 312 | self.WarpPointer(self.anchor_point[0], self.anchor_point[1]) 313 | self.mouse_in = False 314 | self.Refresh() 315 | self.UpdateDrawing() 316 | 317 | def AcceptsFocusFromKeyboard(self): 318 | """Overridden base class virtual.""" 319 | return True 320 | 321 | def AcceptsFocus(self): 322 | """ Overridden base class virtual. """ 323 | return True 324 | 325 | def HasFocus(self): 326 | """ Returns whether or not we have the focus. """ 327 | return self.focused 328 | 329 | def GetValue(self): 330 | return self.cur_value 331 | 332 | def SetValue(self, value): 333 | self.cur_value = value 334 | 335 | def SetLabel(self, label): 336 | self.label = label 337 | 338 | def UpdateWidget(self): 339 | self.change_value += self.change_rate/2.0 340 | 341 | if self.change_value >= 1: 342 | if self.Increasing(): 343 | if self.cur_value < self.max_value: 344 | self.cur_value += self.step_size 345 | else: 346 | if (self.cur_value - 1) >= self.min_value: 347 | if self.cur_value > self.min_value: 348 | self.cur_value -= self.step_size 349 | 350 | # Reset the change value since the value was just changed. 351 | self.change_value = 0 352 | 353 | self.SendChangeEvent() 354 | 355 | def DoGetBestSize(self): 356 | """ 357 | Overridden base class virtual. Determines the best size of the control 358 | based on the label size, the bitmap size and the current font. 359 | """ 360 | 361 | normal_label = self.label 362 | value_label = str(self.cur_value) + self.suffix 363 | 364 | font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) 365 | 366 | dc = wx.ClientDC(self) 367 | dc.SetFont(font) 368 | 369 | # Measure our labels 370 | lbl_text_w, lbl_text_h = dc.GetTextExtent(normal_label) 371 | val_text_w, val_text_h = dc.GetTextExtent(value_label) 372 | 373 | totalwidth = lbl_text_w + val_text_w + self.padding_x + 76 374 | 375 | # To avoid issues with drawing the control properly, we 376 | # always make sure the width is an even number. 377 | if totalwidth % 2: 378 | totalwidth -= 1 379 | totalheight = lbl_text_h + self.padding_y 380 | 381 | best = wx.Size(totalwidth, totalheight) 382 | 383 | # Cache the best size so it doesn't need to be calculated again, 384 | # at least until some properties of the window change 385 | self.CacheBestSize(best) 386 | 387 | return best 388 | -------------------------------------------------------------------------------- /gswidgetkit/panel.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import wx 4 | 5 | 6 | class RoundedPanel(wx.Panel): 7 | """ 8 | Base class for panels with rounded corners 9 | """ 10 | def __init__(self, parent, *args, **kwargs): 11 | wx.Panel.__init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, 12 | size=wx.DefaultSize, style=wx.NO_BORDER | wx.TAB_TRAVERSAL) 13 | self.parent = parent -------------------------------------------------------------------------------- /gswidgetkit/popups.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # gswidgetkit Copyright 2021-2022 by Noah Rahm and contributors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ---------------------------------------------------------------------------- 16 | 17 | import wx 18 | from wx.lib.newevent import NewCommandEvent 19 | 20 | 21 | class Popup(wx.Control): 22 | def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.NO_BORDER, *args, **kwargs): 23 | wx.Control.__init__(self, parent, id, pos, size, style, *args, **kwargs) 24 | 25 | self.parent = parent 26 | 27 | -------------------------------------------------------------------------------- /gswidgetkit/scrollbar.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # gswidgetkit Copyright 2021-2022 by Noah Rahm and contributors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ---------------------------------------------------------------------------- 16 | -------------------------------------------------------------------------------- /gswidgetkit/textctrl.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # gswidgetkit Copyright 2021-2022 by Noah Rahm and contributors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ---------------------------------------------------------------------------- 16 | 17 | import wx 18 | from wx import stc 19 | 20 | from .constants import (ACCENT_COLOR, TEXT_COLOR, TEXTCTRL_BG_COLOR, 21 | TEXTCTRL_BORDER_COLOR) 22 | 23 | 24 | class StyledTextCtrl(stc.StyledTextCtrl): 25 | def __init__(self, parent, value="", placeholder="", scrollbar=False, 26 | style=0, bg_color=TEXTCTRL_BG_COLOR, sel_color=ACCENT_COLOR, 27 | *args, **kwargs): 28 | stc.StyledTextCtrl.__init__(self, parent, style=style | wx.TRANSPARENT_WINDOW, *args, **kwargs) 29 | 30 | if scrollbar is False: 31 | self.SetUseVerticalScrollBar(False) 32 | self.SetUseHorizontalScrollBar(False) 33 | self.SetCaretWidth(2) 34 | self.SetCaretForeground(sel_color) 35 | self.SetMarginLeft(8) 36 | self.SetMarginRight(8) 37 | self.SetMarginWidth(1, 0) 38 | self.SetEOLMode(stc.STC_EOL_LF) 39 | self.SetLexer(stc.STC_LEX_NULL) 40 | self.SetIndent(4) 41 | self.SetUseTabs(False) 42 | self.SetTabWidth(4) 43 | self.SetValue(value) 44 | self.SetScrollWidth(self.GetSize()[0]) 45 | self.SetScrollWidthTracking(True) 46 | self.SetSelBackground(True, sel_color) 47 | self.StyleSetBackground(stc.STC_STYLE_DEFAULT, wx.Colour(bg_color)) 48 | self.StyleSetForeground(stc.STC_STYLE_DEFAULT, wx.Colour("#ffffff")) 49 | self.StyleSetFont(stc.STC_STYLE_DEFAULT, 50 | wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)) 51 | self.StyleClearAll() 52 | self.SetValue(value) 53 | 54 | 55 | class NativeTextCtrl(wx.TextCtrl): 56 | def __init__(self, parent, value="", style=wx.BORDER_SIMPLE, *args, **kwargs): 57 | wx.TextCtrl.__init__(self, parent, value=value, style=style, *args, **kwargs) 58 | self.SetBackgroundColour(wx.Colour(TEXTCTRL_BG_COLOR)) 59 | self.SetForegroundColour(wx.Colour("#fff")) 60 | 61 | 62 | class TextCtrl(wx.Control): 63 | def __init__(self, parent, default="", icon=None, size=wx.DefaultSize): 64 | wx.Control.__init__(self, parent, wx.ID_ANY, pos=wx.DefaultPosition, 65 | size=size, style=wx.NO_BORDER) 66 | 67 | self.parent = parent 68 | self.focused = False 69 | self.mouse_in = False 70 | self.control_size = wx.DefaultSize 71 | self.buffer = None 72 | 73 | self.value = default 74 | self.icon = icon 75 | 76 | self.padding_x = 20 77 | self.padding_y = 30 78 | 79 | # Inner text ctrl 80 | self.textctrl = StyledTextCtrl(self, value=str(self.value), 81 | style=wx.BORDER_NONE, 82 | bg_color=TEXTCTRL_BG_COLOR, 83 | sel_color=ACCENT_COLOR, pos=(0, 0), 84 | size=(10, 24)) 85 | 86 | self.textctrl.Bind(wx.EVT_KILL_FOCUS, self.OnMouseLeave) 87 | self.textctrl.Bind(wx.EVT_SET_FOCUS, self.OnFocused) 88 | self.textctrl.Bind(wx.EVT_CHAR_HOOK, self.OnKey) 89 | 90 | self.Bind(wx.EVT_PAINT, self.OnPaint) 91 | self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None) 92 | self.Bind(wx.EVT_SIZE, self.OnSize) 93 | 94 | def OnPaint(self, event): 95 | wx.BufferedPaintDC(self, self.buffer) 96 | 97 | def OnSize(self, event): 98 | size = self.GetClientSize() 99 | 100 | # Make sure size is at least 1px to avoid 101 | # strange "invalid bitmap size" errors. 102 | if size[0] < 1: 103 | size = (1, 1) 104 | self.buffer = wx.Bitmap(*size) 105 | self.UpdateDrawing() 106 | 107 | def UpdateDrawing(self): 108 | dc = wx.MemoryDC() 109 | dc.SelectObject(self.buffer) 110 | dc = wx.GCDC(dc) 111 | self.OnDrawBackground(dc) 112 | self.OnDrawWidget(dc) 113 | del dc # need to get rid of the MemoryDC before Update() is called. 114 | self.Refresh() 115 | self.Update() 116 | 117 | def OnDrawBackground(self, dc): 118 | dc.SetBackground(wx.Brush(self.parent.GetBackgroundColour())) 119 | dc.Clear() 120 | 121 | def OnDrawWidget(self, dc): 122 | fnt = self.parent.GetFont() 123 | dc.SetFont(fnt) 124 | 125 | width = self.Size[0] 126 | height = self.Size[1] 127 | 128 | if self.mouse_in: 129 | text_color = wx.Colour(TEXT_COLOR) 130 | border_color = wx.Colour(ACCENT_COLOR) 131 | bg_color = wx.Colour(TEXTCTRL_BG_COLOR) 132 | else: 133 | text_color = wx.Colour(TEXT_COLOR) 134 | border_color = wx.Colour(TEXTCTRL_BORDER_COLOR) 135 | bg_color = wx.Colour(TEXTCTRL_BG_COLOR).ChangeLightness(85) 136 | 137 | dc.SetTextForeground(text_color) 138 | dc.SetPen(wx.Pen(border_color, 1)) 139 | dc.SetBrush(wx.Brush(bg_color)) 140 | dc.DrawRoundedRectangle(1, 1, width-1, height-1, 4) 141 | 142 | self.textctrl.StyleSetBackground(stc.STC_STYLE_DEFAULT, bg_color) 143 | 144 | if self.icon != None: 145 | dc.DrawBitmap(self.icon, 8, int(self.Size[1]/2) - (self.icon.Height/2)) 146 | 147 | # Update position of textctrl 148 | if self.icon == None: 149 | self.textctrl.SetPosition((2, (int(self.Size[1]/2) - 10))) 150 | self.textctrl.SetSize((int(self.Size[0]-4), 20)) 151 | else: 152 | self.textctrl.SetPosition((self.icon.Width + 4, (int(self.Size[1]/2) - 10))) 153 | self.textctrl.SetSize((int(self.Size[0]-(self.icon.Width + 4)-4), 20)) 154 | self.textctrl.SetCurrentPos(len(str(self.value))) 155 | self.textctrl.SelectNone() 156 | 157 | def OnKey(self, event): 158 | key = event.GetKeyCode() 159 | if key == wx.WXK_RETURN: 160 | self.mouse_in = False 161 | self.focused = False 162 | self.textctrl.SetCurrentPos(len(str(self.value))) 163 | self.UpdateDrawing() 164 | else: 165 | event.Skip() 166 | 167 | def OnFocused(self, event): 168 | self.mouse_in = True 169 | event.Skip() 170 | self.UpdateDrawing() 171 | 172 | def OnSetFocus(self, event): 173 | self.focused = True 174 | self.textctrl.SetFocus() 175 | event.Skip() 176 | self.Refresh() 177 | 178 | def OnKillFocus(self, event): 179 | self.focused = False 180 | event.Skip() 181 | self.Refresh() 182 | 183 | def OnMouseLeave(self, event): 184 | self.mouse_in = False 185 | event.Skip() 186 | self.Refresh() 187 | self.UpdateDrawing() 188 | 189 | def AcceptsFocusFromKeyboard(self): 190 | """Overridden base class virtual.""" 191 | return True 192 | 193 | def AcceptsFocus(self): 194 | """ Overridden base class virtual. """ 195 | return True 196 | 197 | def HasFocus(self): 198 | """ Returns whether or not we have the focus. """ 199 | return self.focused 200 | 201 | def GetValue(self): 202 | return self.value 203 | 204 | def SetValue(self, value): 205 | self.value = value 206 | self.textctrl.SetValue(value) 207 | 208 | def SetIcon(self, icon): 209 | self.icon = icon 210 | 211 | def SetFocus(self): 212 | self.textctrl.SetFocus() 213 | 214 | def DoGetBestSize(self): 215 | """ 216 | Overridden base class virtual. Determines the best size of the control. 217 | """ 218 | font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) 219 | dc = wx.ClientDC(self) 220 | dc.SetFont(font) 221 | 222 | # Calculate sizing 223 | totalwidth = self.padding_x + self.textctrl.GetSize()[0] + 130 224 | totalheight = self.textctrl.GetSize()[1] + self.padding_y 225 | 226 | best = wx.Size(totalwidth, totalheight) 227 | 228 | # Cache the best size so it doesn't need to be calculated again, 229 | # at least until some properties of the window change 230 | self.CacheBestSize(best) 231 | 232 | return best 233 | -------------------------------------------------------------------------------- /gswidgetkit/tooltip.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # gswidgetkit Copyright 2021-2022 by Noah Rahm and contributors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # This file includes modified code from wx.lib.agw.supertooltip 17 | # 18 | # ---------------------------------------------------------------------------- 19 | 20 | import wx 21 | import wx.lib.agw.supertooltip as STT 22 | 23 | 24 | def MakeBold(font): 25 | """ 26 | Makes a font bold. Utility method. 27 | 28 | :param `font`: the font to be made bold. 29 | """ 30 | 31 | newFont = wx.Font(font.GetPointSize(), font.GetFamily(), font.GetStyle(), 32 | wx.FONTWEIGHT_BOLD, font.GetUnderlined(), font.GetFaceName()) 33 | 34 | return newFont 35 | 36 | 37 | def ExtractLink(line): 38 | """ 39 | Extract the link from an hyperlink line. 40 | 41 | :param `line`: the line of text to be processed. 42 | """ 43 | 44 | line = line[4:] 45 | indxStart = line.find("{") 46 | indxEnd = line.find("}") 47 | hl = line[indxStart+1:indxEnd].strip() 48 | line = line[0:indxStart].strip() 49 | 50 | return line, hl 51 | 52 | 53 | class ToolTipWindowBase(object): 54 | """ Base class for the different Windows and Mac implementation. """ 55 | 56 | def __init__(self, parent, classParent): 57 | """ 58 | Default class constructor. 59 | 60 | :param `parent`: the :class:`SuperToolTip` parent widget; 61 | :param `classParent`: the :class:`SuperToolTip` class object. 62 | """ 63 | 64 | self._spacing = 18 65 | self._wasOnLink = False 66 | self._hyperlinkRect, self._hyperlinkWeb = [], [] 67 | 68 | self._classParent = classParent 69 | 70 | # Bind the events 71 | self.Bind(wx.EVT_PAINT, self.OnPaint) 72 | self.Bind(wx.EVT_SIZE, self.OnSize) 73 | self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) 74 | self.Bind(wx.EVT_MOTION, self.OnMouseMotion) 75 | parent.Bind(wx.EVT_KILL_FOCUS, self.OnDestroy) 76 | self.Bind(wx.EVT_LEFT_DOWN, self.OnDestroy) 77 | self.Bind(wx.EVT_LEFT_DCLICK, self.OnDestroy) 78 | 79 | 80 | def OnPaint(self, event): 81 | """ 82 | Handles the ``wx.EVT_PAINT`` event for :class:`SuperToolTip`. 83 | 84 | If the `event` parameter is ``None``, calculates best size and returns it. 85 | 86 | :param `event`: a :class:`PaintEvent` event to be processed or ``None``. 87 | """ 88 | 89 | maxWidth = 0 90 | if event is None: 91 | dc = wx.ClientDC(self) 92 | else: 93 | # Go with double buffering... 94 | dc = wx.BufferedPaintDC(self) 95 | 96 | frameRect = self.GetClientRect() 97 | x, y, width, _height = frameRect 98 | # Store the rects for the hyperlink lines 99 | self._hyperlinkRect, self._hyperlinkWeb = [], [] 100 | classParent = self._classParent 101 | 102 | # Retrieve the colours for the blended triple-gradient background 103 | topColour, middleColour, bottomColour = classParent.GetTopGradientColour(), \ 104 | classParent.GetMiddleGradientColour(), \ 105 | classParent.GetBottomGradientColour() 106 | 107 | # Get the user options for header, bitmaps etc... 108 | drawHeader, drawFooter = classParent.GetDrawHeaderLine(), classParent.GetDrawFooterLine() 109 | topRect = wx.Rect(frameRect.x, frameRect.y, frameRect.width, frameRect.height/2) 110 | bottomRect = wx.Rect(frameRect.x, frameRect.y+frameRect.height/2, frameRect.width, frameRect.height/2+1) 111 | # Fill the triple-gradient 112 | dc.GradientFillLinear(topRect, topColour, middleColour, wx.SOUTH) 113 | dc.GradientFillLinear(bottomRect, middleColour, bottomColour, wx.SOUTH) 114 | 115 | header, headerBmp = classParent.GetHeader(), classParent.GetHeaderBitmap() 116 | headerFont, messageFont, footerFont, hyperlinkFont = classParent.GetHeaderFont(), classParent.GetMessageFont(), \ 117 | classParent.GetFooterFont(), classParent.GetHyperlinkFont() 118 | 119 | yPos = 0 120 | bmpXPos = 0 121 | bmpHeight = textHeight = bmpWidth = 0 122 | 123 | if headerBmp and headerBmp.IsOk(): 124 | # We got the header bitmap 125 | bmpHeight, bmpWidth = headerBmp.GetHeight(), headerBmp.GetWidth() 126 | bmpXPos = self._spacing 127 | 128 | if header: 129 | # We got the header text 130 | dc.SetFont(headerFont) 131 | textWidth, textHeight = dc.GetTextExtent(header) 132 | maxWidth = max(bmpWidth+(textWidth+self._spacing*2), maxWidth) 133 | # Calculate the header height 134 | height = max(textHeight, bmpHeight) 135 | normalText = classParent.GetTextColour() 136 | if header: 137 | dc.SetTextForeground(normalText) 138 | dc.DrawText(header, bmpXPos+bmpWidth+self._spacing, (height-textHeight+self._spacing)/2) 139 | if headerBmp and headerBmp.IsOk(): 140 | dc.DrawBitmap(headerBmp, bmpXPos, (height-bmpHeight+self._spacing)/2, True) 141 | 142 | if header or (headerBmp and headerBmp.IsOk()): 143 | yPos += height 144 | if drawHeader: 145 | # Draw the separator line after the header 146 | dc.SetPen(wx.GREY_PEN) 147 | dc.DrawLine(self._spacing, yPos+self._spacing, width-self._spacing, yPos+self._spacing) 148 | yPos += self._spacing 149 | 150 | maxWidth = max(bmpXPos + bmpWidth + self._spacing, maxWidth) 151 | # Get the big body image (if any) 152 | embeddedImage = classParent.GetBodyImage() 153 | bmpWidth = bmpHeight = -1 154 | if embeddedImage and embeddedImage.IsOk(): 155 | bmpWidth, bmpHeight = embeddedImage.GetWidth(), embeddedImage.GetHeight() 156 | 157 | # A bunch of calculations to draw the main body message 158 | messageHeight = 0 159 | lines = classParent.GetMessage().split("\n") 160 | yText = yPos 161 | embImgPos = yPos 162 | hyperLinkText = wx.BLUE 163 | messagePos = self._getTextExtent(dc, lines[0] if lines else "")[1] // 2 + self._spacing 164 | for line in lines: 165 | # Loop over all the lines in the message 166 | if line.startswith("
"): # draw a line 167 | yText += self._spacing * 2 168 | dc.DrawLine(self._spacing, yText+self._spacing, width-self._spacing, yText+self._spacing) 169 | else: 170 | isLink = False 171 | dc.SetTextForeground(normalText) 172 | if line.startswith(""): # is a bold line 173 | line = line[4:] 174 | font = MakeBold(messageFont) 175 | dc.SetFont(font) 176 | elif line.startswith(""): # is a link 177 | dc.SetFont(hyperlinkFont) 178 | isLink = True 179 | line, hl = ExtractLink(line) 180 | dc.SetTextForeground(hyperLinkText) 181 | else: 182 | # Is a normal line 183 | dc.SetFont(messageFont) 184 | 185 | textWidth, textHeight = self._getTextExtent(dc, line) 186 | 187 | messageHeight += textHeight 188 | 189 | xText = (bmpWidth + 2 * (self._spacing/2)) if bmpWidth > 0 else self._spacing 190 | yText += textHeight/2+(self._spacing/2) 191 | maxWidth = max(xText + textWidth + self._spacing, maxWidth) 192 | dc.DrawText(line, xText, yText) 193 | if isLink: 194 | self._storeHyperLinkInfo(xText, yText, textWidth, textHeight, hl) 195 | 196 | toAdd = 0 197 | if bmpHeight > messageHeight: 198 | yPos += 2*self._spacing + bmpHeight 199 | toAdd = self._spacing 200 | else: 201 | yPos += messageHeight + 2*self._spacing 202 | 203 | yText = max(messageHeight, bmpHeight+2*self._spacing) 204 | if embeddedImage and embeddedImage.IsOk(): 205 | # Draw the main body image 206 | dc.DrawBitmap(embeddedImage, self._spacing, embImgPos + (self._spacing * 2), True) 207 | 208 | footer, footerBmp = classParent.GetFooter(), classParent.GetFooterBitmap() 209 | bmpHeight = bmpWidth = textHeight = textWidth = 0 210 | bmpXPos = 0 211 | 212 | if footerBmp and footerBmp.IsOk(): 213 | # Got the footer bitmap 214 | bmpHeight, bmpWidth = footerBmp.GetHeight(), footerBmp.GetWidth() 215 | bmpXPos = self._spacing 216 | 217 | if footer: 218 | # Got the footer text 219 | footerFont.SetWeight(wx.FONTWEIGHT_NORMAL) 220 | dc.SetFont(footerFont.Italic()) 221 | textWidth, textHeight = dc.GetTextExtent(footer) 222 | 223 | if textHeight or bmpHeight: 224 | if drawFooter: 225 | # Draw the separator line before the footer 226 | dc.SetPen(wx.GREY_PEN) 227 | dc.DrawLine(self._spacing, yPos-self._spacing/2+toAdd, 228 | width-self._spacing, yPos-self._spacing/2+toAdd) 229 | # Draw the footer and footer bitmap (if any) 230 | dc.SetTextForeground(wx.Colour("#96B0DA")) 231 | height = max(textHeight, bmpHeight) 232 | yPos += toAdd 233 | if footer: 234 | toAdd = (height - textHeight + (self._spacing/2)) // 2 235 | dc.DrawText(footer, bmpXPos + bmpWidth + self._spacing, yPos + toAdd) 236 | maxWidth = max(bmpXPos + bmpWidth + (self._spacing/2) + textWidth, maxWidth) 237 | if footerBmp and footerBmp.IsOk(): 238 | toAdd = (height - bmpHeight + (self._spacing/2)) / 2 239 | dc.DrawBitmap(footerBmp, bmpXPos, yPos + toAdd, True) 240 | maxWidth = max(footerBmp.GetSize().GetWidth() + bmpXPos, maxWidth) 241 | 242 | maxHeight = yPos + height + toAdd + 8 243 | if event is None: 244 | return maxWidth, maxHeight 245 | 246 | 247 | @staticmethod 248 | def _getTextExtent(dc, line): 249 | textWidth, textHeight = dc.GetTextExtent(line) 250 | if textHeight == 0: 251 | _, textHeight = dc.GetTextExtent("a") 252 | return textWidth, textHeight 253 | 254 | def _storeHyperLinkInfo(self, hTextPos, vTextPos, textWidth, textHeight, linkTarget): 255 | # Store the hyperlink rectangle and link 256 | self._hyperlinkRect.append(wx.Rect(hTextPos, vTextPos, textWidth, textHeight)) 257 | self._hyperlinkWeb.append(linkTarget) 258 | 259 | 260 | def OnEraseBackground(self, event): 261 | """ 262 | Handles the ``wx.EVT_ERASE_BACKGROUND`` event for :class:`SuperToolTip`. 263 | 264 | :param `event`: a :class:`EraseEvent` event to be processed. 265 | 266 | :note: This method is intentionally empty to reduce flicker. 267 | """ 268 | 269 | # This is intentionally empty to reduce flicker 270 | pass 271 | 272 | 273 | def OnSize(self, event): 274 | """ 275 | Handles the ``wx.EVT_SIZE`` event for :class:`SuperToolTip`. 276 | 277 | :param `event`: a :class:`wx.SizeEvent` event to be processed. 278 | """ 279 | 280 | self.Refresh() 281 | event.Skip() 282 | 283 | 284 | def OnMouseMotion(self, event): 285 | """ 286 | Handles the ``wx.EVT_MOTION`` event for :class:`SuperToolTip`. 287 | 288 | :param `event`: a :class:`MouseEvent` event to be processed. 289 | """ 290 | 291 | x, y = event.GetPosition() 292 | for rect in self._hyperlinkRect: 293 | if rect.Contains((x, y)): 294 | # We are over one hyperlink... 295 | self.SetCursor(wx.Cursor(wx.CURSOR_HAND)) 296 | self._wasOnLink = True 297 | return 298 | 299 | if self._wasOnLink: 300 | # Restore the normal cursor 301 | self._wasOnLink = False 302 | self.SetCursor(wx.NullCursor) 303 | 304 | 305 | def OnDestroy(self, event): 306 | """ 307 | Handles the ``wx.EVT_LEFT_DOWN``, ``wx.EVT_LEFT_DCLICK`` and ``wx.EVT_KILL_FOCUS`` 308 | events for :class:`SuperToolTip`. All these events destroy the :class:`SuperToolTip`, 309 | unless the user clicked on one hyperlink. 310 | 311 | :param `event`: a :class:`MouseEvent` or a :class:`FocusEvent` event to be processed. 312 | """ 313 | 314 | if not isinstance(event, wx.MouseEvent): 315 | # We haven't clicked a link 316 | if self: # Check if window still exists, Destroy might have been called manually (more than once) 317 | self.Destroy() 318 | return 319 | 320 | x, y = event.GetPosition() 321 | for indx, rect in enumerate(self._hyperlinkRect): 322 | if rect.Contains((x, y)): 323 | # Run the webbrowser with the clicked link 324 | webbrowser.open_new_tab(self._hyperlinkWeb[indx]) 325 | return 326 | 327 | self.Destroy() 328 | 329 | 330 | def SetFont(self, font): 331 | """ 332 | Sets the :class:`SuperToolTip` font globally. 333 | 334 | :param `font`: the font to set. 335 | """ 336 | 337 | wx.PopupWindow.SetFont(self, font) 338 | self._classParent.InitFont() 339 | self.Invalidate() 340 | 341 | 342 | def Invalidate(self): 343 | """ Invalidate :class:`SuperToolTip` size and repaint it. """ 344 | 345 | if not self._classParent.GetMessage(): 346 | # No message yet... 347 | return 348 | 349 | self.CalculateBestSize() 350 | self.Refresh() 351 | 352 | def CalculateBestSize(self): 353 | """ Calculates the :class:`SuperToolTip` window best size. """ 354 | 355 | maxWidth, maxHeight = self.OnPaint(None) 356 | self.SetSize((maxWidth, maxHeight)) 357 | 358 | def CalculateBestPosition(self, widget): 359 | x, y = wx.GetMousePosition() 360 | screen = wx.ClientDisplayRect()[2:] 361 | left, top = widget.ClientToScreen((0, 0)) 362 | right, bottom = widget.ClientToScreen(widget.GetClientRect()[2:]) 363 | size = self.GetSize() 364 | 365 | if x+size[0]>screen[0]: 366 | xpos = x-size[0] 367 | else: 368 | xpos = x 369 | 370 | if bottom+size[1]>screen[1]: 371 | ypos = top-size[1] + 6 372 | else: 373 | ypos = bottom + 6 374 | 375 | self.SetPosition((xpos,ypos)) 376 | 377 | 378 | # Handle Mac and Windows/GTK differences... 379 | 380 | if wx.Platform == "__WXMAC__": 381 | 382 | class ToolTipWindow(wx.Frame, ToolTipWindowBase): 383 | """ Popup window that works on wxMac. """ 384 | 385 | def __init__(self, parent, classParent): 386 | """ 387 | Default class constructor. 388 | 389 | :param `parent`: the :class:`SuperToolTip` parent widget; 390 | :param `classParent`: the :class:`SuperToolTip` class object. 391 | """ 392 | 393 | wx.Frame.__init__(self, parent, style=wx.NO_BORDER|wx.FRAME_FLOAT_ON_PARENT|wx.FRAME_NO_TASKBAR|wx.POPUP_WINDOW) 394 | # Call the base class 395 | ToolTipWindowBase.__init__(self, parent, classParent) 396 | 397 | else: 398 | 399 | class ToolTipWindow(ToolTipWindowBase, wx.PopupWindow): 400 | """ 401 | A simple :class:`PopupWindow` that holds fancy tooltips. 402 | Not available on Mac as :class:`PopupWindow` is not implemented there. 403 | """ 404 | 405 | def __init__(self, parent, classParent): 406 | """ 407 | Default class constructor. 408 | 409 | :param `parent`: the :class:`SuperToolTip` parent widget; 410 | :param `classParent`: the :class:`SuperToolTip` class object. 411 | """ 412 | 413 | wx.PopupWindow.__init__(self, parent) 414 | # Call the base class 415 | ToolTipWindowBase.__init__(self, parent, classParent) 416 | 417 | 418 | 419 | class ToolTip(STT.SuperToolTip): 420 | def __init__(self, header, message, target, bodyImage=wx.NullBitmap, headerBmp=wx.NullBitmap, footer="", footerBmp=wx.NullBitmap): 421 | STT.SuperToolTip.__init__(self, message, bodyImage, header, headerBmp, footer, footerBmp) 422 | 423 | self.SetHeader(header) 424 | self.SetTarget(target) 425 | self.SetStartDelay(.5) 426 | self.SetTopGradientColour(wx.Colour("#272727")) 427 | self.SetMiddleGradientColour(wx.Colour("#272727")) 428 | self.SetBottomGradientColour(wx.Colour("#272727")) 429 | self.SetTextColour(wx.Colour("#ccc")) 430 | 431 | def OnStartTimer(self): 432 | """ The creation time has expired, create the :class:`SuperToolTip`. """ 433 | 434 | # target widget might already be destroyed 435 | if not self._widget: 436 | self._startTimer.Stop() 437 | return 438 | 439 | tip = ToolTipWindow(self._widget, self) 440 | self._superToolTip = tip 441 | self._superToolTip.CalculateBestSize() 442 | self._superToolTip.CalculateBestPosition(self._widget) 443 | 444 | self._superToolTip.Show() 445 | 446 | self._startTimer.Stop() 447 | self._endTimer.Start(self._endDelayTime*1000) 448 | 449 | 450 | def OnEndTimer(self): 451 | """ The show time for :class:`SuperToolTip` has expired, destroy the :class:`SuperToolTip`. """ 452 | 453 | if self._superToolTip: 454 | self._superToolTip.Destroy() 455 | 456 | self._endTimer.Stop() 457 | 458 | 459 | def DoShowNow(self): 460 | """ Create the :class:`SuperToolTip` immediately. """ 461 | 462 | if self._superToolTip: 463 | # need to destroy it if already exists, 464 | # otherwise we might end up with many of them 465 | self._superToolTip.Destroy() 466 | 467 | tip = ToolTipWindow(self._widget, self) 468 | self._superToolTip = tip 469 | self._superToolTip.CalculateBestSize() 470 | self._superToolTip.CalculateBestPosition(self._widget) 471 | 472 | # need to stop this, otherwise we get into trouble when leaving the window 473 | self._startTimer.Stop() 474 | 475 | self._superToolTip.Show() 476 | 477 | self._endTimer.Start(self._endDelayTime*1000) 478 | 479 | -------------------------------------------------------------------------------- /gswidgetkit/utils.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # gswidgetkit Copyright 2021-2022 by Noah Rahm and contributors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ---------------------------------------------------------------------------- 16 | 17 | import wx 18 | 19 | 20 | def GetTextExtent(text): 21 | tdc = wx.WindowDC(wx.GetApp().GetTopWindow()) 22 | w, h = tdc.GetTextExtent(text) 23 | return w, h 24 | -------------------------------------------------------------------------------- /gswidgetkit/z_matrix.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # Gimel Studio Copyright 2021-2022 by Noah Rahm and contributors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # This file is code originally from EmbroidePy 17 | # which is licensed under the MIT License 18 | # Copyright (c) 2018 Metallicow 19 | # ---------------------------------------------------------------------------- 20 | 21 | from wx import AffineMatrix2D 22 | 23 | 24 | class ZMatrix(AffineMatrix2D): 25 | def __init__(self, *args, **kwds): 26 | AffineMatrix2D.__init__(self) 27 | 28 | def Reset(self): 29 | AffineMatrix2D.__init__(self) 30 | 31 | def PostScale(self, sx, sy=None, ax=0, ay=0): 32 | self.Invert() 33 | if sy is None: 34 | sy = sx 35 | if ax == 0 and ay == 0: 36 | self.Scale(1.0 / sx, 1.0 / sy) 37 | else: 38 | self.Translate(ax, ay) 39 | self.Scale(1.0 / sx, 1.0 / sy) 40 | self.Translate(-ax, -ay) 41 | self.Invert() 42 | 43 | def PostTranslate(self, px, py): 44 | self.Invert() 45 | self.Translate(-px, -py) 46 | self.Invert() 47 | 48 | def PostRotate(self, radians, rx=0, ry=0): 49 | self.Invert() 50 | if rx == 0 and ry == 0: 51 | self.Rotate(-radians) 52 | else: 53 | self.Translate(rx, ry) 54 | self.Rotate(-radians) 55 | self.Translate(-rx, -ry) 56 | self.Invert() 57 | 58 | def PreScale(self, sx, sy=None, ax=0, ay=0): 59 | if sy is None: 60 | sy = sx 61 | if ax == 0 and ay == 0: 62 | self.Scale(sx, sy) 63 | else: 64 | self.Translate(ax, ay) 65 | self.Scale(sx, sy) 66 | self.Translate(-ax, -ay) 67 | 68 | def PreTranslate(self, px, py): 69 | self.Translate(px, py) 70 | 71 | def PreRotate(self, radians, rx=0, ry=0): 72 | if rx == 0 and ry == 0: 73 | self.Rotate(radians) 74 | else: 75 | self.Translate(rx, ry) 76 | self.Rotate(radians) 77 | self.Translate(-rx, -ry) 78 | 79 | def GetScaleX(self): 80 | return self.Get()[0].m_11 81 | 82 | def GetScaleY(self): 83 | return self.Get()[0].m_22 84 | 85 | def GetSkewX(self): 86 | return self.Get()[0].m_12 87 | 88 | def GetSkewY(self): 89 | return self.Get()[0].m_21 90 | 91 | def GetTranslateX(self): 92 | return self.Get()[1].x 93 | 94 | def GetTranslateY(self): 95 | return self.Get()[1].y 96 | 97 | def InverseTransformPoint(self, position): 98 | self.Invert() 99 | converted_point = self.TransformPoint(position) 100 | self.Invert() 101 | return converted_point 102 | -------------------------------------------------------------------------------- /gswidgetkit/zoom_panel.py: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------------------- 2 | # gswidgetkit Copyright 2021-2022 by Noah Rahm and contributors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ---------------------------------------------------------------------------- 16 | 17 | import wx 18 | 19 | from .z_matrix import ZMatrix 20 | 21 | 22 | class ZoomPanel(wx.Panel): 23 | def __init__(self, *args, **kwds): 24 | self.matrix = ZMatrix() 25 | self.identity = ZMatrix() 26 | self.matrix.Reset() 27 | self.identity.Reset() 28 | self.previous_position = None 29 | self._Buffer = None 30 | 31 | kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE 32 | wx.Panel.__init__(self, *args, **kwds) 33 | 34 | self.Bind(wx.EVT_PAINT, self.OnPaint) 35 | self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None) 36 | self.Bind(wx.EVT_MOTION, self.OnMouseMove) 37 | self.Bind(wx.EVT_MOUSEWHEEL, self.OnMousewheel) 38 | self.Bind(wx.EVT_MIDDLE_DOWN, self.OnMouseMiddleDown) 39 | self.Bind(wx.EVT_MIDDLE_UP, self.OnMouseMiddleUp) 40 | self.Bind(wx.EVT_SIZE, self.OnSize) 41 | 42 | def OnPaint(self, event): 43 | dc = wx.BufferedPaintDC(self, self._Buffer) 44 | 45 | def OnSize(self, event): 46 | size = self.GetClientSize() 47 | 48 | # Make sure size is at least 1px to avoid 49 | # strange "invalid bitmap size" errors. 50 | if size[0] < 1: 51 | size = (10, 10) 52 | self._Buffer = wx.Bitmap(*size) 53 | self.UpdateDrawing() 54 | 55 | def UpdateDrawing(self): 56 | dc = wx.MemoryDC() 57 | dc.SelectObject(self._Buffer) 58 | self.OnDrawBackground(dc) 59 | dc.SetTransformMatrix(self.matrix) 60 | self.OnDrawScene(dc) 61 | dc.SetTransformMatrix(self.identity) 62 | self.OnDrawInterface(dc) 63 | del dc # need to get rid of the MemoryDC before Update() is called. 64 | self.Refresh() 65 | self.Update() 66 | 67 | def OnDrawBackground(self, dc): 68 | pass 69 | 70 | def OnDrawScene(self, dc): 71 | pass 72 | 73 | def OnDrawInterface(self, dc): 74 | pass 75 | 76 | def SceneMatrixReset(self): 77 | self.matrix.Reset() 78 | 79 | def ScenePostScale(self, sx, sy=None, ax=0, ay=0): 80 | self.matrix.PostScale(sx, sy, ax, ay) 81 | 82 | def ScenePostPan(self, px, py): 83 | self.matrix.PostTranslate(px, py) 84 | 85 | def ScenePostRotate(self, angle, rx=0, ry=0): 86 | self.matrix.PostRotate(angle, rx, ry) 87 | 88 | def ScenePreScale(self, sx, sy=None, ax=0, ay=0): 89 | self.matrix.PreScale(sx, sy, ax, ay) 90 | 91 | def ScenePrePan(self, px, py): 92 | self.matrix.PreTranslate(px, py) 93 | 94 | def ScenePreRotate(self, angle, rx=0, ry=0): 95 | self.matrix.PreRotate(angle, rx, ry) 96 | 97 | def GetScaleX(self): 98 | return self.matrix.GetScaleX() 99 | 100 | def GetScaleY(self): 101 | return self.matrix.GetScaleY() 102 | 103 | def GetSkewX(self): 104 | return self.matrix.GetSkewX() 105 | 106 | def GetSkewY(self): 107 | return self.matrix.GetSkewY() 108 | 109 | def GetTranslateX(self): 110 | return self.matrix.GetTranslateX() 111 | 112 | def GetTranslateY(self): 113 | return self.matrix.GetTranslateY() 114 | 115 | def OnMousewheel(self, event): 116 | rotation = event.GetWheelRotation() 117 | mouse = event.GetPosition() 118 | if rotation > 1: 119 | self.ScenePostScale(1.1, 1.1, mouse[0], mouse[1]) 120 | elif rotation < -1: 121 | self.ScenePostScale(0.9, 0.9, mouse[0], mouse[1]) 122 | self.UpdateDrawing() 123 | 124 | def OnMouseMiddleDown(self, event): 125 | self.previous_position = event.GetPosition() 126 | self.SetCursor(wx.Cursor(wx.CURSOR_SIZING)) 127 | 128 | def OnMouseMiddleUp(self, event): 129 | self.previous_position = None 130 | self.SetCursor(wx.Cursor(wx.CURSOR_ARROW)) 131 | 132 | def OnMouseMove(self, event): 133 | if self.previous_position is None: 134 | return 135 | scene_position = event.GetPosition() 136 | previous_scene_position = self.previous_position 137 | dx = (scene_position[0] - previous_scene_position[0]) 138 | dy = (scene_position[1] - previous_scene_position[1]) 139 | self.ScenePostPan(dx, dy) 140 | self.UpdateDrawing() 141 | self.previous_position = scene_position 142 | 143 | def FocusPositionScene(self, scene_point): 144 | window_width, window_height = self.ClientSize 145 | scale_x = self.GetScaleX() 146 | scale_y = self.GetScaleY() 147 | self.SceneMatrixReset() 148 | self.ScenePostPan(-scene_point[0], -scene_point[1]) 149 | self.ScenePostScale(scale_x, scale_y) 150 | self.ScenePostPan(window_width / 2.0, window_height / 2.0) 151 | 152 | def FocusViewportScene(self, new_scene_viewport, buffer=0, lock=True): 153 | window_width, window_height = self.ClientSize 154 | left = new_scene_viewport[0] 155 | top = new_scene_viewport[1] 156 | right = new_scene_viewport[2] 157 | bottom = new_scene_viewport[3] 158 | viewport_width = right - left 159 | viewport_height = bottom - top 160 | 161 | left -= viewport_width * buffer 162 | right += viewport_width * buffer 163 | top -= viewport_height * buffer 164 | bottom += viewport_height * buffer 165 | 166 | if right == left: 167 | scale_x = 100 168 | else: 169 | scale_x = window_width / float(right - left) 170 | if bottom == top: 171 | scale_y = 100 172 | else: 173 | scale_y = window_height / float(bottom - top) 174 | 175 | cx = ((right + left) / 2) 176 | cy = ((top + bottom) / 2) 177 | self.matrix.Reset() 178 | self.matrix.PostTranslate(-cx, -cy) 179 | if lock: 180 | scale = min(scale_x, scale_y) 181 | if scale != 0: 182 | self.matrix.PostScale(scale) 183 | else: 184 | if scale_x != 0 and scale_y != 0: 185 | self.matrix.PostScale(scale_x, scale_y) 186 | self.matrix.PostTranslate(window_width / 2.0, window_height / 2.0) 187 | 188 | def ConvertSceneToWindow(self, position): 189 | return self.matrix.TransformPoint([position[0], position[1]]) 190 | 191 | def ConvertWindowToScene(self, position): 192 | return self.matrix.InverseTransformPoint([position[0], position[1]]) 193 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name='gswidgetkit', 6 | packages=['gswidgetkit', 'gswidgetkit.icons'], 7 | version='0.3.3', 8 | license='Apache 2.0', 9 | description='Custom widget toolkit for easier creation of customized wxPython GUIs', 10 | long_description_content_type="text/markdown", 11 | author='Noah Rahm', 12 | author_email='correctsyntax@yahoo.com', 13 | url='https://github.com/GimelStudio/gswidgetkit', 14 | keywords=['wxpython', 'widgets', 'custom'], 15 | install_requires=[ 16 | 'wxpython==4.2.1' 17 | ], 18 | classifiers=[ 19 | 'Development Status :: 2 - Pre-Alpha', 20 | 'Intended Audience :: Developers', 21 | 'Operating System :: OS Independent', 22 | 'Topic :: Desktop Environment', 23 | 'Topic :: Multimedia :: Graphics :: Editors', 24 | 'License :: OSI Approved :: Apache Software License', 25 | 'Programming Language :: Python :: 3', 26 | 'Programming Language :: Python :: 3.8', 27 | 'Programming Language :: Python :: 3.9', 28 | 'Programming Language :: Python :: 3.10', 29 | 'Programming Language :: Python :: 3.11', 30 | ], 31 | ) 32 | --------------------------------------------------------------------------------