#!/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"