# $Id: ScrollBar.py,v 1.46.2.4 2007/01/27 11:10:37 marcusva Exp $
#
# Copyright (c) 2004-2007, Marcus von Appen
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""A widget, which allows scrolling using buttons and a slider."""
from pygame import K_KP_PLUS, K_PLUS, K_RIGHT, K_DOWN, K_KP_MINUS, K_MINUS
from pygame import K_LEFT, K_UP, K_PAGEUP, K_PAGEDOWN, K_HOME, K_END, Rect
from Range import Range
from Constants import *
from StyleInformation import StyleInformation
import base
# Timer value for the button press delay.
_TIMER = 25
class ScrollBar (Range):
"""ScrollBar () -> ScrollBar
An abstract widget class, which is suitable for scrolling.
The ScrollBar widget works much the same like a Scale widget except
that it supports buttons for adjusting the value and that its
minimum value always is 0. It is suitable for widgets which need
scrolling ability and a scrolling logic.
Inheriting widgets have to implement the _get_value_from_coords()
and _get_coords_from_value() methods, which calculate the value of
the ScrollBar using a pair of coordinates and vice versa. Example
implementations can be found in the HScrollBar and VScrollBar widget
classes. They also need to implement the _get_button_coords()
method, which has to return a tuple of the both button coordinates
[(x, y, width, height)].
Default action (invoked by activate()):
Give the ScrollBar the input focus.
Mnemonic action (invoked by activate_mnemonic()):
None
Signals:
SIG_MOUSEDOWN - Invoked, when a mouse button is pressed on the
ScrollBar.
SIG_MOUSEUP - Invoked, when a mouse buttor is released on the
ScrollBar.
SIG_MOUSEMOVE - Invoked, when the mouse moves over the ScrollBar.
Attributes:
button_dec - Indicates, if the decrease button is pressed.
button_inc - Indicates, if the increase button is pressed.
"""
def __init__ (self):
Range.__init__ (self, 0, 1, 1)
# Signals.
self._signals[SIG_MOUSEDOWN] = []
self._signals[SIG_MOUSEMOVE] = []
self._signals[SIG_MOUSEUP] = []
self._signals[SIG_KEYDOWN] = None # Dummy for keyboard activation.
self._signals[SIG_TICK] = None # Dummy for automatic scrolling.
# Internal state handlers for the events. Those need to be known by
# the inheritors.
self._buttondec = False
self._buttoninc = False
self._timer = _TIMER
self._click = False
def activate (self):
"""S.activate () -> None
Activates the ScrollBar default action.
Activates the ScrollBar default action. This usually means giving
the ScrollBar the input focus.
"""
if not self.sensitive:
return
self.focus = True
def _get_button_coords (self, area):
"""S._get_button_coords (...) -> tuple
Gets a tuple with the coordinates of the in- and decrease buttons.
This method has to be implemented by inherited widgets.
"""
raise NotImplementedError
def _get_coords_from_value (self):
"""S._get_coords_from_value () -> float
Calculates the slider coordinates for the ScrollBar.
This method has to be implemented by inherited widgets.
"""
raise NotImplementedError
def _get_value_from_coords (self, area, coords):
"""S._get_value_from_coords (...) -> float
Calculates the slider coordinates for the ScrollBar.
This method has to be implemented by inherited widgets.
"""
raise NotImplementedError
def _get_slider_size (self):
"""S._get_slider_size (...) -> int
Calculates the size of the slider knob.
This method has to be implemented by inherited widgets.
"""
raise NotImplementedError
def _check_collision (self, pos, rect):
"""S._check_collirion (...) -> bool
Checks the collision of the given position with the passed rect.
"""
# Rect: (x, y, width, height), pos: (x, y).
return (pos[0] >= rect[0]) and (pos[0] <= (rect[2] + rect[0])) and \
(pos[1] >= rect[1]) and (pos[1] <= (rect[3] + rect[1]))
def set_minimum (self, minimum):
"""S.set_minimum (...) -> Exception
This method does not have any use.
"""
pass
def notify (self, event):
"""S.notify (...) -> None
Notifies the ScrollBar about an event.
"""
if not self.sensitive:
return
if event.signal in SIGNALS_MOUSE:
eventarea = self.rect_to_client ()
collision = eventarea.collidepoint (event.data.pos)
if event.signal == SIG_MOUSEDOWN and collision:
self.focus = True
# Act only on left clicks or scrollwheel events.
if event.data.button == 1:
self.state = STATE_ACTIVE
self.run_signal_handlers (SIG_MOUSEDOWN, event.data)
if event.data.button == 1:
buttons = self._get_button_coords (eventarea)
if self._check_collision (event.data.pos, buttons[0]):
self._buttondec = True
self._buttoninc = False
self._click = False
self.decrease ()
elif self._check_collision (event.data.pos, buttons[1]):
self._buttoninc = True
self._buttondec = False
self._click = False
self.increase ()
else:
self._click = True
self._buttondec = False
self._buttoninc = False
val = self._get_value_from_coords (eventarea,
event.data.pos)
if val != self.value:
self.value = val
# Mouse wheel.
elif event.data.button == 4:
self.decrease ()
elif event.data.button == 5:
self.increase ()
event.handled = True
elif event.signal == SIG_MOUSEMOVE:
dirty = False
if collision:
self.focus = True
if self.state == STATE_NORMAL:
self.state = STATE_ENTERED
self.run_signal_handlers (SIG_MOUSEMOVE, event.data)
buttons = self._get_button_coords (eventarea)
if not self._check_collision (event.data.pos, buttons[0]) \
and self._buttondec:
self._buttondec = False
dirty = True
if not self._check_collision (event.data.pos, buttons[1]) \
and self._buttoninc:
self._buttoninc = False
dirty = True
if self._click:
val = self._get_value_from_coords (eventarea,
event.data.pos)
if val != self.value:
self.value = val
dirty = False
self.dirty = dirty
event.handled = True
elif self.state == STATE_ENTERED:
self.state = STATE_NORMAL
elif event.signal == SIG_MOUSEUP:
if self._click or self._buttoninc or self._buttondec:
self._buttondec = False
self._buttoninc = False
self._click = False
if collision:
if event.data.button == 1:
if self.state == STATE_ACTIVE:
self.state = STATE_ENTERED
self.run_signal_handlers (SIG_MOUSEUP, event.data)
event.handled = True
else:
self.state = STATE_NORMAL
# Reset timer
self._timer = _TIMER
# The user holds the mouse clicked over one button.
elif (self._buttondec or self._buttoninc) and \
(event.signal == SIG_TICK):
# Wait half a second before starting to in/decrease.
if self._timer > 0:
self._timer -= 1
else:
if self._buttondec:
self.decrease ()
elif self._buttoninc:
self.increase ()
# Keyboard activation.
elif (event.signal == SIG_KEYDOWN) and self.focus:
if event.data.key in (K_KP_PLUS, K_PLUS, K_RIGHT, K_DOWN):
self.increase ()
event.handled = True
elif event.data.key in (K_KP_MINUS, K_MINUS, K_LEFT, K_UP):
self.decrease ()
event.handled = True
elif event.data.key == K_PAGEUP:
val = self.value - 10 * self.step
if val > self.minimum:
self.value = val
else:
self.value = self.minimum
event.handled = True
elif event.data.key == K_PAGEDOWN:
val = self.value + 10 * self.step
if val < self.maximum:
self.value = val
else:
self.value = self.maximum
event.handled = True
elif event.data.key == K_END:
self.value = self.maximum
event.handled = True
elif event.data.key == K_HOME:
self.value = self.minimum
event.handled = True
Range.notify (self, event)
button_dec = property (lambda self: self._buttondec,
doc = """Indicates, whether the decrease
button is pressed.""")
button_inc = property (lambda self: self._buttoninc,
doc = """Indicates, whether the increase
button is pressed.""")
class HScrollBar (ScrollBar):
"""HScrollBar (width, scroll) -> HScrollBar
A horizontal ScrollBar widget.
A ScrollBar widget with a horizontal orientation. By default, its
height is the sum of the button height (HSCROLLBAR_BUTTON_SIZE) and
the border drawn around it (2 * SCROLLBAR_BORDER) and has the passed
width. The scrolling area is the passed scroll value minus the width
of the ScrollBar.
Thus, if the area to scroll is 200 pixels wide and the ScrollBar is
about 100 pixels long, the ScrollBar its value range will go from 0
to 100 (maximum = scroll - width). If the ScrollBar is longer than
the area to scroll (scroll < width), then the value range will be 0.
Note: The minimum size of the scrollbar is at least twice its
size[1] parameter. This means, that it can display the both
scrolling buttons next to each other. This will override the passed
width value in the constructor, if necessary.
"""
def __init__ (self, width, scroll):
ScrollBar.__init__ (self)
# Minimum size for the two scrolling buttons next to each other
border = base.GlobalStyle.get_border_size \
(self.__class__, self.style,
StyleInformation.get ("SCROLLBAR_BORDER")) * 2
height = StyleInformation.get ("HSCROLLBAR_BUTTON_SIZE")[1] + border
if width < 2 * height:
width = 2 * height
self.lock ()
self.minsize = (width, height) # Default size.
self.maximum = scroll
self.unlock ()
def set_maximum (self, maximum):
"""H.set_maximum (...) -> None
Sets the maximum value to scroll.
The passed maximum value differs from maximum value of the
slider. The HScrollBar also subtracts its own height from the
scrolling maximum, so that the real maximum of its value range
can be expressed in the formula:
real_maximum = maximum - self.minsize[1]
That means, that if the HScrollBar is 100 pixels high and the
passed maximum value is 200, the scrolling range of the
HScrollBar will go from 0 to 100 (100 + size = 200).
Raises a ValueError, if the passed argument is smaller than
the first element of the ScrollBar its size.
"""
if maximum < self.minsize[0]:
raise ValueError ("maximum must be greater than or equal to %d"
% self.minsize[0])
ScrollBar.set_maximum (self, maximum - self.minsize[0])
self.dirty = True
def _get_button_coords (self, area):
"""H._get_button_coords (...) -> tuple
Gets a tuple with the coordinates of the in- and decrease buttons.
"""
border = base.GlobalStyle.get_border_size \
(self.__class__, self.style,
StyleInformation.get ("SCROLLBAR_BORDER"))
# Respect the set shadow for the ScrollBar.
button1 = (area.left + border, area.top + border,
area.height - 2 * border, area.height - 2 * border)
button2 = (area.left + area.width - area.height - border,
area.top + border, area.height - 2 * border,
area.height - 2 * border)
return (button1, button2)
def _get_slider_size (self):
"""H._get_slider_size () -> int
Calculates the size of the slider knob.
"""
border = base.GlobalStyle.get_border_size \
(self.__class__, self.style,
StyleInformation.get ("SCROLLBAR_BORDER"))
# Minimum slider size, if the scrollbar is big enough.
minsize = 10
fullsize = self.size[0] - 2 * self.size[1]
if fullsize == 0:
# If only the both scrolling buttons can be displayed, we will
# completely skip the slider.
return 0
# Full size.
fullsize += 2 * border
slider_width = fullsize
if self.maximum != 0:
slider_width = fullsize / (float (self.maximum) + fullsize) * \
fullsize
if slider_width < minsize:
slider_width = minsize
return int (slider_width)
def _get_coords_from_value (self):
"""H._get_coords_from_value () -> int
Calculates the slider coordinates for the HScrollBar.
"""
border = base.GlobalStyle.get_border_size \
(self.__class__, self.style,
StyleInformation.get ("SCROLLBAR_BORDER"))
val = 0
if self.maximum > 0:
slider = self._get_slider_size ()
# Start offset for scrolling - this is the height
# (button + 2 * border) - border plus the half of the
# slider.
sl_x = self.minsize[1] - border + float (slider) / 2
# Valid sliding range.
slide = self.minsize[0] - 2 * sl_x
step = self.maximum / float (slide)
val = self.value / step + sl_x
return val
return self.size[0] / 2
def _get_value_from_coords (self, area, coords):
"""H._get_value_from_coords (...) -> float
Calculates the slider coordinates for the HScrollBar.
"""
border = base.GlobalStyle.get_border_size \
(self.__class__, self.style,
StyleInformation.get ("SCROLLBAR_BORDER"))
val = 0
if self.maximum > 0:
slider = self._get_slider_size ()
sl_x = self.minsize[1] - border + float (slider) / 2
slide = self.minsize[0] - 2 * sl_x
n = coords[0] - area.left - sl_x
step = self.maximum / float (slide)
val = n * step
if val > self.maximum:
val = self.maximum
elif val < 0:
val = 0
return val
def draw_bg (self):
"""H.draw_bg () -> Surface
Draws the HScrollBar background surface and returns it.
Creates the visible surface of the HScrollBar and returns it to
the caller.
"""
return base.GlobalStyle.engine.draw_scrollbar (self,
ORIENTATION_HORIZONTAL)
def draw (self):
"""H.draw () -> None
Draws the HScrollBar surface and places its Buttons and slider on it.
"""
ScrollBar.draw (self)
cls = self.__class__
style = base.GlobalStyle
st = self.style or style.get_style (cls)
rect = self.image.get_rect ()
draw_rect = style.engine.draw_rect
draw_border = style.engine.draw_border
draw_arrow = style.engine.draw_arrow
# Create both buttons.
border = style.get_border_size \
(cls, st, StyleInformation.get ("SCROLLBAR_BORDER"))
button_type = StyleInformation.get ("SCROLLBAR_BUTTON_BORDER")
width_button = rect.height - 2 * border
# We use a temporary state here, so that just the buttons will
# have the typical sunken effect.
tmp_state = self.state
if self.state == STATE_ACTIVE:
tmp_state = STATE_NORMAL
# First button.
state_button = tmp_state
if self.button_dec:
state_button = STATE_ACTIVE
button1 = draw_rect (width_button, width_button, state_button, cls, st)
draw_border (button1, state_button, cls, st, button_type)
rect_button1 = button1.get_rect ()
# Draw the arrow.
draw_arrow (button1, ARROW_LEFT, state_button, cls, st)
rect_button1.x = border
rect_button1.centery = rect.centery
self.image.blit (button1, rect_button1)
# Second button
state_button = tmp_state
if self.button_inc:
state_button = STATE_ACTIVE
button2 = draw_rect (width_button, width_button, state_button, cls, st)
draw_border (button2, state_button, cls, st, button_type)
rect_button2 = button2.get_rect ()
# Draw the arrow.
draw_arrow (button2, ARROW_RIGHT, state_button, cls, st)
rect_button2.x = rect.width - width_button - border
rect_button2.centery = rect.centery
self.image.blit (button2, rect_button2)
# Create the slider.
slider_size = self._get_slider_size ()
if slider_size > 0:
sl = style.engine.draw_slider (slider_size, width_button,
tmp_state, cls, st)
r = sl.get_rect ()
r.centerx = self._get_coords_from_value ()
r.centery = rect.centery
self.image.blit (sl, r)
class VScrollBar (ScrollBar):
"""VScrollBar (height, scroll) -> VScrollBar
A vertical ScrollBar widget.
A ScrollBar widget with a vertical orientation. By default, its
width is the sum of the button width (VSCROLLBAR_BUTTON_SIZE) and
the border drawn around it (2 * SCROLLBAR_BORDER) and has the passed
height. The scrolling area is the passed scroll value minus the
height of the ScrollBar.
Thus, if the area to scroll is 200 pixels high and the ScrollBar is
about 100 pixels high, the ScrollBar its value range will go from 0
to 100 (maximum = scroll - height). If the ScrollBar is longer than
the area to scroll (scroll < height), then the value range will be 0.
Note: The minimum size of the scrollbar is at least twice its
size[0] parameter. This means, that it can display the both
scrolling buttons next to each other. This will override the passed
width value in the constructor, if necessary.
"""
def __init__ (self, height, scroll):
ScrollBar.__init__ (self)
# Minimum size for the two scrolling buttons next to each other.
border = base.GlobalStyle.get_border_size \
(self.__class__, self.style,
StyleInformation.get ("SCROLLBAR_BORDER")) * 2
width = StyleInformation.get ("VSCROLLBAR_BUTTON_SIZE")[0] + border
if height < 2 * width:
height = 2 * width
self.lock ()
self.minsize = (width, height) # Default size.
self.maximum = scroll
self.unlock ()
def set_maximum (self, maximum):
"""V.set_maximum (...) -> None
Sets the maximum value to scroll.
The passed maximum value differs from maximum value of the
slider. The VScrollBar also subtracts its own width from the
scrolling maximum, so that the real maximum of its value range
can be expressed in the formula:
real_maximum = maximum - self.minsize[0]
That means, that if the VScrollBar is 100 pixels long and the
passed maximum value is 200, the scrolling range of the
VScrollBar will go from 0 to 100 (100 + size = 200).
Raises a ValueError, if the passed argument is smaller than
the second element of the ScrollBar its size.
"""
if maximum < self.minsize[1]:
raise ValueError ("maximum must be greater than or equal to %d"
% self.minsize[1])
ScrollBar.set_maximum (self, maximum - self.minsize[1])
self.dirty = True
def _get_button_coords (self, area):
"""V._get_button_coords (...) -> tuple
Gets a tuple with the coordinates of the in- and decrease buttons.
"""
border = base.GlobalStyle.get_border_size \
(self.__class__, self.style,
StyleInformation.get ("SCROLLBAR_BORDER"))
# Respect the set shadow for the ScrollBar.
button1 = (area.left + border, area.top + border,
area.width - 2 * border, area.width - 2 * border)
button2 = (area.left + border,
area.top + area.height - area.width - border,
area.width - 2 * border, area.width - border)
return (button1, button2)
def _get_slider_size (self):
"""V._get_slider_size () -> int
Calculates the size of the slider knob.
"""
# Minimum slider size.
minsize = 10
if (self.size[1] - 2 * self.size[0]) == 0:
# If only the both scrolling buttons can be displayed, we will
# completely skip the slider.
return 0
# Full size.
fullsize = self.size[1] - 2 * self.size[0]
slider_height = fullsize
if self.maximum != 0:
slider_height = fullsize / (float (self.maximum) + fullsize) * \
fullsize
if slider_height < minsize:
slider_height = minsize
return int (slider_height)
def _get_coords_from_value (self):
"""V._get_coords_from_value () -> int
Calculates the slider coordinates for the VScrollBar.
"""
border = base.GlobalStyle.get_border_size \
(self.__class__, self.style,
StyleInformation.get ("SCROLLBAR_BORDER"))
val = 0
if self.maximum > 0:
slider = self._get_slider_size ()
sl_y = self.minsize[0] - border + float (slider) / 2
slide = self.minsize[1] - 2 * sl_y
step = self.maximum / float (slide)
val = self.value / step + sl_y
return val
return self.size[1] / 2
def _get_value_from_coords (self, area, coords):
"""V._get_value_from_coords (...) -> float
Calculates the slider coordinates for the VScrollBar.
"""
border = base.GlobalStyle.get_border_size \
(self.__class__, self.style,
StyleInformation.get ("SCROLLBAR_BORDER"))
val = 0
if self.maximum > 0:
slider = self._get_slider_size ()
# Start offset for scrolling - this is the width
# (button + 2 * border) - border plus the half of the
# slider.
sl_y = self.minsize[0] - border + float (slider) / 2
# Valid sliding range.
slide = self.minsize[1] - 2 * sl_y
n = coords[1] - area.top - sl_y
step = self.maximum / float (slide)
val = n * step
if val > self.maximum:
val = self.maximum
elif val < 0:
val = 0
return val
def draw_bg (self):
"""V.draw_bg (...) -> Surface
Draws the VScrollBar background surface and returns it.
Creates the visible surface of the VScrollBar and returns it to
the caller.
"""
return base.GlobalStyle.engine.draw_scrollbar (self,
ORIENTATION_VERTICAL)
def draw (self):
"""V.draw () -> None
Draws the VScrollBar surface and places its Buttons and slider on it.
"""
ScrollBar.draw (self)
cls = self.__class__
style = base.GlobalStyle
st = self.style or style.get_style (cls)
rect = self.image.get_rect ()
draw_rect = style.engine.draw_rect
draw_border = style.engine.draw_border
draw_arrow = style.engine.draw_arrow
# Create both buttons.
border = style.get_border_size \
(cls, st, StyleInformation.get ("SCROLLBAR_BORDER"))
button_type = StyleInformation.get ("SCROLLBAR_BUTTON_BORDER")
width_button = rect.width - 2 * border
# We use a temporary state here, so that just the buttons will
# have the typical sunken effect.
tmp_state = self.state
if self.state == STATE_ACTIVE:
tmp_state = STATE_NORMAL
# First button.
state_button = tmp_state
if self.button_dec:
state_button = STATE_ACTIVE
button1 = draw_rect (width_button, width_button, state_button, cls, st)
draw_border (button1, state_button, cls, st, button_type)
rect_button1 = button1.get_rect ()
# Draw the arrow.
draw_arrow (button1, ARROW_UP, state_button, cls, st)
rect_button1.y = border
rect_button1.centerx = rect.centerx
self.image.blit (button1, rect_button1)
# Second button
state_button = tmp_state
if self.button_inc:
state_button = STATE_ACTIVE
button2 = draw_rect (width_button, width_button, state_button, cls, st)
draw_border (button2, state_button, cls, st, button_type)
rect_button2 = button2.get_rect ()
# Draw the arrow.
draw_arrow (button2, ARROW_DOWN, state_button, cls, st)
rect_button2.y = rect.height - width_button - border
rect_button2.centerx = rect.centerx
self.image.blit (button2, rect_button2)
# Create the slider.
slider_size = self._get_slider_size ()
if slider_size > 0:
sl = style.engine.draw_slider (width_button, slider_size,
tmp_state, cls, st)
r = sl.get_rect ()
r.centerx = rect.centerx
r.centery = self._get_coords_from_value ()
self.image.blit (sl, r)
syntax highlighted by Code2HTML, v. 0.9.1