├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── TODO.md ├── bin └── pygameui-kitchensink.py ├── distribute_setup.py ├── pygameui ├── __init__.py ├── alert.py ├── button.py ├── callback.py ├── checkbox.py ├── colors.py ├── dialog.py ├── flipbook.py ├── focus.py ├── grid.py ├── imagebutton.py ├── imageview.py ├── kvc.py ├── label.py ├── listview.py ├── notification.py ├── progress.py ├── render.py ├── resource.py ├── resources │ ├── fonts │ │ ├── bold.ttf │ │ ├── font info.txt │ │ └── regular.ttf │ └── images │ │ ├── info.png │ │ ├── logo.png │ │ ├── shadow.png │ │ ├── spinner.png │ │ └── star.png ├── scene.py ├── scroll.py ├── select.py ├── slider.py ├── spinner.py ├── textfield.py ├── theme.py ├── view.py └── window.py ├── screenshot.png └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | MANIFEST 4 | dist 5 | build 6 | *.egg-info 7 | Makefile 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of 2 | this software and associated documentation files (the "Software"), to deal in 3 | the Software without restriction, including without limitation the rights to 4 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 5 | the Software, and to permit persons to whom the Software is furnished to do so, 6 | subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include distribute_setup.py 2 | recursive-include pygameui/resources * 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pygameui 2 | 3 | A simple GUI framework for [Pygame](http://www.pygame.org). 4 | 5 | ![](https://github.com/fictorial/pygameui/raw/master/screenshot.png) 6 | 7 | ## Installation 8 | 9 | pip install pygameui 10 | 11 | While Pygame is listed as a dependency in our package metadata, you should 12 | [install it separately](http://www.pygame.org/install.html) ahead of time to 13 | avoid issues with libpng being improperly referenced, etc. 14 | 15 | ## Environment 16 | 17 | Tested on Mac OS X 10.7.3 running system Python 2.7.1 and Pygame installed via 18 | the "Lion apple supplied python" mpkg. Please let me know if you have issues 19 | running this on other versions of Python and/or Pygame. 20 | 21 | ## Author 22 | 23 | Brian Hammond (brian@fictorial.com) 24 | 25 | Copyright © 2012 Fictorial LLC. 26 | 27 | ## Other 28 | 29 | See the `simpler` branch for a much simpler version. 30 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TO DO 2 | 3 | ## Priority 1 4 | 5 | I really hate that I have to setup subviews in init then lay them out in layout; 6 | better would be autoresizing support; or do not take frame as arg to ctors; 7 | just set frame in layout. 8 | 9 | ## Enhancements 10 | 11 | - Make a dark theme 12 | - The only image view content scale mode supported is 'scale to fill' 13 | 14 | ## Views to create 15 | 16 | - color picker (RGB, HSV; see `colorsys` module) 17 | - modal dialogs 18 | - file picker 19 | 20 | ## Bugs 21 | 22 | - Alerts with message text that contains words that are too wide to be 23 | wrapped cause layout problems. If a word is wider than the label's width 24 | and word wrap is enabled, do a character wrap for that word? 25 | - Is scroll view showing all of content view? far right? 26 | - When a list view is in a scroll view and the list view fits, the selected 27 | item does not extend to where the scrollbars are (hidden). For now, I am 28 | simply never hiding vertical scrollbars. 29 | 30 | ## Larger sub-projects 31 | 32 | - Flipbook should not use resource.`get_image()` since that is for 33 | packaged images; or make `get_image` generic 34 | - ScrollbarView should be decoupled from ScrollView; delegate 35 | - No support for layouts; everything is placed in parent-relative coordinates; 36 | Add support for springs and struts auto-resizing ala UIKit. 37 | - No high-level animation support (bounce, slide, fade, etc.) 38 | - GUI builder tool that reads / writes pickles (versioning?) 39 | - CPU utilization is bit high since all controls are redrawn every frame. 40 | I don't really care because this is really just for game prototypes. 41 | - Support multiple windows 42 | - Support resizable main window (after autoresizing is in place) 43 | 44 | ### Text Manipulation 45 | 46 | - No support for text selection 47 | - No cursor 48 | - No support for copy / paste 49 | - No key-bindings other than backspace and key input 50 | -------------------------------------------------------------------------------- /bin/pygameui-kitchensink.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import random 4 | import sys 5 | import os 6 | 7 | 8 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) 9 | import pygameui as ui 10 | 11 | 12 | import logging 13 | log_format = '%(asctime)-6s: %(name)s - %(levelname)s - %(message)s' 14 | console_handler = logging.StreamHandler() 15 | console_handler.setFormatter(logging.Formatter(log_format)) 16 | logger = logging.getLogger() 17 | logger.setLevel(logging.DEBUG) 18 | logger.addHandler(console_handler) 19 | 20 | 21 | LIST_WIDTH = 180 22 | MARGIN = 20 23 | SMALL_MARGIN = 10 24 | 25 | 26 | class KitchenSinkScene(ui.Scene): 27 | def __init__(self): 28 | ui.Scene.__init__(self) 29 | 30 | label_height = ui.theme.current.label_height 31 | scrollbar_size = ui.SCROLLBAR_SIZE 32 | 33 | frame = ui.Rect(MARGIN, MARGIN, 200, label_height) 34 | self.name_textfield = ui.TextField(frame, placeholder='Your name') 35 | self.name_textfield.centerx = self.frame.centerx 36 | self.add_child(self.name_textfield) 37 | 38 | gridview = ui.GridView(ui.Rect(0, 0, 500, 500)) 39 | self.scroll_gridview = ui.ScrollView(ui.Rect( 40 | MARGIN, self.name_textfield.frame.bottom + MARGIN, 41 | 200 - scrollbar_size, 250), gridview) 42 | self.add_child(self.scroll_gridview) 43 | 44 | items = ['Apples', 'Bananas', 'Grapes', 'Cheese', 'Goats', 'Beer'] 45 | labels = [ui.Label(ui.Rect( 46 | 0, 0, LIST_WIDTH, label_height), item, halign=ui.LEFT) 47 | for item in items] 48 | list_view = ui.ListView(ui.Rect(0, 0, LIST_WIDTH, 400), labels) 49 | list_view.on_selected.connect(self.item_selected) 50 | list_view.on_deselected.connect(self.item_deselected) 51 | self.scroll_list = ui.ScrollView(ui.Rect( 52 | MARGIN, self.scroll_gridview.frame.bottom + MARGIN, 53 | LIST_WIDTH, 80), list_view) 54 | self.add_child(self.scroll_list) 55 | 56 | self.greet_button = ui.Button(ui.Rect( 57 | self.name_textfield.frame.right + SMALL_MARGIN, 58 | self.name_textfield.frame.top, 0, 0), 'Submit') 59 | self.greet_button.on_clicked.connect(self.greet) 60 | self.add_child(self.greet_button) 61 | 62 | self.image_view = ui.view_for_image_named('logo') 63 | self.image_view.frame.right = self.frame.right - MARGIN 64 | self.image_view.frame.top = MARGIN 65 | self.add_child(self.image_view) 66 | 67 | self.checkbox = ui.Checkbox(ui.Rect( 68 | self.scroll_gridview.frame.right + MARGIN, 69 | self.scroll_gridview.frame.top, 70 | 200, label_height), 'I eat food') 71 | self.add_child(self.checkbox) 72 | 73 | self.checkbox1 = ui.Checkbox(ui.Rect( 74 | self.checkbox.frame.left, 75 | self.checkbox.frame.bottom + SMALL_MARGIN, 76 | 200, label_height), 'I drink water') 77 | self.add_child(self.checkbox1) 78 | 79 | self.checkbox2 = ui.Checkbox(ui.Rect( 80 | self.checkbox.frame.left, 81 | self.checkbox1.frame.bottom + SMALL_MARGIN, 82 | 200, label_height), 'I exercise regularly') 83 | self.add_child(self.checkbox2) 84 | 85 | info_image = ui.get_image('info') 86 | self.info_button = ui.ImageButton(ui.Rect( 87 | self.checkbox2.frame.left, 88 | self.checkbox2.frame.bottom + MARGIN, 89 | 0, 0), info_image) 90 | self.info_button.on_clicked.connect(self.show_alert) 91 | self.add_child(self.info_button) 92 | 93 | notify_image = ui.get_image('star') 94 | self.notify_button = ui.ImageButton(ui.Rect( 95 | self.info_button.frame.right + MARGIN, 96 | self.info_button.frame.top, 97 | 0, 0), notify_image) 98 | self.notify_button.on_clicked.connect(self.show_notification) 99 | self.add_child(self.notify_button) 100 | 101 | self.task_button = ui.Button(ui.Rect( 102 | self.info_button.frame.left, 103 | self.info_button.frame.bottom + MARGIN, 104 | 0, 0), 'Consume!') 105 | self.task_button.on_clicked.connect(self.run_fake_task) 106 | self.add_child(self.task_button) 107 | 108 | self.running_task = False 109 | self.progress_view = ui.ProgressView(ui.Rect( 110 | self.frame.right - MARGIN - 180, 111 | self.task_button.frame.centery - scrollbar_size // 2, 112 | 180, scrollbar_size)) 113 | self.add_child(self.progress_view) 114 | self.progress_view.hidden = True 115 | 116 | labels2 = [ui.Label( 117 | ui.Rect(0, 0, LIST_WIDTH, label_height), 118 | 'Item %d' % (i + 1)) for i in range(10)] 119 | for l in labels2: 120 | l.halign = ui.LEFT 121 | self.select_view = ui.SelectView(ui.Rect( 122 | self.task_button.frame.left, 123 | self.task_button.frame.bottom + MARGIN, 124 | LIST_WIDTH, label_height), labels2) 125 | self.select_view.on_selection_changed.connect(self.selection_changed) 126 | self.add_child(self.select_view) 127 | 128 | self.hslider = ui.SliderView(ui.Rect( 129 | self.select_view.frame.left, 130 | self.select_view.frame.bottom + MARGIN*2, 131 | 100, scrollbar_size), ui.HORIZONTAL, 0, 100) 132 | self.hslider.on_value_changed.connect(self.value_changed) 133 | self.add_child(self.hslider) 134 | 135 | self.vslider = ui.SliderView(ui.Rect( 136 | self.hslider.frame.right + SMALL_MARGIN, 137 | self.hslider.frame.centery, 138 | scrollbar_size, 100), ui.VERTICAL, 0, 100) 139 | self.vslider.on_value_changed.connect(self.value_changed) 140 | self.add_child(self.vslider) 141 | 142 | self.slider_value = ui.Label(ui.Rect( 143 | self.hslider.frame.centerx - 25, 144 | self.hslider.frame.bottom + MARGIN, 145 | 50, label_height), '') 146 | self.add_child(self.slider_value) 147 | 148 | self.spinner = ui.SpinnerView(ui.Rect( 149 | self.frame.right - MARGIN - ui.SpinnerView.size, 150 | self.frame.bottom - MARGIN - ui.SpinnerView.size, 151 | 0, 0)) 152 | self.add_child(self.spinner) 153 | 154 | def layout(self): 155 | self.checkbox.toggle() 156 | self.checkbox1.toggle() 157 | ui.Scene.layout(self) 158 | 159 | def item_selected(self, list_view, item, index): 160 | item.state = 'selected' 161 | 162 | def item_deselected(self, list_view, item, index): 163 | item.state = 'normal' 164 | 165 | def selection_changed(self, selection_view, item, index): 166 | logger.info('new selection: %s' % str(item)) 167 | 168 | def value_changed(self, slider_view, value): 169 | self.slider_value.text = '%d' % value 170 | 171 | def greet(self, btn, mbtn): 172 | name = self.name_textfield.text.strip() 173 | if len(name) == 0: 174 | name = 'uh, you?' 175 | ui.show_alert(title='Greetings!', message='Hello, %s' % name) 176 | 177 | def show_alert(self, btn, mbtn): 178 | msgs = [ 179 | 'This is an alert', 180 | 'This is an alert with\na line break in it', 181 | 'This is a rather long alert that should ' + 182 | 'automatically word wrap to multiple lines', 183 | 'This is an very long alert that should ' + 184 | 'automatically word wrap to multiple lines...' + 185 | 'is this not the best thing EVAR? wow, mindblowing...' 186 | ] 187 | msg = random.choice(msgs) 188 | ui.show_alert(msg) 189 | 190 | def show_notification(self, btn, mbtn): 191 | msgs = [ 192 | 'Achievement Unlocked! Notification!', 193 | 'This is a notification\nwith a linebreak in it', 194 | 'This notification is rather long and should ' + 195 | 'automatically word wrap to multiple lines' 196 | ] 197 | msg = random.choice(msgs) 198 | ui.show_notification(msg) 199 | 200 | def run_fake_task(self, btn, mbtn): 201 | if not self.running_task: 202 | self.task_button.enabled = False 203 | self.progress_view.hidden = False 204 | self.progress_view.progress = 0 205 | self.running_task = True 206 | 207 | def update(self, dt): 208 | ui.Scene.update(self, dt) 209 | if self.running_task: 210 | progress = min(1.0, self.progress_view.progress + 0.01) 211 | self.progress_view.progress = progress 212 | self.running_task = (self.progress_view.progress < 1.0) 213 | self.task_button.enabled = not self.running_task 214 | if self.task_button.enabled: 215 | ui.show_alert("I'M FINISHED!", title='Milkshake') 216 | self.progress_view.progress = 0 217 | self.progress_view.hidden = True 218 | 219 | 220 | if __name__ == '__main__': 221 | ui.init('pygameui - Kitchen Sink') 222 | ui.scene.push(KitchenSinkScene()) 223 | ui.run() 224 | -------------------------------------------------------------------------------- /distribute_setup.py: -------------------------------------------------------------------------------- 1 | #!python 2 | """Bootstrap distribute installation 3 | 4 | If you want to use setuptools in your package's setup.py, just include this 5 | file in the same directory with it, and add this to the top of your setup.py:: 6 | 7 | from distribute_setup import use_setuptools 8 | use_setuptools() 9 | 10 | If you want to require a specific version of setuptools, set a download 11 | mirror, or use an alternate download directory, you can do so by supplying 12 | the appropriate options to ``use_setuptools()``. 13 | 14 | This file can also be run as a script to install or upgrade setuptools. 15 | """ 16 | import os 17 | import sys 18 | import time 19 | import fnmatch 20 | import tempfile 21 | import tarfile 22 | from distutils import log 23 | 24 | try: 25 | from site import USER_SITE 26 | except ImportError: 27 | USER_SITE = None 28 | 29 | try: 30 | import subprocess 31 | 32 | def _python_cmd(*args): 33 | args = (sys.executable,) + args 34 | return subprocess.call(args) == 0 35 | 36 | except ImportError: 37 | # will be used for python 2.3 38 | def _python_cmd(*args): 39 | args = (sys.executable,) + args 40 | # quoting arguments if windows 41 | if sys.platform == 'win32': 42 | def quote(arg): 43 | if ' ' in arg: 44 | return '"%s"' % arg 45 | return arg 46 | args = [quote(arg) for arg in args] 47 | return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 48 | 49 | DEFAULT_VERSION = "0.6.25" 50 | DEFAULT_URL = "https://pypi.python.org/packages/source/d/distribute/" 51 | SETUPTOOLS_FAKED_VERSION = "0.6c11" 52 | 53 | SETUPTOOLS_PKG_INFO = """\ 54 | Metadata-Version: 1.0 55 | Name: setuptools 56 | Version: %s 57 | Summary: xxxx 58 | Home-page: xxx 59 | Author: xxx 60 | Author-email: xxx 61 | License: xxx 62 | Description: xxx 63 | """ % SETUPTOOLS_FAKED_VERSION 64 | 65 | 66 | def _install(tarball, install_args=()): 67 | # extracting the tarball 68 | tmpdir = tempfile.mkdtemp() 69 | log.warn('Extracting in %s', tmpdir) 70 | old_wd = os.getcwd() 71 | try: 72 | os.chdir(tmpdir) 73 | tar = tarfile.open(tarball) 74 | _extractall(tar) 75 | tar.close() 76 | 77 | # going in the directory 78 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 79 | os.chdir(subdir) 80 | log.warn('Now working in %s', subdir) 81 | 82 | # installing 83 | log.warn('Installing Distribute') 84 | if not _python_cmd('setup.py', 'install', *install_args): 85 | log.warn('Something went wrong during the installation.') 86 | log.warn('See the error message above.') 87 | finally: 88 | os.chdir(old_wd) 89 | 90 | 91 | def _build_egg(egg, tarball, to_dir): 92 | # extracting the tarball 93 | tmpdir = tempfile.mkdtemp() 94 | log.warn('Extracting in %s', tmpdir) 95 | old_wd = os.getcwd() 96 | try: 97 | os.chdir(tmpdir) 98 | tar = tarfile.open(tarball) 99 | _extractall(tar) 100 | tar.close() 101 | 102 | # going in the directory 103 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 104 | os.chdir(subdir) 105 | log.warn('Now working in %s', subdir) 106 | 107 | # building an egg 108 | log.warn('Building a Distribute egg in %s', to_dir) 109 | _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) 110 | 111 | finally: 112 | os.chdir(old_wd) 113 | # returning the result 114 | log.warn(egg) 115 | if not os.path.exists(egg): 116 | raise IOError('Could not build the egg.') 117 | 118 | 119 | def _do_download(version, download_base, to_dir, download_delay): 120 | egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' 121 | % (version, sys.version_info[0], sys.version_info[1])) 122 | if not os.path.exists(egg): 123 | tarball = download_setuptools(version, download_base, 124 | to_dir, download_delay) 125 | _build_egg(egg, tarball, to_dir) 126 | sys.path.insert(0, egg) 127 | import setuptools 128 | setuptools.bootstrap_install_from = egg 129 | 130 | 131 | def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 132 | to_dir=os.curdir, download_delay=15, no_fake=True): 133 | # making sure we use the absolute path 134 | to_dir = os.path.abspath(to_dir) 135 | was_imported = 'pkg_resources' in sys.modules or \ 136 | 'setuptools' in sys.modules 137 | try: 138 | try: 139 | import pkg_resources 140 | if not hasattr(pkg_resources, '_distribute'): 141 | if not no_fake: 142 | _fake_setuptools() 143 | raise ImportError 144 | except ImportError: 145 | return _do_download(version, download_base, to_dir, download_delay) 146 | try: 147 | pkg_resources.require("distribute>="+version) 148 | return 149 | except pkg_resources.VersionConflict: 150 | e = sys.exc_info()[1] 151 | if was_imported: 152 | sys.stderr.write( 153 | "The required version of distribute (>=%s) is not available,\n" 154 | "and can't be installed while this script is running. Please\n" 155 | "install a more recent version first, using\n" 156 | "'easy_install -U distribute'." 157 | "\n\n(Currently using %r)\n" % (version, e.args[0])) 158 | sys.exit(2) 159 | else: 160 | del pkg_resources, sys.modules['pkg_resources'] # reload ok 161 | return _do_download(version, download_base, to_dir, 162 | download_delay) 163 | except pkg_resources.DistributionNotFound: 164 | return _do_download(version, download_base, to_dir, 165 | download_delay) 166 | finally: 167 | if not no_fake: 168 | _create_fake_setuptools_pkg_info(to_dir) 169 | 170 | def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 171 | to_dir=os.curdir, delay=15): 172 | """Download distribute from a specified location and return its filename 173 | 174 | `version` should be a valid distribute version number that is available 175 | as an egg for download under the `download_base` URL (which should end 176 | with a '/'). `to_dir` is the directory where the egg will be downloaded. 177 | `delay` is the number of seconds to pause before an actual download 178 | attempt. 179 | """ 180 | # making sure we use the absolute path 181 | to_dir = os.path.abspath(to_dir) 182 | try: 183 | from urllib.request import urlopen 184 | except ImportError: 185 | from urllib2 import urlopen 186 | tgz_name = "distribute-%s.tar.gz" % version 187 | url = download_base + tgz_name 188 | saveto = os.path.join(to_dir, tgz_name) 189 | src = dst = None 190 | if not os.path.exists(saveto): # Avoid repeated downloads 191 | try: 192 | log.warn("Downloading %s", url) 193 | src = urlopen(url) 194 | # Read/write all in one block, so we don't create a corrupt file 195 | # if the download is interrupted. 196 | data = src.read() 197 | dst = open(saveto, "wb") 198 | dst.write(data) 199 | finally: 200 | if src: 201 | src.close() 202 | if dst: 203 | dst.close() 204 | return os.path.realpath(saveto) 205 | 206 | def _no_sandbox(function): 207 | def __no_sandbox(*args, **kw): 208 | try: 209 | from setuptools.sandbox import DirectorySandbox 210 | if not hasattr(DirectorySandbox, '_old'): 211 | def violation(*args): 212 | pass 213 | DirectorySandbox._old = DirectorySandbox._violation 214 | DirectorySandbox._violation = violation 215 | patched = True 216 | else: 217 | patched = False 218 | except ImportError: 219 | patched = False 220 | 221 | try: 222 | return function(*args, **kw) 223 | finally: 224 | if patched: 225 | DirectorySandbox._violation = DirectorySandbox._old 226 | del DirectorySandbox._old 227 | 228 | return __no_sandbox 229 | 230 | def _patch_file(path, content): 231 | """Will backup the file then patch it""" 232 | existing_content = open(path).read() 233 | if existing_content == content: 234 | # already patched 235 | log.warn('Already patched.') 236 | return False 237 | log.warn('Patching...') 238 | _rename_path(path) 239 | f = open(path, 'w') 240 | try: 241 | f.write(content) 242 | finally: 243 | f.close() 244 | return True 245 | 246 | _patch_file = _no_sandbox(_patch_file) 247 | 248 | def _same_content(path, content): 249 | return open(path).read() == content 250 | 251 | def _rename_path(path): 252 | new_name = path + '.OLD.%s' % time.time() 253 | log.warn('Renaming %s into %s', path, new_name) 254 | os.rename(path, new_name) 255 | return new_name 256 | 257 | def _remove_flat_installation(placeholder): 258 | if not os.path.isdir(placeholder): 259 | log.warn('Unkown installation at %s', placeholder) 260 | return False 261 | found = False 262 | for file in os.listdir(placeholder): 263 | if fnmatch.fnmatch(file, 'setuptools*.egg-info'): 264 | found = True 265 | break 266 | if not found: 267 | log.warn('Could not locate setuptools*.egg-info') 268 | return 269 | 270 | log.warn('Removing elements out of the way...') 271 | pkg_info = os.path.join(placeholder, file) 272 | if os.path.isdir(pkg_info): 273 | patched = _patch_egg_dir(pkg_info) 274 | else: 275 | patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) 276 | 277 | if not patched: 278 | log.warn('%s already patched.', pkg_info) 279 | return False 280 | # now let's move the files out of the way 281 | for element in ('setuptools', 'pkg_resources.py', 'site.py'): 282 | element = os.path.join(placeholder, element) 283 | if os.path.exists(element): 284 | _rename_path(element) 285 | else: 286 | log.warn('Could not find the %s element of the ' 287 | 'Setuptools distribution', element) 288 | return True 289 | 290 | _remove_flat_installation = _no_sandbox(_remove_flat_installation) 291 | 292 | def _after_install(dist): 293 | log.warn('After install bootstrap.') 294 | placeholder = dist.get_command_obj('install').install_purelib 295 | _create_fake_setuptools_pkg_info(placeholder) 296 | 297 | def _create_fake_setuptools_pkg_info(placeholder): 298 | if not placeholder or not os.path.exists(placeholder): 299 | log.warn('Could not find the install location') 300 | return 301 | pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) 302 | setuptools_file = 'setuptools-%s-py%s.egg-info' % \ 303 | (SETUPTOOLS_FAKED_VERSION, pyver) 304 | pkg_info = os.path.join(placeholder, setuptools_file) 305 | if os.path.exists(pkg_info): 306 | log.warn('%s already exists', pkg_info) 307 | return 308 | 309 | log.warn('Creating %s', pkg_info) 310 | f = open(pkg_info, 'w') 311 | try: 312 | f.write(SETUPTOOLS_PKG_INFO) 313 | finally: 314 | f.close() 315 | 316 | pth_file = os.path.join(placeholder, 'setuptools.pth') 317 | log.warn('Creating %s', pth_file) 318 | f = open(pth_file, 'w') 319 | try: 320 | f.write(os.path.join(os.curdir, setuptools_file)) 321 | finally: 322 | f.close() 323 | 324 | _create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info) 325 | 326 | def _patch_egg_dir(path): 327 | # let's check if it's already patched 328 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 329 | if os.path.exists(pkg_info): 330 | if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): 331 | log.warn('%s already patched.', pkg_info) 332 | return False 333 | _rename_path(path) 334 | os.mkdir(path) 335 | os.mkdir(os.path.join(path, 'EGG-INFO')) 336 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 337 | f = open(pkg_info, 'w') 338 | try: 339 | f.write(SETUPTOOLS_PKG_INFO) 340 | finally: 341 | f.close() 342 | return True 343 | 344 | _patch_egg_dir = _no_sandbox(_patch_egg_dir) 345 | 346 | def _before_install(): 347 | log.warn('Before install bootstrap.') 348 | _fake_setuptools() 349 | 350 | 351 | def _under_prefix(location): 352 | if 'install' not in sys.argv: 353 | return True 354 | args = sys.argv[sys.argv.index('install')+1:] 355 | for index, arg in enumerate(args): 356 | for option in ('--root', '--prefix'): 357 | if arg.startswith('%s=' % option): 358 | top_dir = arg.split('root=')[-1] 359 | return location.startswith(top_dir) 360 | elif arg == option: 361 | if len(args) > index: 362 | top_dir = args[index+1] 363 | return location.startswith(top_dir) 364 | if arg == '--user' and USER_SITE is not None: 365 | return location.startswith(USER_SITE) 366 | return True 367 | 368 | 369 | def _fake_setuptools(): 370 | log.warn('Scanning installed packages') 371 | try: 372 | import pkg_resources 373 | except ImportError: 374 | # we're cool 375 | log.warn('Setuptools or Distribute does not seem to be installed.') 376 | return 377 | ws = pkg_resources.working_set 378 | try: 379 | setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', 380 | replacement=False)) 381 | except TypeError: 382 | # old distribute API 383 | setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) 384 | 385 | if setuptools_dist is None: 386 | log.warn('No setuptools distribution found') 387 | return 388 | # detecting if it was already faked 389 | setuptools_location = setuptools_dist.location 390 | log.warn('Setuptools installation detected at %s', setuptools_location) 391 | 392 | # if --root or --preix was provided, and if 393 | # setuptools is not located in them, we don't patch it 394 | if not _under_prefix(setuptools_location): 395 | log.warn('Not patching, --root or --prefix is installing Distribute' 396 | ' in another location') 397 | return 398 | 399 | # let's see if its an egg 400 | if not setuptools_location.endswith('.egg'): 401 | log.warn('Non-egg installation') 402 | res = _remove_flat_installation(setuptools_location) 403 | if not res: 404 | return 405 | else: 406 | log.warn('Egg installation') 407 | pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') 408 | if (os.path.exists(pkg_info) and 409 | _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): 410 | log.warn('Already patched.') 411 | return 412 | log.warn('Patching...') 413 | # let's create a fake egg replacing setuptools one 414 | res = _patch_egg_dir(setuptools_location) 415 | if not res: 416 | return 417 | log.warn('Patched done.') 418 | _relaunch() 419 | 420 | 421 | def _relaunch(): 422 | log.warn('Relaunching...') 423 | # we have to relaunch the process 424 | # pip marker to avoid a relaunch bug 425 | if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']: 426 | sys.argv[0] = 'setup.py' 427 | args = [sys.executable] + sys.argv 428 | sys.exit(subprocess.call(args)) 429 | 430 | 431 | def _extractall(self, path=".", members=None): 432 | """Extract all members from the archive to the current working 433 | directory and set owner, modification time and permissions on 434 | directories afterwards. `path' specifies a different directory 435 | to extract to. `members' is optional and must be a subset of the 436 | list returned by getmembers(). 437 | """ 438 | import copy 439 | import operator 440 | from tarfile import ExtractError 441 | directories = [] 442 | 443 | if members is None: 444 | members = self 445 | 446 | for tarinfo in members: 447 | if tarinfo.isdir(): 448 | # Extract directories with a safe mode. 449 | directories.append(tarinfo) 450 | tarinfo = copy.copy(tarinfo) 451 | tarinfo.mode = 448 # decimal for oct 0700 452 | self.extract(tarinfo, path) 453 | 454 | # Reverse sort directories. 455 | if sys.version_info < (2, 4): 456 | def sorter(dir1, dir2): 457 | return cmp(dir1.name, dir2.name) 458 | directories.sort(sorter) 459 | directories.reverse() 460 | else: 461 | directories.sort(key=operator.attrgetter('name'), reverse=True) 462 | 463 | # Set correct owner, mtime and filemode on directories. 464 | for tarinfo in directories: 465 | dirpath = os.path.join(path, tarinfo.name) 466 | try: 467 | self.chown(tarinfo, dirpath) 468 | self.utime(tarinfo, dirpath) 469 | self.chmod(tarinfo, dirpath) 470 | except ExtractError: 471 | e = sys.exc_info()[1] 472 | if self.errorlevel > 1: 473 | raise 474 | else: 475 | self._dbg(1, "tarfile: %s" % e) 476 | 477 | def _build_install_args(argv): 478 | install_args = [] 479 | user_install = '--user' in argv 480 | if user_install and sys.version_info < (2,6): 481 | log.warn("--user requires Python 2.6 or later") 482 | raise SystemExit(1) 483 | if user_install: 484 | install_args.append('--user') 485 | return install_args 486 | 487 | def main(argv, version=DEFAULT_VERSION): 488 | """Install or upgrade setuptools and EasyInstall""" 489 | tarball = download_setuptools() 490 | _install(tarball, _build_install_args(argv)) 491 | 492 | 493 | if __name__ == '__main__': 494 | main(sys.argv[1:]) 495 | -------------------------------------------------------------------------------- /pygameui/__init__.py: -------------------------------------------------------------------------------- 1 | """A simple GUI framework for Pygame. 2 | 3 | This framework is not meant as a competitor to PyQt or other, perhaps more 4 | formal, GUI frameworks. Instead, pygameui is but a simple framework for game 5 | prototypes. 6 | 7 | The app is comprised of a stack of scenes; the top-most or current scene is 8 | what is displayed in the window. Scenes are comprised of Views which are 9 | comprised of other Views. pygameui contains view classes for things like 10 | labels, buttons, and scrollbars. 11 | 12 | pygameui is a framework, not a library. While you write view controllers in the 13 | form of scenes, pygameui will run the overall application by running a loop 14 | that receives device events (mouse button clicks, keyboard presses, etc.) and 15 | dispatches the events to the relevant view(s) in your scene(s). 16 | 17 | Each view in pygameui is rectangular in shape and whose dimensions are 18 | determined by the view's "frame". A view is backed by a Pygame surface. 19 | Altering a view's frame requires that you call 'relayout' which will resize the 20 | view's backing surface and give each child view a chance to reposition and/or 21 | resize itself in response. 22 | 23 | Events on views can trigger response code that you control. For instance, when 24 | a button is clicked, your code can be called back. The click is a "signal" and 25 | your code is a "slot". The view classes define various signals to which you 26 | connect zero or more slots. 27 | 28 | a_button.on_clicked.connect(click_callback) 29 | 30 | """ 31 | 32 | AUTHOR = 'Brian Hammond ' 33 | COPYRIGHT = 'Copyright (C) 2012 Fictorial LLC.' 34 | LICENSE = 'MIT' 35 | 36 | __version__ = '0.2.0' 37 | 38 | 39 | import pygame 40 | 41 | from alert import * 42 | from button import * 43 | from callback import * 44 | from checkbox import * 45 | from dialog import * 46 | from flipbook import * 47 | from grid import * 48 | from imagebutton import * 49 | from imageview import * 50 | from label import * 51 | from listview import * 52 | from notification import * 53 | from progress import * 54 | from render import * 55 | from resource import * 56 | from scroll import * 57 | from select import * 58 | from slider import * 59 | from spinner import * 60 | from textfield import * 61 | from view import * 62 | 63 | import focus 64 | import window 65 | import scene 66 | import theme 67 | 68 | from scene import Scene 69 | 70 | 71 | import logging 72 | logger = logging.getLogger(__name__) 73 | 74 | 75 | Rect = pygame.Rect 76 | window_surface = None 77 | 78 | 79 | def init(name='', window_size=(640, 480)): 80 | logger.debug('init %s %s' % (__name__, __version__)) 81 | pygame.init() 82 | logger.debug('pygame %s' % pygame.__version__) 83 | pygame.key.set_repeat(200, 50) 84 | global window_surface 85 | window_surface = pygame.display.set_mode(window_size) 86 | pygame.display.set_caption(name) 87 | window.rect = pygame.Rect((0, 0), window_size) 88 | theme.init() 89 | 90 | 91 | def run(): 92 | assert len(scene.stack) > 0 93 | 94 | clock = pygame.time.Clock() 95 | down_in_view = None 96 | 97 | elapsed = 0 98 | 99 | while True: 100 | dt = clock.tick(60) 101 | 102 | elapsed += dt 103 | if elapsed > 5000: 104 | elapsed = 0 105 | logger.debug('%d FPS', clock.get_fps()) 106 | 107 | for e in pygame.event.get(): 108 | if e.type == pygame.QUIT: 109 | pygame.quit() 110 | import sys 111 | sys.exit() 112 | 113 | mousepoint = pygame.mouse.get_pos() 114 | 115 | if e.type == pygame.MOUSEBUTTONDOWN: 116 | hit_view = scene.current.hit(mousepoint) 117 | logger.debug('hit %s' % hit_view) 118 | if (hit_view is not None and 119 | not isinstance(hit_view, scene.Scene)): 120 | focus.set(hit_view) 121 | down_in_view = hit_view 122 | pt = hit_view.from_window(mousepoint) 123 | hit_view.mouse_down(e.button, pt) 124 | else: 125 | focus.set(None) 126 | elif e.type == pygame.MOUSEBUTTONUP: 127 | hit_view = scene.current.hit(mousepoint) 128 | if hit_view is not None: 129 | if down_in_view and hit_view != down_in_view: 130 | down_in_view.blurred() 131 | focus.set(None) 132 | pt = hit_view.from_window(mousepoint) 133 | hit_view.mouse_up(e.button, pt) 134 | down_in_view = None 135 | elif e.type == pygame.MOUSEMOTION: 136 | if down_in_view and down_in_view.draggable: 137 | pt = down_in_view.from_window(mousepoint) 138 | down_in_view.mouse_drag(pt, e.rel) 139 | else: 140 | scene.current.mouse_motion(mousepoint) 141 | elif e.type == pygame.KEYDOWN: 142 | if focus.view: 143 | focus.view.key_down(e.key, e.unicode) 144 | else: 145 | scene.current.key_down(e.key, e.unicode) 146 | elif e.type == pygame.KEYUP: 147 | if focus.view: 148 | focus.view.key_up(e.key) 149 | else: 150 | scene.current.key_up(e.key) 151 | 152 | scene.current.update(dt / 1000.0) 153 | scene.current.draw() 154 | window_surface.blit(scene.current.surface, (0, 0)) 155 | pygame.display.flip() 156 | -------------------------------------------------------------------------------- /pygameui/alert.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | import dialog 4 | import label 5 | import theme 6 | import button 7 | import window 8 | 9 | 10 | OK = 1 11 | CANCEL = 2 12 | 13 | 14 | class AlertView(dialog.DialogView): 15 | """A non-modal alert dialog box.""" 16 | 17 | def __init__(self, title, message, buttons=0xFF): 18 | dialog.DialogView.__init__(self, pygame.Rect(0, 0, 1, 1)) 19 | 20 | self.title = title 21 | self.message = message 22 | self.buttons = buttons 23 | 24 | self.message_label = label.Label(pygame.Rect(0, 0, 1, 1), 25 | message, wrap=label.WORD_WRAP) 26 | self.message_label.valign = label.TOP 27 | self.add_child(self.message_label) 28 | 29 | self.title_label = label.Label(pygame.Rect(0, 0, 1, 1), title) 30 | self.add_child(self.title_label) 31 | 32 | self.ok = button.Button(pygame.Rect(0, 0, 0, 0), 'OK') 33 | self.ok.on_clicked.connect(self._dismiss) 34 | self.add_child(self.ok) 35 | 36 | self.cancel = button.Button(pygame.Rect(0, 0, 0, 0), 'Cancel') 37 | self.cancel.on_clicked.connect(self._dismiss) 38 | self.add_child(self.cancel) 39 | 40 | def layout(self): 41 | self.frame.w = max(100, window.rect.w // 3) 42 | self.frame.h = max(100, window.rect.h // 3) 43 | 44 | self.title_label.frame.topleft = self.padding 45 | self.title_label.frame.w = self.frame.w - self.padding[0] * 2 46 | self.title_label.frame.h = theme.current.label_height 47 | self.title_label.layout() 48 | 49 | self.message_label.frame.top = (self.title_label.frame.bottom + 50 | max(self.message_label.margin[1], 51 | self.title_label.margin[1])) 52 | self.message_label.frame.w = self.frame.w - self.padding[0] * 2 53 | self.message_label.render() 54 | self.message_label.shrink_wrap() 55 | self.message_label.frame.centerx = self.frame.w // 2 56 | 57 | assert self.ok.margin[1] == self.cancel.margin[1] 58 | 59 | self.ok.frame.h = theme.current.button_height 60 | self.cancel.frame.h = theme.current.button_height 61 | 62 | btn_top = (self.message_label.frame.bottom + 63 | max(self.message_label.margin[1], 64 | self.ok.margin[1])) 65 | self.ok.frame.top = btn_top 66 | self.cancel.frame.top = btn_top 67 | 68 | if self.buttons & CANCEL: 69 | self.cancel.hidden = False 70 | buttons_width = (self.ok.frame.w + 71 | max(self.ok.margin[0], self.cancel.margin[0]) + 72 | self.cancel.frame.w) 73 | 74 | self.ok.frame.centerx = self.frame.w // 2 - buttons_width // 2 75 | self.cancel.frame.centerx = self.frame.w // 2 + buttons_width // 2 76 | else: 77 | self.cancel.hidden = True 78 | self.ok.frame.centerx = self.frame.w // 2 79 | 80 | self.ok.layout() 81 | self.cancel.layout() 82 | 83 | self.frame.h = (self.padding[1] + 84 | self.title_label.frame.h + 85 | max(self.title_label.margin[1], 86 | self.message_label.margin[1]) + 87 | self.message_label.frame.h + 88 | max(self.message_label.margin[1], 89 | self.ok.margin[1]) + 90 | self.ok.frame.h + 91 | self.padding[1]) 92 | 93 | dialog.DialogView.layout(self) 94 | 95 | def _dismiss(self, btn, mbtn): 96 | self.dismiss() 97 | 98 | def key_down(self, key, code): 99 | dialog.DialogView.key_down(self, key, code) 100 | if key == pygame.K_RETURN: # ~ ok 101 | self.dismiss() 102 | 103 | 104 | def show_alert(message, title='Info', buttons=OK): 105 | alert_view = AlertView(title, message, buttons) 106 | import scene 107 | scene.current.add_child(alert_view) 108 | alert_view.focus() 109 | alert_view.center() 110 | -------------------------------------------------------------------------------- /pygameui/button.py: -------------------------------------------------------------------------------- 1 | import label 2 | import callback 3 | import focus 4 | import theme 5 | 6 | 7 | class Button(label.Label): 8 | """A button with a text caption. 9 | 10 | Essentially an interactive label. 11 | 12 | Signals 13 | 14 | on_clicked(button, mousebutton) 15 | 16 | """ 17 | 18 | def __init__(self, frame, caption): 19 | if frame.h == 0: 20 | frame.h = theme.current.button_height 21 | label.Label.__init__(self, frame, caption) 22 | self._enabled = True 23 | self.on_clicked = callback.Signal() 24 | 25 | def layout(self): 26 | label.Label.layout(self) 27 | if self.frame.w == 0: 28 | self.frame.w = self.text_size[0] + self.padding[0] * 2 29 | label.Label.layout(self) 30 | 31 | def mouse_up(self, button, point): 32 | focus.set(None) 33 | self.on_clicked(self, button) 34 | -------------------------------------------------------------------------------- /pygameui/callback.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Signal(object): 4 | "A simple signal - slot mechanism" 5 | 6 | # TODO use weak refs? really? 7 | 8 | def __init__(self): 9 | self.slots = [] 10 | 11 | def connect(self, slot): 12 | "slot: is a function / method" 13 | 14 | assert callable(slot) 15 | self.slots.append(slot) 16 | 17 | def __call__(self, *args, **kwargs): 18 | "Fire the signal to connected slots" 19 | 20 | for slot in self.slots: 21 | slot(*args, **kwargs) 22 | -------------------------------------------------------------------------------- /pygameui/checkbox.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | import view 4 | import callback 5 | import label 6 | import theme 7 | import focus 8 | 9 | 10 | class Checkbox(view.View): 11 | """A checkbox. 12 | 13 | Signals 14 | 15 | on_checked(checkbox) 16 | on_unchecked(checkbox) 17 | 18 | """ 19 | 20 | def __init__(self, frame, text): 21 | view.View.__init__(self, frame) 22 | 23 | self.checked = False 24 | 25 | check_frame = pygame.Rect(0, 0, 1, 1) 26 | self.check_label = label.Label(check_frame, ' ') 27 | self.add_child(self.check_label) 28 | 29 | self.label = label.Label(pygame.Rect(0, 0, 1, 1), text) 30 | self.add_child(self.label) 31 | 32 | self.on_checked = callback.Signal() 33 | self.on_unchecked = callback.Signal() 34 | 35 | def layout(self): 36 | self.check_label.frame.topleft = self.padding 37 | check_size = theme.current.label_height - self.padding[1] * 2 38 | self.check_label.frame.w = check_size 39 | self.check_label.frame.h = check_size 40 | self.check_label.layout() 41 | 42 | self.label.shrink_wrap() 43 | margin = max(self.check_label.margin[0], self.label.margin[0]) 44 | self.label.frame.top = self.padding[1] 45 | self.label.frame.left = self.check_label.frame.right + margin 46 | self.label.frame.h = check_size 47 | self.label.layout() 48 | 49 | self.frame.w = (self.check_label.frame.w + margin + 50 | self.label.frame.w + self.padding[0] * 2) 51 | self.frame.h = theme.current.label_height 52 | 53 | view.View.layout(self) 54 | 55 | def mouse_up(self, button, point): 56 | view.View.mouse_up(self, button, point) 57 | self.toggle() 58 | focus.set(None) 59 | 60 | def toggle(self, *args, **kwargs): 61 | self.checked = not self.checked 62 | if self.checked: 63 | self.check_label.text = 'X' 64 | self.on_checked() 65 | else: 66 | self.check_label.text = ' ' 67 | self.on_unchecked() 68 | 69 | def __repr__(self): 70 | return self.label.text 71 | -------------------------------------------------------------------------------- /pygameui/colors.py: -------------------------------------------------------------------------------- 1 | """A few pre-computed colors. 2 | 3 | * a 3-tuple is RGB 4 | * a 4-tuple is RGBA 5 | * a pair of tuples signifies a linear gradient 6 | 7 | """ 8 | 9 | clear_color = (0, 0, 0, 0) 10 | 11 | white_color = (255, 255, 255) 12 | whites_twin_color = (245, 245, 245) 13 | near_white_color = (240, 240, 240) 14 | light_gray_color = (192, 192, 192) 15 | gray_color = (128, 128, 128) 16 | dark_gray_color = (100, 100, 100) 17 | black_color = (0, 0, 0) 18 | 19 | red_color = (255, 0, 0) 20 | orange_color = (255, 128, 0) 21 | yellow_color = (255, 255, 0) 22 | green_color = (173, 222, 78) 23 | blue_color = (0, 0, 255) 24 | indigo_color = (75, 0, 130) 25 | violet_color = (127, 0, 255) 26 | -------------------------------------------------------------------------------- /pygameui/dialog.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | import view 4 | import focus 5 | import callback 6 | 7 | 8 | class DialogView(view.View): 9 | """A non-modal dialog box. 10 | 11 | Signals 12 | 13 | on_dismissed(dialog) 14 | """ 15 | 16 | def __init__(self, frame): 17 | view.View.__init__(self, frame) 18 | self.on_dismissed = callback.Signal() 19 | 20 | def dismiss(self): 21 | self.rm() 22 | focus.set(None) 23 | self.on_dismissed() 24 | 25 | def key_down(self, key, code): 26 | if key == pygame.K_ESCAPE: 27 | self.dismiss() 28 | -------------------------------------------------------------------------------- /pygameui/flipbook.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | import view 4 | 5 | 6 | class FlipbookView(view.View): 7 | """Flipbook-style image animation view. 8 | 9 | Displays animation frames stored in equally sized rectangles of 10 | a single "sprite sheet" image file. 11 | 12 | Only works with sheets with N columns in a single row. 13 | 14 | """ 15 | 16 | def __init__(self, frame, image, delay=1/10.0): 17 | """Create a flipbook view. 18 | 19 | frame.topleft 20 | 21 | where to position the view. 22 | 23 | frame.size 24 | 25 | size of each sub-image. 26 | 27 | image 28 | 29 | the spritesheet image. 30 | 31 | """ 32 | view.View.__init__(self, frame) 33 | self.image = image 34 | self.frame_count = self.image.get_size()[0] // frame.size[0] 35 | self.current_frame = 0 36 | self.delay = delay 37 | self.elapsed = 0 38 | 39 | def update(self, dt): 40 | view.View.update(self, dt) 41 | self.elapsed += dt 42 | if self.elapsed > self.delay: 43 | self.current_frame = (self.current_frame + 1) % self.frame_count 44 | self.elapsed = 0 45 | 46 | def draw(self): 47 | if not view.View.draw(self): 48 | return False 49 | 50 | rect = pygame.Rect((self.current_frame * self.frame.size[0], 0), 51 | self.frame.size) 52 | 53 | self.surface.blit(self.image, (0, 0), rect) 54 | return True 55 | -------------------------------------------------------------------------------- /pygameui/focus.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger(__name__) 3 | 4 | 5 | view = None 6 | 7 | 8 | def set(new_focus): 9 | global view 10 | 11 | prev_focus = view 12 | view = new_focus 13 | 14 | if view is not None: 15 | logger.debug('focus given to %s' % view) 16 | view.focused() 17 | else: 18 | logger.debug('focus cleared') 19 | 20 | if prev_focus and prev_focus != new_focus: 21 | prev_focus.blurred() 22 | -------------------------------------------------------------------------------- /pygameui/grid.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | import view 4 | 5 | 6 | class GridView(view.View): 7 | """A view which renders a uniform 2-D grid using solid lines.""" 8 | 9 | def __init__(self, frame, spacing=50): 10 | view.View.__init__(self, frame) 11 | self.spacing = spacing 12 | 13 | def layout(self): 14 | view.View.layout(self) 15 | 16 | def draw(self): 17 | if not view.View.draw(self): 18 | return False 19 | 20 | for y in range(self.spacing, self.frame.h, self.spacing): 21 | pygame.draw.line(self.surface, self.line_color, 22 | (0, y), (self.frame.w, y)) 23 | 24 | for x in range(self.spacing, self.frame.w, self.spacing): 25 | pygame.draw.line(self.surface, self.line_color, 26 | (x, 0), (x, self.frame.h)) 27 | 28 | return True 29 | -------------------------------------------------------------------------------- /pygameui/imagebutton.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | import view 4 | import callback 5 | import imageview 6 | import focus 7 | 8 | 9 | class ImageButton(view.View): 10 | """A button that uses an image instead of a text caption. 11 | 12 | Signals 13 | 14 | on_clicked(button, mousebutton) 15 | 16 | """ 17 | 18 | def __init__(self, frame, image): 19 | if frame is None: 20 | frame = pygame.Rect((0, 0), image.get_size()) 21 | elif frame.w == 0 or frame.h == 0: 22 | frame.size = image.get_size() 23 | 24 | view.View.__init__(self, frame) 25 | 26 | self.on_clicked = callback.Signal() 27 | 28 | self.image_view = imageview.ImageView(pygame.Rect(0, 0, 0, 0), image) 29 | self.image_view._enabled = False 30 | self.add_child(self.image_view) 31 | 32 | def layout(self): 33 | self.frame.w = self.padding[0] * 2 + self.image_view.frame.w 34 | self.frame.h = self.padding[1] * 2 + self.image_view.frame.h 35 | self.image_view.frame.topleft = self.padding 36 | self.image_view.layout() 37 | view.View.layout(self) 38 | 39 | def mouse_up(self, button, point): 40 | focus.set(None) 41 | self.on_clicked(self, button) 42 | -------------------------------------------------------------------------------- /pygameui/imageview.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | import view 4 | import resource 5 | 6 | 7 | SCALE_TO_FILL = 0 8 | 9 | 10 | class ImageView(view.View): 11 | """A view for displaying an image. 12 | 13 | The only 'content scaling mode' currently supported is 'scale-to-fill'. 14 | 15 | """ 16 | 17 | def __init__(self, frame, img, content_mode=SCALE_TO_FILL): 18 | """Create an image view from an image. 19 | 20 | frame.topleft 21 | 22 | where to position the view. 23 | 24 | frame.size 25 | 26 | if (0, 0) the frame.size is set to the image's size; 27 | otherwise, the image is scaled to this size. 28 | 29 | """ 30 | 31 | assert img is not None 32 | 33 | if frame is None: 34 | frame = pygame.Rect((0, 0), img.get_size()) 35 | elif frame.w == 0 and frame.h == 0: 36 | frame.size = img.get_size() 37 | 38 | view.View.__init__(self, frame) 39 | 40 | self._enabled = False 41 | self.content_mode = content_mode 42 | self.image = img 43 | 44 | @property 45 | def image(self): 46 | return self._image 47 | 48 | @image.setter 49 | def image(self, new_image): 50 | self._image = new_image 51 | 52 | def layout(self): 53 | assert self.padding[0] == 0 and self.padding[1] == 0 54 | if self.content_mode == SCALE_TO_FILL: 55 | self._image = resource.scale_image(self._image, self.frame.size) 56 | else: 57 | assert False, "Unknown content_mode" 58 | view.View.layout(self) 59 | 60 | def draw(self): 61 | self.surface = self._image 62 | 63 | 64 | def view_for_image_named(image_name): 65 | """Create an ImageView for the given image.""" 66 | 67 | image = resource.get_image(image_name) 68 | 69 | if not image: 70 | return None 71 | 72 | return ImageView(pygame.Rect(0, 0, 0, 0), image) 73 | -------------------------------------------------------------------------------- /pygameui/kvc.py: -------------------------------------------------------------------------------- 1 | """This module lets you set/get attribute values by walking 2 | a "key path" from a root or start object. 3 | 4 | A key path is a string with path part specs delimited by period '.'. 5 | Multiple path part specs are concatenated together to form the 6 | entire path spec. 7 | 8 | Each path part spec takes one of two forms: 9 | 10 | - identifier 11 | - identifier[integer] 12 | 13 | Walks proceed by evaluating each path part spec against the 14 | current object, starting with the given object. 15 | 16 | Path part specs work against objects, lists, tuples, and dicts. 17 | 18 | Note that a KeyError or IndexError encountered while walking a 19 | key path part spec is not caught. You have to know that the a 20 | walk of the given key path on the given object will work. 21 | 22 | An example walk: 23 | 24 | class A(object): 25 | def __init__(self): 26 | self.x = dict(y=['hello', 'world']) 27 | 28 | class B(object): 29 | def __init__(self): 30 | self.a = A() 31 | 32 | b = B() 33 | print value_for_keypath(b, 'a.x.y[1]') # prints 'world' 34 | 35 | # part spec context 36 | # --------- ------- 37 | # 'a' b.a 38 | # 'x' b.a.x 39 | # 'y[1]' b.a.x.y[1] 40 | """ 41 | 42 | AUTHOR = 'Brian Hammond ' 43 | LICENSE = 'MIT' 44 | 45 | __version__ = '0.1.0' 46 | 47 | import re 48 | 49 | list_index_re = re.compile(r'([^\[]+)\[(\d+)\]') 50 | 51 | 52 | def _extract(val, key): 53 | if isinstance(val, dict): 54 | return val[key] 55 | return getattr(val, key, None) 56 | 57 | 58 | def value_for_keypath(obj, path): 59 | """Get value from walking key path with start object obj. 60 | """ 61 | val = obj 62 | for part in path.split('.'): 63 | match = re.match(list_index_re, part) 64 | if match is not None: 65 | val = _extract(val, match.group(1)) 66 | if not isinstance(val, list) and not isinstance(val, tuple): 67 | raise TypeError('expected list/tuple') 68 | index = int(match.group(2)) 69 | val = val[index] 70 | else: 71 | val = _extract(val, part) 72 | if val is None: 73 | return None 74 | return val 75 | 76 | 77 | def set_value_for_keypath(obj, path, new_value, preserve_child = False): 78 | """Set attribute value new_value at key path of start object obj. 79 | """ 80 | parts = path.split('.') 81 | last_part = len(parts) - 1 82 | dst = obj 83 | for i, part in enumerate(parts): 84 | match = re.match(list_index_re, part) 85 | if match is not None: 86 | dst = _extract(dst, match.group(1)) 87 | if not isinstance(dst, list) and not isinstance(dst, tuple): 88 | raise TypeError('expected list/tuple') 89 | index = int(match.group(2)) 90 | if i == last_part: 91 | dst[index] = new_value 92 | else: 93 | dst = dst[index] 94 | else: 95 | if i != last_part: 96 | dst = _extract(dst, part) 97 | else: 98 | if isinstance(dst, dict): 99 | dst[part] = new_value 100 | else: 101 | if not preserve_child: 102 | setattr(dst, part, new_value) 103 | else: 104 | try: 105 | v = getattr(dst, part) 106 | except AttributeError: 107 | setattr(dst, part, new_value) 108 | 109 | 110 | if __name__ == '__main__': 111 | class A(object): 112 | def __init__(self): 113 | self.x = dict(y=['hello', 'world']) 114 | 115 | class B(object): 116 | def __init__(self): 117 | self.a = A() 118 | 119 | b = B() 120 | assert value_for_keypath(b, 'a.x.y[1]') == 'world' 121 | 122 | set_value_for_keypath(b, 'a.x.y[1]', 2) 123 | assert value_for_keypath(b, 'a.x.y[1]') == 2 124 | -------------------------------------------------------------------------------- /pygameui/label.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import view 4 | 5 | 6 | CENTER = 0 7 | LEFT = 1 8 | RIGHT = 2 9 | TOP = 3 10 | BOTTOM = 4 11 | 12 | WORD_WRAP = 0 13 | CLIP = 1 14 | 15 | 16 | class Label(view.View): 17 | """Multi-line, word-wrappable, uneditable text view. 18 | 19 | 20 | Attributes: 21 | 22 | halign 23 | 24 | CENTER, LEFT, or RIGHT. Horizontal alignment of 25 | text. 26 | 27 | valign 28 | 29 | CENTER, TOP, or BOTTOM. Vertical alignment of text. 30 | 31 | wrap_mode 32 | 33 | WORD_WRAP or CLIP. Determines how text is wrapped to 34 | fit within the label's frame width-wise. Text that 35 | is wrapped to multiple rendered lines is clipped at 36 | the bottom of the frame. After setting the text 37 | attribute, the text_size attribute may be used to 38 | resize the label's frame; also see shrink_wrap. 39 | 40 | Changing wrap_mode forces a redraw of the label. 41 | 42 | text 43 | 44 | The text to render. 45 | 46 | Changing the text forces a redraw of the label. 47 | 48 | 49 | Style attributes: 50 | 51 | Changing a style attribute does not automatically redraw 52 | text in the new style given that you will likely change 53 | a number of style attributes. Call 'layout' when you 54 | have finished changing style attributes. 55 | 56 | Using a new theme automatically restylizes and thus redraws 57 | the text using the new theme's style attributes. 58 | 59 | text_color 60 | 61 | The color of the text. 62 | 63 | text_shadow_color 64 | 65 | The color of the fake text-shadow. 66 | 67 | text_shadow_offset 68 | 69 | The offset of the fake text-shadow in the form 70 | (dx, dy). 71 | 72 | font 73 | 74 | The font used for rendering the text. 75 | 76 | padding 77 | 78 | Horizontal and vertical spacing from the label's 79 | interior edges where text is rendered. 80 | 81 | """ 82 | 83 | def __init__(self, frame, text, 84 | halign=CENTER, valign=CENTER, 85 | wrap=CLIP): 86 | 87 | view.View.__init__(self, frame) 88 | self.halign = halign 89 | self.valign = valign 90 | self._wrap_mode = wrap 91 | self._text = text 92 | self._enabled = False 93 | 94 | @property 95 | def text(self): 96 | return self._text 97 | 98 | @text.setter 99 | def text(self, text): 100 | self._text = text 101 | self.render() 102 | 103 | @property 104 | def wrap_mode(self): 105 | return self._wrap_mode 106 | 107 | @property 108 | def wrap_mode(self, mode): 109 | self._wrap_mode = mode 110 | self.render() 111 | 112 | def layout(self): 113 | self.render() 114 | view.View.layout(self) 115 | 116 | def render(self): 117 | """Force (re)draw the text to cached surfaces. 118 | """ 119 | self._render(self._text) 120 | 121 | def _render(self, text): 122 | self.text_surfaces, self.text_shadow_surfaces = [], [] 123 | 124 | if text is None or len(text) == 0: 125 | self._text = None 126 | self.text_size = (0, 0) 127 | return 128 | 129 | text = text.replace("\r\n", "\n").replace("\r", "\n") 130 | wants_shadows = (self.text_shadow_color is not None and 131 | self.text_shadow_offset is not None) 132 | 133 | if self._wrap_mode == CLIP: 134 | self._text = re.sub(r'[\n\t]{2, }', ' ', text) 135 | self.text_size = self._render_line(self._text, wants_shadows) 136 | elif self._wrap_mode == WORD_WRAP: 137 | self._render_word_wrapped(text, wants_shadows) 138 | 139 | def _render_line(self, line_text, wants_shadows): 140 | line_text = line_text.strip() 141 | text_surface = self.font.render(line_text, True, self.text_color) 142 | self.text_surfaces.append(text_surface) 143 | if wants_shadows: 144 | text_shadow_surface = self.font.render( 145 | line_text, True, self.text_shadow_color) 146 | self.text_shadow_surfaces.append(text_shadow_surface) 147 | return text_surface.get_size() 148 | 149 | def _render_word_wrapped(self, text, wants_shadows): 150 | self._text = text 151 | self.text_size = [0, 0] 152 | 153 | line_width = 0 154 | max_line_width = self.frame.w - self.padding[0] * 2 155 | 156 | line_tokens = [] 157 | tokens = re.split(r'(\s)', self._text) 158 | token_widths = {} 159 | 160 | for token in tokens: 161 | if len(token) == 0: 162 | continue 163 | 164 | token_width, _ = token_widths.setdefault(token, 165 | self.font.size(token)) 166 | 167 | if token == '\n' or token_width + line_width >= max_line_width: 168 | line_size = self._render_line(''.join(line_tokens), 169 | wants_shadows) 170 | self.text_size[0] = max(self.text_size[0], line_size[0]) 171 | self.text_size[1] += line_size[1] 172 | 173 | if token == '\n': 174 | line_tokens, line_width = [], 0 175 | else: 176 | line_tokens, line_width = [token], token_width 177 | else: 178 | line_width += token_width 179 | line_tokens.append(token) 180 | 181 | if len(line_tokens) > 0: 182 | line_size = self._render_line(''.join(line_tokens), 183 | wants_shadows) 184 | self.text_size[0] = max(self.text_size[0], line_size[0]) 185 | self.text_size[1] += line_size[1] 186 | 187 | def shrink_wrap(self): 188 | """Tightly bound the current text respecting current padding.""" 189 | 190 | self.frame.size = (self.text_size[0] + self.padding[0] * 2, 191 | self.text_size[1] + self.padding[1] * 2) 192 | 193 | def _determine_top(self): 194 | if self.valign == TOP: 195 | y = self.padding[1] 196 | elif self.valign == CENTER: 197 | y = self.frame.h // 2 - self.text_size[1] // 2 198 | elif self.valign == BOTTOM: 199 | y = self.frame.h - self.padding[1] - self.text_size[1] 200 | return y 201 | 202 | def _determine_left(self, text_surface): 203 | w = text_surface.get_size()[0] 204 | if self.halign == LEFT: 205 | x = self.padding[0] 206 | elif self.halign == CENTER: 207 | x = self.frame.w // 2 - w // 2 208 | elif self.halign == RIGHT: 209 | x = self.frame.w - 1 - self.padding[0] - w 210 | return x 211 | 212 | def draw(self): 213 | if not view.View.draw(self) or not self._text: 214 | return False 215 | 216 | wants_shadows = (self.text_shadow_color is not None and 217 | self.text_shadow_offset is not None) 218 | 219 | y = self._determine_top() 220 | 221 | for index, text_surface in enumerate(self.text_surfaces): 222 | x = self._determine_left(text_surface) 223 | 224 | if wants_shadows: 225 | text_shadow_surface = self.text_shadow_surfaces[index] 226 | top_left = (x + self.text_shadow_offset[0], 227 | y + self.text_shadow_offset[1]) 228 | self.surface.blit(text_shadow_surface, top_left) 229 | 230 | self.surface.blit(text_surface, (x, y)) 231 | y += text_surface.get_size()[1] 232 | 233 | return True 234 | 235 | def __repr__(self): 236 | if self._text is None: 237 | return '' 238 | return self._text 239 | -------------------------------------------------------------------------------- /pygameui/listview.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | import view 4 | import callback 5 | import scroll 6 | 7 | 8 | class ListView(view.View): 9 | """Vertical list of items with single-selection support. 10 | 11 | Signals 12 | 13 | on_selected(list_view, item, index) 14 | item clicked 15 | 16 | on_deselected(list_view, item, index) 17 | item clicked when selected 18 | 19 | """ 20 | 21 | def __init__(self, frame, items): 22 | """items: list of views""" 23 | frame.size = self._find_size_to_contain(items) 24 | view.View.__init__(self, frame) 25 | self.items = items 26 | self.selected_index = None 27 | self.on_selected = callback.Signal() 28 | self.on_deselected = callback.Signal() 29 | 30 | @property 31 | def items(self): 32 | return self._items 33 | 34 | @items.setter 35 | def items(self, new_items): 36 | for child in self.children: 37 | child.rm() 38 | 39 | w, h = 0, 0 40 | for item in new_items: 41 | item.frame.topleft = (0, h) 42 | self.add_child(item) 43 | w = max(w, item.frame.w) 44 | h += item.frame.h 45 | self.frame.size = (w, h) 46 | 47 | self._items = new_items 48 | 49 | if self.parent is not None: 50 | self.layout() 51 | 52 | def _find_size_to_contain(self, items): 53 | w, h = 0, 0 54 | for item in items: 55 | w = max(w, item.frame.w) 56 | h += item.frame.h 57 | return (w, h) 58 | 59 | def deselect(self): 60 | if self.selected_index is not None: 61 | self.items[self.selected_index].state = 'normal' 62 | self.on_deselected(self, 63 | self.items[self.selected_index], 64 | self.selected_index) 65 | self.selected_index = None 66 | 67 | def select(self, index): 68 | self.deselect() 69 | self.selected_index = index 70 | 71 | if index is not None: 72 | item = self.items[self.selected_index] 73 | item.state = 'selected' 74 | self.on_selected(self, item, index) 75 | 76 | if isinstance(self.parent, scroll.ScrollView): 77 | # auto-scroll container scroll view on new selection 78 | cy = item.frame.centery + self.frame.top 79 | if cy > self.parent.frame.h or cy < 0: 80 | percentage = item.frame.top / float(self.frame.h) 81 | self.parent.set_content_offset( 82 | self.parent._content_offset[0], percentage) 83 | 84 | def mouse_down(self, button, point): 85 | for index, child in enumerate(self.children): 86 | if point[1] >= child.frame.top and point[1] <= child.frame.bottom: 87 | self.select(index) 88 | break 89 | 90 | def key_down(self, key, code): 91 | index = self.selected_index 92 | 93 | if index is None: 94 | index = 0 95 | 96 | if key == pygame.K_DOWN: 97 | self.select(min(len(self.items) - 1, index + 1)) 98 | elif key == pygame.K_UP: 99 | self.select(max(0, index - 1)) 100 | -------------------------------------------------------------------------------- /pygameui/notification.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | import dialog 4 | import window 5 | import label 6 | 7 | 8 | DOWN = 0 9 | UP = 1 10 | IDLE = 2 11 | 12 | 13 | class NotificationView(dialog.DialogView): 14 | """A notification alert view. 15 | 16 | The notification animates down from the top of 17 | the window and can be closed by mouse clock or 18 | automatically close itself after a few seconds. 19 | 20 | auto_close 21 | 22 | Automatically close the notification; 23 | default: True. 24 | 25 | auto_close_after 26 | 27 | How long to wait before closing the notification; 28 | default: 3 (seconds). 29 | 30 | """ 31 | 32 | def __init__(self, msg): 33 | frame = pygame.Rect(0, 0, window.rect.w // 3, window.rect.h // 2) 34 | dialog.DialogView.__init__(self, frame) 35 | 36 | self.message_label = label.Label(pygame.Rect((0, 0), frame.size), 37 | msg, wrap=label.WORD_WRAP) 38 | self.add_child(self.message_label) 39 | 40 | self.auto_close = True 41 | self.auto_close_after = 3 42 | self.elapsed = 0 43 | 44 | def layout(self): 45 | assert self.get_border_widths()[0] == 0 # top; check for animations 46 | assert self.padding[0] == 0 and self.padding[1] == 0 47 | self.message_label.shrink_wrap() 48 | self.message_label.frame.w = self.frame.w 49 | self.frame.h = self.message_label.frame.h 50 | dialog.DialogView.layout(self) 51 | 52 | def parented(self): 53 | self.animation_state = DOWN 54 | self.frame.top = -self.frame.h 55 | self.frame.centerx = self.parent.frame.w // 2 56 | self.stylize() 57 | 58 | def mouse_down(self, button, point): 59 | dialog.DialogView.mouse_down(self, button, point) 60 | self.animation_state = UP 61 | 62 | def update(self, dt): 63 | dialog.DialogView.update(self, dt) 64 | rate = 300 65 | if self.animation_state == DOWN: 66 | if self.frame.top < 0: 67 | self.frame.top += dt * rate 68 | self.frame.top = min(self.frame.top, 0) 69 | else: 70 | self.animation_state = IDLE 71 | elif self.animation_state == UP: 72 | if self.frame.top > -self.frame.h: 73 | self.frame.top -= dt * rate 74 | else: 75 | self.rm() 76 | elif self.animation_state == IDLE: 77 | self.elapsed += dt 78 | if self.elapsed > self.auto_close_after: 79 | self.animation_state = UP 80 | 81 | 82 | def show_notification(message): 83 | notification = NotificationView(message) 84 | import scene 85 | scene.current.add_child(notification) 86 | notification.stylize() 87 | -------------------------------------------------------------------------------- /pygameui/progress.py: -------------------------------------------------------------------------------- 1 | import slider 2 | 3 | 4 | class ProgressView(slider.SliderView): 5 | """A progress bar.""" 6 | 7 | def __init__(self, frame): 8 | slider.SliderView.__init__(self, frame, slider.HORIZONTAL, 9 | 0.0, 1.0, show_thumb=False) 10 | self.enabled = False 11 | self.progress = 0 12 | 13 | @property 14 | def progress(self): 15 | """progress is in the range [0, 1]""" 16 | return self._value 17 | 18 | @progress.setter 19 | def progress(self, value): 20 | assert 0.0 <= value <= 1.0 21 | self.value = value 22 | -------------------------------------------------------------------------------- /pygameui/render.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | 4 | def fill_gradient(surface, color, gradient, 5 | rect=None, vertical=True, forward=True): 6 | 7 | """Fill a surface with a linear gradient pattern. 8 | 9 | color 10 | 11 | starting color 12 | 13 | gradient 14 | 15 | final color 16 | 17 | rect 18 | 19 | area to fill; default is surface's rect 20 | 21 | vertical 22 | 23 | True=vertical; False=horizontal 24 | 25 | forward 26 | 27 | True=forward; False=reverse 28 | 29 | See http://www.pygame.org/wiki/GradientCode 30 | """ 31 | 32 | if rect is None: 33 | rect = surface.get_rect() 34 | 35 | x1, x2 = rect.left, rect.right 36 | y1, y2 = rect.top, rect.bottom 37 | 38 | if vertical: 39 | h = y2 - y1 40 | else: 41 | h = x2 - x1 42 | 43 | assert h > 0 44 | 45 | if forward: 46 | a, b = color, gradient 47 | else: 48 | b, a = color, gradient 49 | 50 | rate = (float(b[0] - a[0]) / h, 51 | float(b[1] - a[1]) / h, 52 | float(b[2] - a[2]) / h) 53 | 54 | fn_line = pygame.draw.line 55 | if vertical: 56 | for line in range(y1, y2): 57 | color = (min(max(a[0] + (rate[0] * (line - y1)), 0), 255), 58 | min(max(a[1] + (rate[1] * (line - y1)), 0), 255), 59 | min(max(a[2] + (rate[2] * (line - y1)), 0), 255)) 60 | fn_line(surface, color, (x1, line), (x2, line)) 61 | else: 62 | for col in range(x1, x2): 63 | color = (min(max(a[0] + (rate[0] * (col - x1)), 0), 255), 64 | min(max(a[1] + (rate[1] * (col - x1)), 0), 255), 65 | min(max(a[2] + (rate[2] * (col - x1)), 0), 255)) 66 | fn_line(surface, color, (col, y1), (col, y2)) 67 | 68 | 69 | def fillrect(surface, color, rect, vertical=True): 70 | if len(color) == 2: # gradient 71 | fill_gradient(surface, color[0], color[1], 72 | rect=rect, vertical=vertical) 73 | else: 74 | surface.fill(color, rect) 75 | -------------------------------------------------------------------------------- /pygameui/resource.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import pkg_resources 3 | 4 | import weakref 5 | import logging 6 | 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | font_cache = weakref.WeakValueDictionary() 12 | image_cache = weakref.WeakValueDictionary() 13 | sound_cache = weakref.WeakValueDictionary() 14 | 15 | 16 | package_name = 'pygameui' 17 | 18 | 19 | def get_font(size, use_bold=False, font=None): 20 | filename = 'regular' 21 | if use_bold: 22 | filename = 'bold' 23 | if font is not None: 24 | filename = font 25 | key = '%s:%d' % (filename, size) 26 | try: 27 | font = font_cache[key] 28 | except KeyError: 29 | path = 'resources/fonts/%s.ttf' % filename 30 | path = pkg_resources.resource_filename(package_name, path) 31 | try: 32 | logger.debug('loading font %s' % path) 33 | font = pygame.font.Font(path, size) 34 | except pygame.error, e: 35 | logger.warn('failed to load font: %s: %s' % (path, e)) 36 | backup_fonts = 'helvetica,arial' 37 | font = pygame.font.SysFont(backup_fonts, size, use_bold) 38 | else: 39 | font_cache[key] = font 40 | return font 41 | 42 | 43 | # TODO update this to support multiple search paths 44 | 45 | 46 | def get_image(name): 47 | try: 48 | img = image_cache[name] 49 | except KeyError: 50 | path = 'resources/images/%s.png' % name 51 | path = pkg_resources.resource_filename(package_name, path) 52 | try: 53 | logger.debug('loading image %s' % path) 54 | img = pygame.image.load(path) 55 | except pygame.error, e: 56 | logger.warn('failed to load image: %s: %s' % (path, e)) 57 | img = None 58 | else: 59 | img = img.convert_alpha() 60 | image_cache[path] = img 61 | return img 62 | 63 | 64 | def scale_image(image, size): 65 | return pygame.transform.smoothscale(image, size) 66 | 67 | 68 | def get_sound(name): 69 | class NoSound: 70 | def play(self): 71 | pass 72 | 73 | if not pygame.mixer or not pygame.mixer.get_init(): 74 | return NoSound() 75 | 76 | try: 77 | sound = sound_cache[name] 78 | except KeyError: 79 | path = 'resources/sounds/%s.ogg' % name 80 | path = pkg_resources.resource_filename(package_name, path) 81 | try: 82 | sound = pygame.mixer.Sound(path) 83 | except pygame.error, e: 84 | logger.warn('failed to load sound: %s: %s' % (path, e)) 85 | sound = NoSound() 86 | else: 87 | sound_cache[path] = sound 88 | return sound 89 | -------------------------------------------------------------------------------- /pygameui/resources/fonts/bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horatio-sans-serif/pygameui/af6a35f347d6fafa66c4255bbbe38736d842ff65/pygameui/resources/fonts/bold.ttf -------------------------------------------------------------------------------- /pygameui/resources/fonts/font info.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horatio-sans-serif/pygameui/af6a35f347d6fafa66c4255bbbe38736d842ff65/pygameui/resources/fonts/font info.txt -------------------------------------------------------------------------------- /pygameui/resources/fonts/regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horatio-sans-serif/pygameui/af6a35f347d6fafa66c4255bbbe38736d842ff65/pygameui/resources/fonts/regular.ttf -------------------------------------------------------------------------------- /pygameui/resources/images/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horatio-sans-serif/pygameui/af6a35f347d6fafa66c4255bbbe38736d842ff65/pygameui/resources/images/info.png -------------------------------------------------------------------------------- /pygameui/resources/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horatio-sans-serif/pygameui/af6a35f347d6fafa66c4255bbbe38736d842ff65/pygameui/resources/images/logo.png -------------------------------------------------------------------------------- /pygameui/resources/images/shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horatio-sans-serif/pygameui/af6a35f347d6fafa66c4255bbbe38736d842ff65/pygameui/resources/images/shadow.png -------------------------------------------------------------------------------- /pygameui/resources/images/spinner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horatio-sans-serif/pygameui/af6a35f347d6fafa66c4255bbbe38736d842ff65/pygameui/resources/images/spinner.png -------------------------------------------------------------------------------- /pygameui/resources/images/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horatio-sans-serif/pygameui/af6a35f347d6fafa66c4255bbbe38736d842ff65/pygameui/resources/images/star.png -------------------------------------------------------------------------------- /pygameui/scene.py: -------------------------------------------------------------------------------- 1 | import view 2 | import window 3 | import focus 4 | 5 | 6 | stack = [] 7 | current = None 8 | 9 | 10 | def push(scene): 11 | global current 12 | stack.append(scene) 13 | current = scene 14 | current.entered() 15 | focus.set(None) 16 | 17 | 18 | def pop(): 19 | global current 20 | 21 | if len(stack) > 0: 22 | current.exited() 23 | stack.pop() 24 | 25 | if len(stack) > 0: 26 | current = stack[-1] 27 | current.entered() 28 | 29 | focus.set(None) 30 | 31 | 32 | class Scene(view.View): 33 | """A view that takes up the entire window content area.""" 34 | 35 | def __init__(self): 36 | view.View.__init__(self, window.rect) 37 | 38 | def key_down(self, key, code): 39 | import pygame 40 | 41 | if key == pygame.K_ESCAPE: 42 | pop() 43 | 44 | def exited(self): 45 | pass 46 | 47 | def entered(self): 48 | self.stylize() 49 | -------------------------------------------------------------------------------- /pygameui/scroll.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | import view 4 | import render 5 | import callback 6 | 7 | 8 | HORIZONTAL = 0 9 | VERTICAL = 1 10 | 11 | 12 | SCROLLBAR_SIZE = 16 13 | 14 | 15 | class ScrollbarThumbView(view.View): 16 | """Draggable thumb of a scrollbar.""" 17 | 18 | def __init__(self, direction): 19 | size = SCROLLBAR_SIZE 20 | view.View.__init__(self, pygame.Rect(0, 0, size, size)) 21 | self.direction = direction 22 | self.draggable = True 23 | 24 | def key_down(self, key, code): 25 | # Simulate mouse drag to scroll with keyboard. 26 | 27 | if self.direction == VERTICAL: 28 | if key == pygame.K_DOWN: 29 | self.mouse_drag((0, 0), (0, 1)) 30 | elif key == pygame.K_UP: 31 | self.mouse_drag((0, 0), (0, - 1)) 32 | else: 33 | if key == pygame.K_RIGHT: 34 | self.mouse_drag((0, 0), (1, 0)) 35 | elif key == pygame.K_LEFT: 36 | self.mouse_drag((0, 0), (-1, 0)) 37 | 38 | 39 | class ScrollbarView(view.View): 40 | """A scrollbar.""" 41 | 42 | def __init__(self, scroll_view, direction): 43 | """Create a scrollbar for the given scrollable view.""" 44 | if direction == VERTICAL: 45 | height = scroll_view.frame.h - SCROLLBAR_SIZE 46 | frame = pygame.Rect(0, 0, SCROLLBAR_SIZE, height) 47 | frame.right = scroll_view.frame.w 48 | else: 49 | width = scroll_view.frame.w - SCROLLBAR_SIZE 50 | frame = pygame.Rect(0, 0, width, SCROLLBAR_SIZE) 51 | frame.bottom = scroll_view.frame.h 52 | view.View.__init__(self, frame) 53 | 54 | self.direction = direction 55 | self.scroll_view = scroll_view 56 | 57 | self.thumb = ScrollbarThumbView(self.direction) 58 | self.add_child(self.thumb) 59 | 60 | def layout(self): 61 | self._update_thumb() 62 | self.thumb.layout() 63 | view.View.layout(self) 64 | 65 | def _update_thumb(self): 66 | self.thumb.frame.top = max(0, self.thumb.frame.top) 67 | self.thumb.frame.bottom = min(self.frame.bottom, 68 | self.thumb.frame.bottom) 69 | self.thumb.frame.left = max(0, self.thumb.frame.left) 70 | self.thumb.frame.right = min(self.frame.right, self.thumb.frame.right) 71 | 72 | if self.direction == VERTICAL: 73 | self.thumb.frame.centerx = SCROLLBAR_SIZE // 2 74 | else: 75 | self.thumb.frame.centery = SCROLLBAR_SIZE // 2 76 | 77 | if self.direction == VERTICAL: 78 | self.frame.right = self.scroll_view.frame.w 79 | off_x = self.scroll_view._content_offset[0] 80 | off_y = self.thumb.frame.top / float(self.frame.h) 81 | self.scroll_view.set_content_offset(off_x, off_y) 82 | percentage = (self.scroll_view.frame.h / 83 | float(self.scroll_view.content_view.frame.h)) 84 | self.thumb.frame.h = self.frame.h * percentage 85 | # self.hidden = (percentage >= 1) 86 | else: 87 | self.frame.bottom = self.scroll_view.frame.h 88 | off_x = self.thumb.frame.left / float(self.frame.w) 89 | off_y = self.scroll_view._content_offset[1] 90 | self.scroll_view.set_content_offset(off_x, off_y) 91 | percentage = (self.scroll_view.frame.w / 92 | float(self.scroll_view.content_view.frame.w)) 93 | self.thumb.frame.w = self.frame.w * percentage 94 | self.hidden = (percentage >= 1) 95 | 96 | if (self.direction == VERTICAL and 97 | self.scroll_view.hscrollbar.hidden and 98 | not self.scroll_view.vscrollbar.hidden): 99 | self.frame.h = self.scroll_view.frame.h 100 | elif (self.direction == HORIZONTAL and 101 | self.scroll_view.vscrollbar.hidden and 102 | not self.scroll_view.hscrollbar.hidden): 103 | self.frame.w = self.scroll_view.frame.w 104 | 105 | def _child_dragged(self, child): 106 | assert child == self.thumb 107 | self.layout() 108 | 109 | # Jump to offset at clicked point; does not allow dragging 110 | # without reclicking thumb 111 | 112 | def mouse_down(self, button, point): 113 | if self.direction == VERTICAL: 114 | self.thumb.frame.top = point[1] 115 | self._update_thumb() 116 | else: 117 | self.thumb.frame.left = point[0] 118 | self._update_thumb() 119 | 120 | 121 | class ScrollView(view.View): 122 | """A view that scrolls a content view 123 | 124 | Signals 125 | 126 | on_scrolled(scroll_view) 127 | content offset was updated. 128 | 129 | """ 130 | 131 | def __init__(self, frame, content_view): 132 | width = frame.size[0] + SCROLLBAR_SIZE 133 | height = frame.size[1] + SCROLLBAR_SIZE 134 | rect = pygame.Rect(frame.topleft, (width, height)) 135 | view.View.__init__(self, rect) 136 | 137 | self.on_scrolled = callback.Signal() 138 | 139 | self.content_view = content_view 140 | self._content_offset = (0, 0) 141 | self.add_child(self.content_view) 142 | 143 | self.hscrollbar = ScrollbarView(self, HORIZONTAL) 144 | self.vscrollbar = ScrollbarView(self, VERTICAL) 145 | self.add_child(self.hscrollbar) 146 | self.add_child(self.vscrollbar) 147 | 148 | def layout(self): 149 | self.hscrollbar.layout() 150 | self.vscrollbar.layout() 151 | view.View.layout(self) 152 | 153 | def set_content_offset(self, percent_w, percent_h, 154 | update_scrollbar_size=True): 155 | 156 | self._content_offset = (min(1, max(0, percent_w)), 157 | min(1, max(0, percent_h))) 158 | 159 | self.content_view.frame.topleft = ( 160 | -self._content_offset[0] * self.content_view.frame.w, 161 | -self._content_offset[1] * self.content_view.frame.h) 162 | 163 | if update_scrollbar_size: 164 | self.vscrollbar.thumb.centery = percent_h * self.vscrollbar.frame.h 165 | self.hscrollbar.thumb.centerx = percent_w * self.hscrollbar.frame.w 166 | 167 | self.on_scrolled(self) 168 | 169 | def draw(self): 170 | if not view.View.draw(self): 171 | return False 172 | 173 | if not self.vscrollbar.hidden and not self.hscrollbar.hidden: 174 | hole = pygame.Rect(self.vscrollbar.frame.left, 175 | self.vscrollbar.frame.bottom, 176 | SCROLLBAR_SIZE, 177 | SCROLLBAR_SIZE) 178 | render.fillrect(self.surface, self.hole_color, hole) 179 | 180 | return True 181 | -------------------------------------------------------------------------------- /pygameui/select.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | import view 4 | import theme 5 | import callback 6 | import listview 7 | import scroll 8 | import label 9 | import button 10 | 11 | 12 | class SelectView(view.View): 13 | """Drop-down selector with single selection support. 14 | 15 | Signals 16 | 17 | on_list_opened(select_view, yesno) 18 | list opened / closed 19 | 20 | on_selection_changed(select_view, item, index) 21 | item selected 22 | """ 23 | 24 | def __init__(self, frame, items): 25 | """items: list of views; str(item) used for selection display""" 26 | assert len(items) > 0 27 | 28 | view.View.__init__(self, pygame.Rect(frame.topleft, (1, 1))) 29 | 30 | self.on_selection_changed = callback.Signal() 31 | self.on_list_opened = callback.Signal() 32 | 33 | self.top_label = label.Label(pygame.Rect(0, 0, 1, 1), '') 34 | self.top_label.halign = label.LEFT 35 | self.top_label._enabled = True 36 | self.top_label.on_mouse_down.connect(self.show_list) 37 | self.add_child(self.top_label) 38 | 39 | self.list_view = listview.ListView(pygame.Rect(0, 0, 1, 1), items) 40 | self.list_view.on_selected.connect(self.item_selected) 41 | self.list_view.on_deselected.connect(self.item_deselected) 42 | self.scroll_view = scroll.ScrollView(pygame.Rect(0, 0, 1, 1), 43 | self.list_view) 44 | self.scroll_view.hidden = True 45 | self.add_child(self.scroll_view) 46 | 47 | self.disclosure = button.Button(pygame.Rect(0, 0, 1, 1), caption='') 48 | self.disclosure.on_clicked.connect(self._toggle_show_list) 49 | self.add_child(self.disclosure) 50 | 51 | def layout(self): 52 | assert self.padding[0] == 0 and self.padding[1] == 0 53 | 54 | self.scroll_view.frame.top = self.top_label.frame.bottom - 1 55 | self.scroll_view.frame.h = 100 56 | self.scroll_view.frame.w = (self.list_view.frame.w + 57 | scroll.SCROLLBAR_SIZE) 58 | 59 | self.frame.w = self.scroll_view.frame.w 60 | 61 | if self.scroll_view.hidden: 62 | self.frame.h = theme.current.label_height 63 | else: 64 | self.frame.h = (self.top_label.frame.h + 65 | self.scroll_view.frame.h - 1) 66 | 67 | self.disclosure.frame.w = theme.current.label_height 68 | self.disclosure.frame.h = theme.current.label_height 69 | self.disclosure.frame.right = self.scroll_view.frame.right 70 | 71 | self.top_label.frame.w = self.disclosure.frame.left 72 | self.top_label.frame.h = theme.current.label_height 73 | self.top_label.frame.topleft = (0, 0) 74 | 75 | self.list_view.layout() 76 | self.scroll_view.layout() 77 | self.top_label.layout() 78 | self.disclosure.layout() 79 | view.View.layout(self) 80 | 81 | def show_list(self, show=True, *args, **kwargs): 82 | self.list_view.focus() 83 | if show: 84 | self.scroll_view.hidden = False 85 | self.bring_to_front() 86 | else: 87 | self.scroll_view.hidden = True 88 | self.on_list_opened(self, show) 89 | self.layout() 90 | 91 | def _toggle_show_list(self, *args, **kwargs): 92 | self.show_list(self.scroll_view.hidden) 93 | if not self.scroll_view.hidden: 94 | self.list_view.focus() 95 | 96 | def draw(self): 97 | if not view.View.draw(self): 98 | return False 99 | 100 | f = self.disclosure.frame 101 | if self.scroll_view.hidden: 102 | points = [(f.left + f.w // 4, f.h // 3), 103 | (f.right - f.w // 4, f.h // 3), 104 | (f.centerx, f.h - f.h // 3)] 105 | else: 106 | points = [(f.left + f.w // 4, f.h - f.h // 3), 107 | (f.right - f.w // 4, f.h - f.h // 3), 108 | (f.centerx, f.h // 3)] 109 | 110 | pygame.draw.polygon(self.surface, 111 | self.disclosure_triangle_color, 112 | points) 113 | return True 114 | 115 | def item_selected(self, list_view, item, index): 116 | item.state = 'selected' 117 | self.top_label.text = str(item) 118 | self.show_list(False) 119 | self.on_selection_changed(list_view, item, index) 120 | 121 | def item_deselected(self, list_view, item, index): 122 | item.state = 'normal' 123 | self.top_label.text = '' 124 | self.on_selection_changed(list_view, item, index) 125 | -------------------------------------------------------------------------------- /pygameui/slider.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | import view 4 | import render 5 | import callback 6 | import scroll 7 | 8 | 9 | HORIZONTAL = 0 10 | VERTICAL = 1 11 | 12 | 13 | class SliderTrackView(view.View): 14 | "The track on which the thumb slides" 15 | 16 | def __init__(self, frame, direction): 17 | view.View.__init__(self, frame) 18 | self.direction = direction 19 | self.value_percent = 0.5 20 | 21 | def draw(self): 22 | if not view.View.draw(self): 23 | return False 24 | 25 | if self.value_percent > 0: 26 | t, l, b, r = self.get_border_widths() 27 | if self.direction == VERTICAL: 28 | w = self.frame.w - r - l 29 | h = self.value_percent * self.frame.h - b - t 30 | y = self.frame.h - b - h 31 | else: 32 | y = t 33 | w = self.value_percent * self.frame.w - r - l 34 | h = self.frame.h - b - t 35 | rect = pygame.Rect(l, y, w, h) 36 | render.fillrect(self.surface, self.value_color, rect=rect, 37 | vertical=(self.direction == HORIZONTAL)) 38 | 39 | return True 40 | 41 | 42 | class SliderView(view.View): 43 | """Drag a thumb to select a value from a given range 44 | 45 | Signals 46 | on_value_changed(sliderview, value) 47 | 48 | """ 49 | 50 | def __init__(self, frame, direction, low, high, show_thumb=True): 51 | view.View.__init__(self, frame) 52 | 53 | self.on_value_changed = callback.Signal() 54 | 55 | self.direction = direction 56 | self.low = min(low, high) 57 | self.high = max(low, high) 58 | 59 | self.thumb = scroll.ScrollbarThumbView(self.direction) 60 | self.thumb.hidden = (not show_thumb) 61 | self._add_track(show_thumb) 62 | self.add_child(self.thumb) 63 | 64 | self._value = None 65 | self.value = (low + high) / 2.0 66 | 67 | def _add_track(self, show_thumb): 68 | if self.direction == HORIZONTAL: 69 | trackh = self.thumb.frame.h - self.thumb.frame.h // 4 70 | offset = 0 71 | if show_thumb: 72 | offset = self.thumb.frame.w // 2 73 | trackrect = pygame.Rect(offset, 74 | self.frame.h // 2 - trackh // 2, 75 | self.frame.w - offset * 2, trackh) 76 | else: 77 | track_width = self.thumb.frame.w - self.thumb.frame.w // 4 78 | offset = 0 79 | if show_thumb: 80 | offset = self.thumb.frame.h // 2 81 | trackrect = pygame.Rect(self.frame.w // 2 - track_width // 2, 82 | offset, track_width, 83 | self.frame.h - offset * 2) 84 | self.track = SliderTrackView(trackrect, self.direction) 85 | self.add_child(self.track) 86 | 87 | @property 88 | def value(self): 89 | """value is in the range [low, high]""" 90 | return self._value 91 | 92 | @value.setter 93 | def value(self, val): 94 | self._set_value(val) 95 | 96 | def _set_value(self, val, update_thumb=True): 97 | if val == self._value: 98 | return 99 | 100 | self._value = max(self.low, min(self.high, val)) 101 | self.track.value_percent = (val - self.low) / (self.high - self.low) 102 | 103 | if update_thumb: 104 | self._update_thumb() 105 | 106 | self.on_value_changed(self, self._value) 107 | 108 | def parented(self): 109 | self._update_thumb() 110 | 111 | def _update_thumb(self): 112 | if self.direction == VERTICAL: 113 | percentage = self._value / float(self.high - self.low) 114 | self.thumb.frame.centery = self.frame.h * percentage 115 | else: 116 | percentage = self._value / float(self.high - self.low) 117 | self.thumb.frame.centerx = self.frame.w * percentage 118 | 119 | def _child_dragged(self, child): 120 | assert child == self.thumb 121 | if self.direction == VERTICAL: 122 | self.thumb.frame.centerx = self.frame.w // 2 123 | self.thumb.frame.top = max(0, self.thumb.frame.top) 124 | self.thumb.frame.bottom = min(self.frame.h, self.thumb.frame.bottom) 125 | percent_px = self.thumb.frame.centery - self.thumb.frame.h // 2 126 | height = self.frame.h - self.thumb.frame.h 127 | t = percent_px / float(height) 128 | value = self.high + t * (self.low - self.high) 129 | self._set_value(value, False) 130 | else: 131 | self.thumb.frame.centery = self.frame.h // 2 132 | self.thumb.frame.left = max(0, self.thumb.frame.left) 133 | self.thumb.frame.right = min(self.frame.w, self.thumb.frame.right) 134 | percent_px = self.thumb.frame.centerx - self.thumb.frame.w // 2 135 | width = self.frame.w - self.thumb.frame.w 136 | t = percent_px / float(width) 137 | value = self.low + t * (self.high - self.low) 138 | self._set_value(value, False) 139 | -------------------------------------------------------------------------------- /pygameui/spinner.py: -------------------------------------------------------------------------------- 1 | import flipbook 2 | import resource 3 | 4 | 5 | class SpinnerView(flipbook.FlipbookView): 6 | size = 24 7 | 8 | def __init__(self, frame): 9 | frame.size = (SpinnerView.size, SpinnerView.size) 10 | image = resource.get_image('spinner') 11 | flipbook.FlipbookView.__init__(self, frame, image) 12 | -------------------------------------------------------------------------------- /pygameui/textfield.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | import view 4 | import label 5 | import callback 6 | 7 | 8 | class TextField(view.View): 9 | """Editable single line of text. 10 | 11 | There are no fancy keybindings; just backspace. 12 | 13 | Signals 14 | 15 | on_text_change(text_field, text) 16 | on_return(text_field, text) 17 | 18 | """ 19 | 20 | def __init__(self, frame, text='', placeholder=''): 21 | view.View.__init__(self, frame) 22 | 23 | self.text = text or '' 24 | self.placeholder = placeholder 25 | 26 | self.label = label.Label(pygame.Rect((0, 0), frame.size), 27 | text or placeholder) 28 | self.label.halign = label.LEFT 29 | self.add_child(self.label) 30 | 31 | self.enabled = True 32 | self.max_len = None 33 | self.secure = False 34 | 35 | self.on_return = callback.Signal() 36 | self.on_text_change = callback.Signal() 37 | 38 | def layout(self): 39 | self.label.topleft = self.padding 40 | r_before = self.label.frame.right 41 | self.label.frame.w = self.frame.w - self.padding[0] * 2 42 | self.label.frame.h = self.frame.h - self.padding[1] * 2 43 | self.label.frame.right = r_before 44 | self._update_text() 45 | view.View.layout(self) 46 | 47 | def key_down(self, key, code): 48 | if key == pygame.K_BACKSPACE: 49 | self.text = self.text[0:-1] 50 | elif key == pygame.K_RETURN: 51 | can_submit = True 52 | if self.placeholder and self.text == self.placeholder: 53 | can_submit = False 54 | if can_submit: 55 | self.on_return(self, self.text) 56 | else: 57 | try: 58 | self.text = '%s%s' % (self.text, str(code)) 59 | except: 60 | pass 61 | self.on_text_change(self, self.text) 62 | 63 | if self.max_len: 64 | self.text = self.text[0:self.max_len] 65 | 66 | self._update_text() 67 | self.label.shrink_wrap() 68 | self.label.layout() 69 | 70 | if self.label.frame.right > self.frame.w - self.padding[0] * 2: 71 | self.label.frame.right = self.frame.w - self.padding[0] * 2 72 | else: 73 | self.label.frame.left = self.padding[0] 74 | 75 | def _update_text(self): 76 | if (len(self.text) == 0 and 77 | self.placeholder is not None and 78 | not self.has_focus()): 79 | self.label.text_color = self.placeholder_text_color 80 | self.label.text = self.placeholder 81 | elif len(self.text) >= 0: 82 | self.label.text_color = self.text_color 83 | self.label.text = self.text 84 | elif self.secure: 85 | self.label.text = '*' * len(self.text) 86 | 87 | def draw(self): 88 | if not view.View.draw(self) or not self.has_focus(): 89 | return False 90 | 91 | if (not self.blink_cursor or 92 | pygame.time.get_ticks() / self.cursor_blink_duration % 2 == 0): 93 | size = self.label.font.size(self.text) 94 | rect = pygame.Rect( 95 | self.label.frame.left + self.label.padding[0] + size[0], 96 | self.label.frame.bottom - self.label.padding[1], 97 | 10, 2) 98 | pygame.draw.rect(self.surface, self.text_color, rect) 99 | return True 100 | 101 | def __repr__(self): 102 | return self.text 103 | -------------------------------------------------------------------------------- /pygameui/theme.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | 3 | import resource 4 | from colors import * 5 | 6 | 7 | class Theme(object): 8 | """A theme is a hierarchical set of view style attributes. 9 | 10 | Each view may have a set of attributes that control its 11 | visual style when rendered. These style attributes are stored 12 | in a Theme. 13 | 14 | Style attributes are hierarchical in that a view class 15 | may override the style attribute of a parent view class. 16 | Also, a view class need not override all style attributes 17 | of a parent view class. 18 | 19 | For instance, let's say we define the default view background 20 | color to be gray and the border color to be black. 21 | 22 | a_theme.set(class_name='View', 23 | state='normal', 24 | key='background_color', 25 | value=(128, 128, 128)) 26 | 27 | a_theme.set(class_name='View', 28 | state='normal', 29 | key='border_color', 30 | value=(0, 0, 0)) 31 | 32 | Let's assume this is the only style information defined for View. 33 | Now, let's override the background color for a Button, add a 34 | style attribute for the text color, and leave the border color. 35 | 36 | a_theme.set(class_name='Button', 37 | state='normal', 38 | key='background_color', 39 | value=(64, 64, 64)) 40 | 41 | a_theme.set(class_name='Button', 42 | state='normal', 43 | key='text_color', 44 | value=(128, 0, 0)) 45 | 46 | When a view is stylized (see View.stylize), style attributes and 47 | values are queried in the current Theme and set on the view instance. 48 | 49 | b = Button() 50 | b.state = 'normal' 51 | b.stylize() 52 | 53 | The style attributes set on 'b' would be: 54 | 55 | background_color: (64, 64, 64) from Button 56 | border_color: (0, 0, 0) from View 57 | text_color: (128, 0, 0) from Button 58 | 59 | Note that the 'key' is really a 'key path' which would allow you 60 | to style views contained in other views. For instance, an AlertView 61 | has a `title_label` which is a Label. You may wish to style 62 | AlertView titles differently than other labels, and you can. See 63 | the `light_theme` entry for `title_label`. Also see the `kvc` module 64 | for a simple form of Apple's Key-Value Coding for Python. 65 | 66 | """ 67 | 68 | def __init__(self): 69 | self._styles = {} 70 | 71 | def set(self, class_name, state, key, value): 72 | """Set a single style value for a view class and state. 73 | 74 | class_name 75 | 76 | The name of the class to be styled; do not 77 | include the package name; e.g. 'Button'. 78 | 79 | state 80 | 81 | The name of the state to be stylized. One of the 82 | following: 'normal', 'focused', 'selected', 'disabled' 83 | is common. 84 | 85 | key 86 | 87 | The style attribute name; e.g. 'background_color'. 88 | 89 | value 90 | 91 | The value of the style attribute; colors are either 92 | a 3-tuple for RGB, a 4-tuple for RGBA, or a pair 93 | thereof for a linear gradient. 94 | 95 | """ 96 | self._styles.setdefault(class_name, {}).setdefault(state, {}) 97 | self._styles[class_name][state][key] = value 98 | 99 | def get_dict_for_class(self, class_name, state=None, base_name='View'): 100 | """The style dict for a given class and state. 101 | 102 | This collects the style attributes from parent classes 103 | and the class of the given object and gives precedence 104 | to values thereof to the children. 105 | 106 | The state attribute of the view instance is taken as 107 | the current state if state is None. 108 | 109 | If the state is not 'normal' then the style definitions 110 | for the 'normal' state are mixed-in from the given state 111 | style definitions, giving precedence to the non-'normal' 112 | style definitions. 113 | 114 | """ 115 | classes = [] 116 | klass = class_name 117 | 118 | while True: 119 | classes.append(klass) 120 | if klass.__name__ == base_name: 121 | break 122 | klass = klass.__bases__[0] 123 | 124 | if state is None: 125 | state = 'normal' 126 | 127 | style = {} 128 | 129 | for klass in classes: 130 | class_name = klass.__name__ 131 | 132 | try: 133 | state_styles = self._styles[class_name][state] 134 | except KeyError: 135 | state_styles = {} 136 | 137 | if state != 'normal': 138 | try: 139 | normal_styles = self._styles[class_name]['normal'] 140 | except KeyError: 141 | normal_styles = {} 142 | 143 | state_styles = dict(chain(normal_styles.iteritems(), 144 | state_styles.iteritems())) 145 | 146 | style = dict(chain(state_styles.iteritems(), 147 | style.iteritems())) 148 | 149 | return style 150 | 151 | def get_dict(self, obj, state=None, base_name='View'): 152 | """The style dict for a view instance. 153 | 154 | """ 155 | return self.get_dict_for_class(class_name=obj.__class__, 156 | state=obj.state, 157 | base_name=base_name) 158 | 159 | def get_value(self, class_name, attr, default_value=None, 160 | state='normal', base_name='View'): 161 | """Get a single style attribute value for the given class. 162 | 163 | """ 164 | styles = self.get_dict_for_class(class_name, state, base_name) 165 | try: 166 | return styles[attr] 167 | except KeyError: 168 | return default_value 169 | 170 | 171 | current = None 172 | light_theme = Theme() 173 | dark_theme = Theme() 174 | 175 | 176 | def use_theme(theme): 177 | """Make the given theme current. 178 | 179 | There are two included themes: light_theme, dark_theme. 180 | """ 181 | global current 182 | current = theme 183 | import scene 184 | if scene.current is not None: 185 | scene.current.stylize() 186 | 187 | 188 | def init_light_theme(): 189 | color1 = (227, 227, 159) # a light yellow 190 | color2 = (173, 222, 78) # a light green 191 | color3 = (77, 148, 83) # a dark green 192 | color4 = white_color 193 | color5 = near_white_color 194 | color6 = light_gray_color 195 | color7 = gray_color 196 | color8 = dark_gray_color 197 | color9 = black_color 198 | 199 | light_theme.set(class_name='View', 200 | state='normal', 201 | key='background_color', 202 | value=(color4, color5)) 203 | light_theme.set(class_name='View', 204 | state='focused', 205 | key='background_color', 206 | value=(color1, color2)) 207 | light_theme.set(class_name='View', 208 | state='selected', 209 | key='background_color', 210 | value=(color1, color2)) 211 | light_theme.set(class_name='View', 212 | state='normal', 213 | key='border_color', 214 | value=color6) 215 | light_theme.set(class_name='View', 216 | state='normal', 217 | key='border_widths', 218 | value=0) 219 | light_theme.set(class_name='View', 220 | state='normal', 221 | key='margin', 222 | value=(6, 6)) 223 | light_theme.set(class_name='View', 224 | state='normal', 225 | key='padding', 226 | value=(0, 0)) 227 | light_theme.set(class_name='View', 228 | state='normal', 229 | key='shadowed', 230 | value=False) 231 | 232 | light_theme.set(class_name='Scene', 233 | state='normal', 234 | key='background_color', 235 | value=(color5, color4)) 236 | 237 | light_theme.set(class_name='Label', 238 | state='normal', 239 | key='text_color', 240 | value=color8) 241 | light_theme.set(class_name='Label', 242 | state='selected', 243 | key='text_color', 244 | value=color3) 245 | light_theme.set(class_name='Label', 246 | state='normal', 247 | key='text_shadow_color', 248 | value=color4) 249 | light_theme.set(class_name='Label', 250 | state='normal', 251 | key='text_shadow_offset', 252 | value=(0, 1)) 253 | light_theme.set(class_name='Label', 254 | state='normal', 255 | key='padding', 256 | value=(6, 6)) 257 | light_theme.set(class_name='Label', 258 | state='normal', 259 | key='border_widths', 260 | value=None) 261 | light_theme.set(class_name='Label', 262 | state='normal', 263 | key='font', 264 | value=resource.get_font(16)) 265 | 266 | light_theme.label_height = 16 + 6 * 2 # font size + padding above/below 267 | 268 | light_theme.set(class_name='Button', 269 | state='normal', 270 | key='background_color', 271 | value=(color4, color6)) 272 | light_theme.set(class_name='Button', 273 | state='focused', 274 | key='background_color', 275 | value=color1) 276 | light_theme.set(class_name='Button', 277 | state='normal', 278 | key='text_color', 279 | value=color8) 280 | light_theme.set(class_name='Button', 281 | state='normal', 282 | key='font', 283 | value=resource.get_font(16, use_bold=True)) 284 | light_theme.set(class_name='Button', 285 | state='normal', 286 | key='border_widths', 287 | value=1) 288 | light_theme.set(class_name='Button', 289 | state='normal', 290 | key='border_color', 291 | value=color6) 292 | 293 | light_theme.button_height = 16 + 6 * 2 # font size + padding above/below 294 | 295 | light_theme.set(class_name='ImageButton', 296 | state='normal', 297 | key='background_color', 298 | value=(color4, color6)) 299 | light_theme.set(class_name='ImageButton', 300 | state='focused', 301 | key='background_color', 302 | value=color1) 303 | light_theme.set(class_name='ImageButton', 304 | state='normal', 305 | key='border_color', 306 | value=color6) 307 | light_theme.set(class_name='ImageButton', 308 | state='normal', 309 | key='border_widths', 310 | value=1) 311 | light_theme.set(class_name='ImageButton', 312 | state='normal', 313 | key='padding', 314 | value=(6, 6)) 315 | 316 | light_theme.set(class_name='ScrollbarThumbView', 317 | state='normal', 318 | key='background_color', 319 | value=(color4, color6)) 320 | light_theme.set(class_name='ScrollbarThumbView', 321 | state='focused', 322 | key='background_color', 323 | value=(color1, color2)) 324 | light_theme.set(class_name='ScrollbarThumbView', 325 | state='normal', 326 | key='border_widths', 327 | value=1) 328 | 329 | light_theme.set(class_name='ScrollbarView', 330 | state='normal', 331 | key='background_color', 332 | value=color5) 333 | light_theme.set(class_name='ScrollbarView', 334 | state='normal', 335 | key='border_widths', 336 | value=(1, 1, 0, 0)) # t l b r 337 | 338 | light_theme.set(class_name='ScrollView', 339 | state='normal', 340 | key='hole_color', 341 | value=whites_twin_color) 342 | light_theme.set(class_name='ScrollView', 343 | state='normal', 344 | key='border_widths', 345 | value=1) 346 | 347 | light_theme.set(class_name='SliderTrackView', 348 | state='normal', 349 | key='background_color', 350 | value=(color5, color4)) 351 | light_theme.set(class_name='SliderTrackView', 352 | state='normal', 353 | key='value_color', 354 | value=(color1, color2)) 355 | light_theme.set(class_name='SliderTrackView', 356 | state='normal', 357 | key='border_widths', 358 | value=1) 359 | 360 | light_theme.set(class_name='SliderView', 361 | state='normal', 362 | key='background_color', 363 | value=clear_color) 364 | light_theme.set(class_name='SliderView', 365 | state='normal', 366 | key='border_widths', 367 | value=None) 368 | 369 | light_theme.set(class_name='ImageView', 370 | state='normal', 371 | key='background_color', 372 | value=None) 373 | light_theme.set(class_name='ImageView', 374 | state='normal', 375 | key='padding', 376 | value=(0, 0)) 377 | 378 | light_theme.set(class_name='Checkbox', 379 | state='normal', 380 | key='background_color', 381 | value=clear_color) 382 | light_theme.set(class_name='Checkbox', 383 | state='normal', 384 | key='padding', 385 | value=(0, 0)) 386 | 387 | light_theme.set(class_name='Checkbox', 388 | state='focused', 389 | key='check_label.background_color', 390 | value=(color1, color2)) 391 | light_theme.set(class_name='Checkbox', 392 | state='normal', 393 | key='check_label.border_widths', 394 | value=1) 395 | 396 | light_theme.set(class_name='Checkbox', 397 | state='normal', 398 | key='label.background_color', 399 | value=clear_color) 400 | 401 | light_theme.set(class_name='SpinnerView', 402 | state='normal', 403 | key='border_widths', 404 | value=None) 405 | 406 | light_theme.set(class_name='DialogView', 407 | state='normal', 408 | key='background_color', 409 | value=(color4, color6)) 410 | light_theme.set(class_name='DialogView', 411 | state='normal', 412 | key='shadowed', 413 | value=True) 414 | 415 | light_theme.shadow_size = 140 416 | 417 | light_theme.set(class_name='AlertView', 418 | state='normal', 419 | key='title_label.background_color', 420 | value=color7) 421 | light_theme.set(class_name='AlertView', 422 | state='normal', 423 | key='title_label.text_color', 424 | value=color4) 425 | light_theme.set(class_name='AlertView', 426 | state='normal', 427 | key='title_label.text_shadow_offset', 428 | value=None) 429 | light_theme.set(class_name='AlertView', 430 | state='normal', 431 | key='message_label.background_color', 432 | value=clear_color) 433 | light_theme.set(class_name='AlertView', 434 | state='normal', 435 | key='font', 436 | value=resource.get_font(16)) 437 | light_theme.set(class_name='AlertView', 438 | state='normal', 439 | key='padding', 440 | value=(6, 6)) 441 | 442 | light_theme.set(class_name='NotificationView', 443 | state='normal', 444 | key='background_color', 445 | value=(color1, color2)) 446 | light_theme.set(class_name='NotificationView', 447 | state='normal', 448 | key='border_color', 449 | value=color3) 450 | light_theme.set(class_name='NotificationView', 451 | state='normal', 452 | key='border_widths', 453 | value=(0, 2, 2, 2)) 454 | light_theme.set(class_name='NotificationView', 455 | state='normal', 456 | key='padding', 457 | value=(0, 0)) 458 | light_theme.set(class_name='NotificationView', 459 | state='normal', 460 | key='message_label.background_color', 461 | value=clear_color) 462 | 463 | light_theme.set(class_name='SelectView', 464 | state='normal', 465 | key='disclosure_triangle_color', 466 | value=color8) 467 | light_theme.set(class_name='SelectView', 468 | state='normal', 469 | key='border_widths', 470 | value=1) 471 | light_theme.set(class_name='SelectView', 472 | state='normal', 473 | key='top_label.focusable', 474 | value=False) 475 | 476 | light_theme.set(class_name='TextField', 477 | state='focused', 478 | key='label.background_color', 479 | value=(color1, color2)) 480 | light_theme.set(class_name='TextField', 481 | state='normal', 482 | key='placeholder_text_color', 483 | value=color6) 484 | light_theme.set(class_name='TextField', 485 | state='normal', 486 | key='border_widths', 487 | value=1) 488 | light_theme.set(class_name='TextField', 489 | state='normal', 490 | key='text_color', 491 | value=color9) 492 | light_theme.set(class_name='TextField', 493 | state='disabled', 494 | key='text_color', 495 | value=color6) 496 | light_theme.set(class_name='TextField', 497 | state='normal', 498 | key='blink_cursor', 499 | value=True) 500 | light_theme.set(class_name='TextField', 501 | state='normal', 502 | key='cursor_blink_duration', 503 | value=450) 504 | 505 | light_theme.set(class_name='GridView', 506 | state='normal', 507 | key='background_color', 508 | value=color4) 509 | light_theme.set(class_name='GridView', 510 | state='normal', 511 | key='line_color', 512 | value=color6) 513 | 514 | 515 | def init_dark_theme(): 516 | # TODO 517 | pass 518 | 519 | 520 | def init(): 521 | """Initialize theme support.""" 522 | init_light_theme() 523 | init_dark_theme() 524 | use_theme(light_theme) 525 | -------------------------------------------------------------------------------- /pygameui/view.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | import render 4 | import theme 5 | import callback 6 | import resource 7 | import focus 8 | 9 | import kvc 10 | 11 | 12 | class View(object): 13 | """A rectangular portion of the window. 14 | 15 | Views may have zero or more child views contained within it. 16 | 17 | Signals 18 | 19 | on_mouse_down(view, button, point) 20 | on_mouse_up(view, button, point) 21 | on_mouse_motion(view, point) 22 | on_mouse_drag(view, point, delta) 23 | 24 | on_key_down(view, key, code) 25 | on_key_up(view, key) 26 | 27 | on_parented(view) 28 | on_orphaned(view) (from parent view) 29 | 30 | on_focused(view) 31 | on_blurred(view) 32 | 33 | on_selected(view) 34 | on_enabled(view) 35 | on_disabled(view) 36 | on_state_changed(view) 37 | 38 | All mouse points passed to event methods and to slots are in local 39 | view coordinates. Use `to_parent` and `to_window` to convert. 40 | 41 | """ 42 | 43 | def __init__(self, frame=None): 44 | self.frame = frame 45 | 46 | self.parent = None 47 | self.children = [] # back->front 48 | 49 | self._state = 'normal' 50 | self._enabled = True 51 | self.hidden = False 52 | self.draggable = False 53 | 54 | self.shadow_image = None 55 | 56 | self.on_focused = callback.Signal() 57 | self.on_blurred = callback.Signal() 58 | 59 | self.on_selected = callback.Signal() 60 | self.on_enabled = callback.Signal() 61 | self.on_disabled = callback.Signal() 62 | self.on_state_changed = callback.Signal() 63 | 64 | self.on_mouse_up = callback.Signal() 65 | self.on_mouse_down = callback.Signal() 66 | self.on_mouse_motion = callback.Signal() 67 | self.on_mouse_drag = callback.Signal() 68 | self.on_key_down = callback.Signal() 69 | self.on_key_up = callback.Signal() 70 | 71 | self.on_parented = callback.Signal() 72 | self.on_orphaned = callback.Signal() 73 | 74 | def layout(self): 75 | """Call to have the view layout itself. 76 | 77 | Subclasses should invoke this after laying out child 78 | views and/or updating its own frame. 79 | """ 80 | if self.shadowed: 81 | shadow_size = theme.current.shadow_size 82 | shadowed_frame_size = (self.frame.w + shadow_size, 83 | self.frame.h + shadow_size) 84 | self.surface = pygame.Surface( 85 | shadowed_frame_size, pygame.SRCALPHA, 32) 86 | shadow_image = resource.get_image('shadow') 87 | self.shadow_image = resource.scale_image(shadow_image, 88 | shadowed_frame_size) 89 | else: 90 | self.surface = pygame.Surface(self.frame.size, pygame.SRCALPHA, 32) 91 | self.shadow_image = None 92 | 93 | def size_to_fit(self): 94 | rect = self.frame 95 | for child in self.children: 96 | rect = rect.union(child.frame) 97 | self.frame = rect 98 | self.layout() 99 | 100 | def update(self, dt): 101 | for child in self.children: 102 | child.update(dt) 103 | 104 | def to_parent(self, point): 105 | return (point[0] + self.frame.topleft[0], 106 | point[1] + self.frame.topleft[1]) 107 | 108 | def from_parent(self, point): 109 | return (point[0] - self.frame.topleft[0], 110 | point[1] - self.frame.topleft[1]) 111 | 112 | def from_window(self, point): 113 | curr = self 114 | ancestors = [curr] 115 | while curr.parent: 116 | ancestors.append(curr.parent) 117 | curr = curr.parent 118 | for a in reversed(ancestors): 119 | point = a.from_parent(point) 120 | return point 121 | 122 | def to_window(self, point): 123 | curr = self 124 | while curr: 125 | point = curr.to_parent(point) 126 | curr = curr.parent 127 | return point 128 | 129 | def mouse_up(self, button, point): 130 | self.on_mouse_up(self, button, point) 131 | 132 | def mouse_down(self, button, point): 133 | self.on_mouse_down(self, button, point) 134 | 135 | def mouse_motion(self, point): 136 | self.on_mouse_motion(self, point) 137 | 138 | # only called on drag event if .draggable is True 139 | 140 | def mouse_drag(self, point, delta): 141 | self.on_mouse_drag(self, point, delta) 142 | 143 | self.frame.topleft = (self.frame.topleft[0] + delta[0], 144 | self.frame.topleft[1] + delta[1]) 145 | 146 | if self.parent: 147 | self.parent._child_dragged(self) 148 | 149 | def key_down(self, key, code): 150 | self.on_key_down(self, key, code) 151 | 152 | def key_up(self, key): 153 | self.on_key_up(self, key) 154 | 155 | @property 156 | def state(self): 157 | """The state of the view. 158 | 159 | Potential values are 'normal', 'focused', 'selected', 'disabled'. 160 | """ 161 | return self._state 162 | 163 | @state.setter 164 | def state(self, new_state): 165 | if self._state != new_state: 166 | self._state = new_state 167 | self.stylize() 168 | self.on_state_changed() 169 | 170 | def focus(self): 171 | focus.set(self) 172 | 173 | def has_focus(self): 174 | return focus.view == self 175 | 176 | def focused(self): 177 | self.state = 'focused' 178 | self.on_focused() 179 | 180 | def blurred(self): 181 | self.state = 'normal' 182 | self.on_blurred() 183 | 184 | def selected(self): 185 | self.state = 'selected' 186 | self.on_selected() 187 | 188 | @property 189 | def enabled(self): 190 | return self._enabled 191 | 192 | @enabled.setter 193 | def enabled(self, yesno): 194 | if self._enabled != yesno: 195 | self._enabled = yesno 196 | if yesno: 197 | self.enabled() 198 | else: 199 | self.disabled() 200 | 201 | def enabled(self): 202 | self.state = 'normal' 203 | self.on_enabled() 204 | 205 | def disabled(self): 206 | self.state = 'disabled' 207 | self.on_disabled() 208 | 209 | def stylize(self): 210 | """Apply theme style attributes to this instance and its children. 211 | 212 | This also causes a relayout to occur so that any changes in padding 213 | or other stylistic attributes may be handled. 214 | """ 215 | # do children first in case parent needs to override their style 216 | for child in self.children: 217 | child.stylize() 218 | style = theme.current.get_dict(self) 219 | preserve_child = False 220 | try: 221 | preserve_child = getattr(theme.current, 'preserve_child') 222 | except: 223 | preserve_child = False 224 | 225 | for key, val in style.iteritems(): 226 | kvc.set_value_for_keypath(self, key, val, preserve_child) 227 | self.layout() 228 | 229 | def draw(self): 230 | """Do not call directly.""" 231 | 232 | if self.hidden: 233 | return False 234 | 235 | if self.background_color is not None: 236 | render.fillrect(self.surface, self.background_color, 237 | rect=pygame.Rect((0, 0), self.frame.size)) 238 | 239 | for child in self.children: 240 | if not child.hidden: 241 | child.draw() 242 | 243 | topleft = child.frame.topleft 244 | 245 | if child.shadowed: 246 | shadow_size = theme.current.shadow_size 247 | shadow_topleft = (topleft[0] - shadow_size // 2, 248 | topleft[1] - shadow_size // 2) 249 | self.surface.blit(child.shadow_image, shadow_topleft) 250 | 251 | self.surface.blit(child.surface, topleft) 252 | 253 | if child.border_color and child.border_widths is not None: 254 | if (type(child.border_widths) is int and 255 | child.border_widths > 0): 256 | pygame.draw.rect(self.surface, child.border_color, 257 | child.frame, child.border_widths) 258 | else: 259 | tw, lw, bw, rw = child.get_border_widths() 260 | 261 | tl = (child.frame.left, child.frame.top) 262 | tr = (child.frame.right - 1, child.frame.top) 263 | bl = (child.frame.left, child.frame.bottom - 1) 264 | br = (child.frame.right - 1, child.frame.bottom - 1) 265 | 266 | if tw > 0: 267 | pygame.draw.line(self.surface, child.border_color, 268 | tl, tr, tw) 269 | if lw > 0: 270 | pygame.draw.line(self.surface, child.border_color, 271 | tl, bl, lw) 272 | if bw > 0: 273 | pygame.draw.line(self.surface, child.border_color, 274 | bl, br, bw) 275 | if rw > 0: 276 | pygame.draw.line(self.surface, child.border_color, 277 | tr, br, rw) 278 | return True 279 | 280 | def get_border_widths(self): 281 | """Return border width for each side top, left, bottom, right.""" 282 | if type(self.border_widths) is int: # uniform size 283 | return [self.border_widths] * 4 284 | return self.border_widths 285 | 286 | def hit(self, pt): 287 | """Find the view (self, child, or None) under the point `pt`.""" 288 | 289 | if self.hidden or not self._enabled: 290 | return None 291 | 292 | if not self.frame.collidepoint(pt): 293 | return None 294 | 295 | local_pt = (pt[0] - self.frame.topleft[0], 296 | pt[1] - self.frame.topleft[1]) 297 | 298 | for child in reversed(self.children): # front to back 299 | hit_view = child.hit(local_pt) 300 | if hit_view is not None: 301 | return hit_view 302 | 303 | return self 304 | 305 | def center(self): 306 | if self.parent is not None: 307 | self.frame.center = (self.parent.frame.w // 2, 308 | self.parent.frame.h // 2) 309 | 310 | def add_child(self, child): 311 | assert child is not None 312 | self.rm_child(child) 313 | self.children.append(child) 314 | child.parent = self 315 | child.parented() 316 | import scene 317 | if scene.current is not None: 318 | child.stylize() 319 | 320 | def rm_child(self, child): 321 | for index, ch in enumerate(self.children): 322 | if ch == child: 323 | ch.orphaned() 324 | del self.children[index] 325 | break 326 | 327 | def rm(self): 328 | if self.parent: 329 | self.parent.rm_child(self) 330 | 331 | def parented(self): 332 | self.on_parented() 333 | 334 | def orphaned(self): 335 | self.on_orphaned() 336 | 337 | def iter_ancestors(self): 338 | curr = self 339 | while curr.parent: 340 | yield curr.parent 341 | curr = curr.parent 342 | 343 | def iter_children(self): 344 | for child in self.children: 345 | yield child 346 | 347 | def bring_to_front(self): 348 | """TODO: explain depth sorting""" 349 | if self.parent is not None: 350 | ch = self.parent.children 351 | index = ch.index(self) 352 | ch[-1], ch[index] = ch[index], ch[-1] 353 | 354 | def move_to_back(self): 355 | if self.parent is not None: 356 | ch = self.parent.children 357 | index = ch.index(self) 358 | ch[0], ch[index] = ch[index], ch[0] 359 | -------------------------------------------------------------------------------- /pygameui/window.py: -------------------------------------------------------------------------------- 1 | rect = None 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/horatio-sans-serif/pygameui/af6a35f347d6fafa66c4255bbbe38736d842ff65/screenshot.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Use 'distribute', a fork of 'setuptools'. 4 | # This seems to be the recommended tool until 'distutils2' is completed. 5 | # See: http://pypi.python.org/pypi/distribute 6 | 7 | import distribute_setup 8 | distribute_setup.use_setuptools() 9 | from setuptools import setup 10 | 11 | # Find the version from the package metadata. 12 | 13 | import os 14 | import re 15 | 16 | package_version = re.search( 17 | "__version__ = '([^']+)'", 18 | open(os.path.join('pygameui', '__init__.py')).read()).group(1) 19 | 20 | # Resources 21 | # - package_data is what to install. MANIFEST.in is what to bundle. 22 | # The distribute documentation says it can determine what data files to 23 | # include without the need of MANIFEST.in but I had no luck with that. 24 | # - We find the bundled resource files at runtime using the pkg_resources 25 | # module from setuptools. Thus, setuptools is also a dependency. 26 | 27 | # Dependencies 28 | # - While Pygame is listed as a dependency, you should install it separately to 29 | # avoid issues with libpng and others. 30 | # See: http://www.pygame.org/install.html 31 | 32 | setup( 33 | name='pygameui', 34 | version=package_version, 35 | author='Brian Hammond', 36 | author_email='brian@fictorial.com', 37 | install_requires=['setuptools', 'pygame>=1.9.1'], 38 | packages=['pygameui'], 39 | package_data={'pygameui': ['resources/*/*']}, 40 | scripts=['bin/pygameui-kitchensink.py'], 41 | description='GUI framework for Pygame', 42 | keywords="UI GUI Pygame button scrollbar progress slider user interface", 43 | license='MIT', 44 | url='https://github.com/fictorial/pygameui', 45 | classifiers=[ 46 | 'Development Status :: 4 - Beta', 47 | 'Intended Audience :: Developers', 48 | 'License :: OSI Approved :: MIT License', 49 | 'Operating System :: Microsoft :: Windows', 50 | 'Operating System :: MacOS :: MacOS X', 51 | 'Operating System :: POSIX :: Linux', 52 | 'Programming Language :: Python :: 2 :: Only', 53 | 'Topic :: Desktop Environment', 54 | 'Topic :: Games/Entertainment', 55 | 'Topic :: Multimedia', 56 | 'Topic :: Software Development', 57 | 'Topic :: Software Development :: Libraries :: pygame', 58 | 'Topic :: Software Development :: Widget Sets' 59 | ] 60 | ) 61 | --------------------------------------------------------------------------------