pygo/sgc/widgets/scroll_box.py

246 lines
8.9 KiB
Python
Raw Normal View History

2012-04-14 22:38:47 +00:00
#!/usr/bin/env python
# Copyright (C) 2011-2012 Sam Bull
"""
Scroll box. A container widget that provides scroll bars to be able to
view a larger widget.
BUG: Scrolling gives focus. Can't scroll without focus.
BUG: Scroll bar pops up over a modal widget.
BUG: Scroll bars don't work in a modal widget.
"""
import pygame.mouse
from pygame.locals import *
from _locals import *
from base_widget import Simple
class ScrollBox(Simple):
"""
Scroll Box
"""
_can_focus = True
_default_size = (300, 200)
_settings_default = {"widget": None, "col": (118, 45, 215)}
_scroll_x = _scroll_y = None
_handle_x = _handle_y = None
def _config(self, **kwargs):
"""
widget: Widget that should be displayed in scroll box.
col: ``tuple`` (r,g,b) Colour used for scroll bars and handles.
"""
if "widget" in kwargs:
self._settings["widget"] = kwargs["widget"]
self._settings["widget"]._parent = self
self._settings["widget"].pos = (0,0)
if "col" in kwargs:
self._settings["col"] = kwargs["col"]
def _draw(self, draw):
# Create scroll bars and handles
if self._settings["widget"].rect.w > self.rect.w:
ratio = float(self.rect.w) / self._settings["widget"].rect.w
self._scroll_x = Simple((self.rect.w * ratio, 3))
self._scroll_x._parent = self
self._scroll_x.image.fill(self._settings["col"])
self._scroll_x.pos = (0, self.rect.h - 3)
self._handle_x = _ScrollHandleH(widget=self)
if self._settings["widget"].rect.h > self.rect.h:
ratio = float(self.rect.h) / self._settings["widget"].rect.h
self._scroll_y = Simple((3, self.rect.h * ratio))
self._scroll_y._parent = self
self._scroll_y.image.fill(self._settings["col"])
self._scroll_y.pos = (self.rect.w - 3, 0)
self._handle_y = _ScrollHandleV(widget=self)
def update(self, time):
"""Update scroll box each frame."""
self._settings["widget"].update(time)
self.image.fill(0)
self.image.blit(self._settings["widget"].image,
self._settings["widget"].pos)
pos = pygame.mouse.get_pos()
if self._scroll_y is not None:
self.image.blit(self._scroll_y.image, self._scroll_y.pos)
r = self._scroll_y.rect_abs
# Add scroll handles when cursor moves near scroll bar
if not self._handle_y.active() and \
r.inflate(20, 5).collidepoint(pos):
# Position to left if handle would be off-screen.
edge = (r.right + self._handle_y.rect.w)
if edge < get_screen().rect.w:
self._handle_y.rect.x = r.right
else:
self._handle_y.rect.right = r.left
self._handle_y.update_pos(pos[1])
self._handle_y.add()
if self._scroll_x is not None:
self.image.blit(self._scroll_x.image, self._scroll_x.pos)
r = self._scroll_x.rect_abs
if not self._handle_x.active() and \
r.inflate(5, 20).collidepoint(pos):
edge = (r.bottom + self._handle_x.rect.h)
if edge < get_screen().rect.h:
self._handle_x.rect.y = r.bottom
else:
self._handle_x.rect.bottom = r.top
self._handle_x.update_pos(pos[0])
self._handle_x.add()
def _event(self, event):
"""Respond to events."""
self._settings["widget"]._event(event)
if event.type == MOUSEBUTTONDOWN:
if event.button == 4: # Scroll up
self.scroll(y=-10)
elif event.button == 5: # Scroll down
self.scroll(y=10)
elif event.button == 6: # Scroll left
self.scroll(x=-10)
elif event.button == 7: # Scroll right
self.scroll(x=10)
def scroll(self, x=None, y=None):
"""Scroll by x and y coordinates."""
if x is not None and self._scroll_x is not None:
# Set scroll bar position
r = self._scroll_x.rect
r.x = max(min(r.x + x, self.rect.w - r.w), 0)
# Set widget's position
ratio = r.x / float(self.rect.w - r.w)
max_w = self._settings["widget"].rect.w - self.rect.w
self._settings["widget"].rect.x = -max_w * ratio
if y is not None and self._scroll_y is not None:
r = self._scroll_y.rect
r.y = max(min(r.y + y, self.rect.h - r.h), 0)
ratio = r.y / float(self.rect.h - r.h)
max_h = self._settings["widget"].rect.h - self.rect.h
self._settings["widget"].rect.y = -max_h * ratio
def _change_focus(self, forward=True):
return self._settings["widget"]._change_focus(forward)
def _focus_exit(self):
self._settings["widget"]._focus_exit()
class _ScrollHandle(Simple):
"""
Scroll bar to manipulate scroll box.
To be inherited from by _ScrollHandle[V/H], not to be used directly.
Uses lots of getattr() and other tricks to provide inheritable functions.
"""
_can_focus = True
_layered = True
_settings_default = {"widget": None}
_drag = None
def _config(self, **kwargs):
"""
widget: Scroll box that this handle should be synced to.
"""
if "init" in kwargs:
self._rect2 = self.rect_abs.inflate(20, 20)
if "widget" in kwargs:
self._settings["widget"] = kwargs["widget"]
def _draw(self, draw):
self.image.fill(self._settings["widget"]._settings["col"])
self.image.fill((200,200,200), self.rect.inflate(-4, -4))
# Draw line in center
r = self.rect
start_pos = (3, r.centery) if self.xy == "y" else (r.centerx, 3)
end_pos = (r.w-4, r.centery) if self.xy == "y" else (r.centerx, r.h-4)
draw.line(self.image, (100,100,100), start_pos, end_pos)
# Draw arrows
if self.xy == "y":
points1 = ((3, r.h/4), (r.centerx, r.h/5-1), (r.w-3, r.h/4))
points2 = ((3, r.h*.75), (r.centerx, r.h*.8), (r.w-3, r.h*.75))
else:
points1 = ((r.w/4, 3), (r.w/5-1, r.centery), (r.w/4, r.h-3))
points2 = ((r.w*.75, 3), (r.w*.8, r.centery), (r.w*.75, r.h-3))
draw.polygon(self.image, (50,50,50), points1)
draw.polygon(self.image, (50,50,50), points2)
def update_pos(self, xy):
"""
Change position of scroll handle.
Args:
xy: Integer to move the scroll handle to, along the correct axis.
"""
scroll_bar = getattr(self._settings["widget"], "_scroll_%s" % self.xy)
if scroll_bar is not None:
r = scroll_bar.rect_abs
a,b = (r.bottom, r.top) if self.xy == "y" else (r.right, r.left)
xy = min(a, max(xy, b))
setattr(self.rect, "center%s" % self.xy, xy)
self._rect2.center = self.rect.center
def update(self, time):
# Move handle to cursor when cursor not hovering over.
if not self.rect.collidepoint(pygame.mouse.get_pos()):
self.update_pos(pygame.mouse.get_pos()[0 if self.xy == "x" else 1])
# Hide handle when cursor moves too far.
if self._drag is None and \
not self._rect2.collidepoint(pygame.mouse.get_pos()):
self.remove()
def _event(self, event):
index = 1 if self.xy == "y" else 0
if event.type == MOUSEBUTTONDOWN and event.button == 1 and \
self.rect.collidepoint(event.pos):
# Initialise drag
center = getattr(self.rect_abs, "center%s" % self.xy)
self._offset = event.pos[index] - center
self._drag = event.pos[index]
elif self._drag is not None:
if event.type == MOUSEMOTION:
# Move scroll handle and bar
self.update_pos(event.pos[index] - self._offset)
kwarg = {self.xy: event.rel[index]}
self._settings["widget"].scroll(**kwarg)
elif event.type == MOUSEBUTTONUP and event.button == 1:
# Move scroll box up when clicked
if -5 < (self._drag - event.pos[index]) < 5:
center = getattr(self.rect_abs, "center%s" % self.xy)
if event.pos[index] < center:
kwarg = {self.xy: -40}
self._settings["widget"].scroll(**kwarg)
else:
kwarg = {self.xy: 40}
self._settings["widget"].scroll(**kwarg)
# Or stop moving and set final position after drag
else:
self.update_pos(event.pos[index] - self._offset)
self._drag = None
class _ScrollHandleV(_ScrollHandle):
_default_size = (12,50)
xy = "y"
class _ScrollHandleH(_ScrollHandle):
_default_size = (50,12)
xy = "x"