[hamster-applet] updated to most recent graphics
- From: Toms Baugis <tbaugis src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [hamster-applet] updated to most recent graphics
- Date: Thu, 13 May 2010 09:41:00 +0000 (UTC)
commit 11c1f095103431c3185017d4f4e15c96fea2f550
Author: Toms Bauģis <toms baugis gmail com>
Date: Wed May 12 02:01:15 2010 +0100
updated to most recent graphics
src/hamster/graphics.py | 355 ++++++++++++++++++++++++++++++++---------------
1 files changed, 242 insertions(+), 113 deletions(-)
---
diff --git a/src/hamster/graphics.py b/src/hamster/graphics.py
index 3f92861..5bb9e70 100644
--- a/src/hamster/graphics.py
+++ b/src/hamster/graphics.py
@@ -9,6 +9,7 @@ import datetime as dt
import gtk, gobject
import pango, cairo
+import re
try:
import pytweener
@@ -19,17 +20,31 @@ import colorsys
from collections import deque
class Colors(object):
+ hex_color_normal = re.compile("#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})")
+ hex_color_short = re.compile("#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])")
+ hex_color_long = re.compile("#([a-fA-F0-9]{4})([a-fA-F0-9]{4})([a-fA-F0-9]{4})")
+
def parse(self, color):
assert color is not None
#parse color into rgb values
- if isinstance(color, str) or isinstance(color, unicode):
- color = gtk.gdk.Color(color)
+ if isinstance(color, basestring):
+ match = self.hex_color_long.match(color)
+ if match:
+ color = [int(color, 16) / 65535.0 for color in match.groups()]
+ else:
+ match = self.hex_color_normal.match(color)
+ if match:
+ color = [int(color, 16) / 255.0 for color in match.groups()]
+ else:
+ match = self.hex_color_short.match(color)
+ color = [int(color + color, 16) / 255.0 for color in match.groups()]
- if isinstance(color, gtk.gdk.Color):
+ elif isinstance(color, gtk.gdk.Color):
color = [color.red / 65535.0,
color.green / 65535.0,
color.blue / 65535.0]
+
else:
# otherwise we assume we have color components in 0..255 range
if color[0] > 1 or color[1] > 1 or color[2] > 1:
@@ -74,12 +89,12 @@ class Graphics(object):
self.opacity = 1.0 # opacity get's adjusted by parent - TODO - wrong inheritance?
self.paths = None # paths for mouse hit checks
self._last_matrix = None
- self._instructions = [] # paths colors and operations
- self._path_instructions = deque() # instruction set until it is converted into path-based instructions
+ self.__instructions = [] # paths colors and operations
+ self.__path_instructions = deque() # instruction set until it is converted into path-based instructions
def clear(self):
"""clear all instructions"""
- self._path_instructions = deque()
+ self.__path_instructions = deque()
self.paths = []
@staticmethod
@@ -141,6 +156,12 @@ class Graphics(object):
self._add_instruction(self._line_to, x, y)
@staticmethod
+ def _rel_line_to(context, x, y): context.rel_line_to(x, y)
+ def rel_line_to(self, x, y):
+ """draw line"""
+ self._add_instruction(self._rel_line_to, x, y)
+
+ @staticmethod
def _curve_to(context, x, y, x2, y2, x3, y3):
context.curve_to(x, y, x2, y2, x3, y3)
def curve_to(self, x, y, x2, y2, x3, y3):
@@ -192,7 +213,7 @@ class Graphics(object):
"""draw 'perfect' ellipse, opposed to squashed circle. works also for
equilateral polygons"""
# the automatic edge case is somewhat arbitrary
- steps = edges or max((32, width, height)) / 3
+ steps = edges or max((32, width, height)) / 2
angle = 0
step = math.pi * 2 / steps
@@ -251,6 +272,18 @@ class Graphics(object):
self.fill(color, opacity)
+ def fill_stroke(self, fill = None, stroke = None, line_width = None):
+ if line_width: self.set_line_style(line_width)
+
+ if fill and stroke:
+ self.fill_preserve(fill)
+ elif fill:
+ self.fill(fill)
+
+ if stroke:
+ self.stroke(stroke)
+
+
@staticmethod
def _show_layout(context, text, font_desc, alignment, width, wrap, ellipsize):
layout = context.create_layout()
@@ -309,7 +342,7 @@ class Graphics(object):
max(self.extents[2], new_extents[2]),
max(self.extents[3], new_extents[3]))
- self.paths.append(context.copy_path_flat())
+ self.paths.append(context.copy_path())
context.restore()
@@ -319,26 +352,26 @@ class Graphics(object):
function(self.context, *params)
else:
self.paths = None
- self._path_instructions.append((function, params))
+ self.__path_instructions.append((function, params))
def _draw(self, context, with_extents = False):
"""draw accumulated instructions in context"""
- if self._path_instructions: #new stuff!
- self._instructions = deque()
+ if self.__path_instructions: #new stuff!
+ self.__instructions = deque()
current_color = None
current_line = None
instruction_cache = []
- while self._path_instructions:
- instruction, args = self._path_instructions.popleft()
+ while self.__path_instructions:
+ instruction, args = self.__path_instructions.popleft()
if instruction in (self._set_source_surface, self._paint):
- self._instructions.append((None, None, None, instruction, args))
+ self.__instructions.append((None, None, None, instruction, args))
elif instruction == self._show_layout:
- self._instructions.append((None, current_color, None, instruction, args))
+ self.__instructions.append((None, current_color, None, instruction, args))
elif instruction == self._set_color:
current_color = args
@@ -349,7 +382,7 @@ class Graphics(object):
elif instruction in (self._stroke, self._fill,
self._stroke_preserve,
self._fill_preserve):
- self._instructions.append((context.copy_path(),
+ self.__instructions.append((context.copy_path(),
current_color,
current_line,
instruction, ()))
@@ -363,7 +396,7 @@ class Graphics(object):
while instruction_cache: # stroke's missing so we just cache
instruction, args = instruction_cache.pop(0)
- self._instructions.append((None, None, None, instruction, args))
+ self.__instructions.append((None, None, None, instruction, args))
# if we have been moved around, we should update bounds
@@ -372,7 +405,7 @@ class Graphics(object):
self.paths = deque()
self.extents = None
- for path, color, line, instruction, args in self._instructions:
+ for path, color, line, instruction, args in self.__instructions:
if color: self._set_color(context, *color)
if line: self._set_line_width(context, *line)
@@ -391,33 +424,72 @@ class Sprite(gtk.Object):
"""The Sprite class is a basic display list building block: a display list
node that can display graphics and can also contain children.
Once you have created the sprite, use Scene's add_child to add it to
- scene"""
+ scene
+ """
__gsignals__ = {
"on-mouse-over": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
"on-mouse-out": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
"on-click": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
"on-drag": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ "on-render": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
#"on-draw": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
}
def __init__(self, x = 0, y = 0,
opacity = 1, visible = True,
rotation = 0, pivot_x = 0, pivot_y = 0,
- interactive = True, draggable = False,
+ scale_x = 1, scale_y = 1,
+ interactive = False, draggable = False,
z_order = 0):
gtk.Object.__init__(self)
+
+ #: list of children sprites. Use :func:`add_child` to add sprites
self.sprites = []
+
+ #: instance of :ref:`graphics` for this sprite
self.graphics = Graphics()
+
+ #: boolean denoting whether the sprite responds to mouse events
self.interactive = interactive
+
+ #: boolean marking if sprite can be automatically dragged
self.draggable = draggable
+
+ #: relative coordinates of the sprites anchor and rotation point
self.pivot_x, self.pivot_y = pivot_x, pivot_y # rotation point in sprite's coordinates
+
+ #: sprite opacity
self.opacity = opacity
+
+ #: boolean visibility flag
self.visible = visible
+
+ #: pointer to parent :class:`Sprite` or :class:`Scene`
self.parent = None
+
+ #: sprite coordinates
self.x, self.y = x, y
+
+ #: rotation of the sprite in radians (use :func:`math.degrees` to convert to degrees if necessary)
self.rotation = rotation
+
+ #: scale X
+ self.scale_x = scale_x
+
+ #: scale Y
+ self.scale_y = scale_y
+
+ #: drawing order between siblings. The one with the highest z_order will be on top.
self.z_order = z_order
+ self._sprite_dirty = True # flag that indicates that the graphics object of the sprite should be rendered
+
+ def __setattr__(self, name, val):
+ self.__dict__[name] = val
+ if name not in ('_sprite_dirty', 'x', 'y', 'rotation', 'scale_x', 'scale_y'):
+ self._sprite_dirty = True
+
+
def add_child(self, *sprites):
"""Add child sprite. Child will be nested within parent"""
for sprite in sprites:
@@ -430,7 +502,7 @@ class Sprite(gtk.Object):
if self.visible is False:
return
- if self.x or self.y or self.rotation:
+ if any([self.x, self.y, self.rotation, self.scale_x, self.scale_y]):
context.save()
if self.x or self.y or self.pivot_x or self.pivot_y:
@@ -442,17 +514,25 @@ class Sprite(gtk.Object):
if self.pivot_x or self.pivot_y:
context.translate(-self.pivot_x, -self.pivot_y)
+ if self.scale_x != 1 or self.scale_y != 1:
+ context.scale(self.scale_x, self.scale_y)
+
self.graphics.opacity = self.opacity * opacity
#self.emit("on-draw") # TODO - this is expensive when doing constant redraw with many frames. maybe we can have a simple callback here?
#self.graphics._move_to(context, 0, 0) # TODO - i'm doing this move because otherwise the currentpoint is pointing to wherever it was left. check if we really need this
context.new_path()
+
+ if (self._sprite_dirty): # send signal to redo the drawing when sprite's dirty
+ self.emit("on-render")
+ self._sprite_dirty = False
+
self.graphics._draw(context, self.interactive or self.draggable)
for sprite in self.sprites:
sprite._draw(context, self.opacity * opacity)
- if self.x or self.y or self.rotation:
+ if any([self.x, self.y, self.rotation, self.scale_x, self.scale_y]):
context.restore()
def _on_click(self, button_state):
@@ -471,74 +551,44 @@ class Sprite(gtk.Object):
self.emit("on-drag", (x, y))
-# a few shapes
-class Shape(Sprite):
- """shape is a simple continuous shape that can have fill and stroke"""
- def __init__(self, stroke = None, fill = None, line_width = None, **kwargs):
- kwargs.setdefault("interactive", False)
- Sprite.__init__(self, **kwargs)
- self.stroke = stroke # stroke color
- self.fill = fill # fill color
- self.line_width = line_width
- self._sprite_dirty = True # a dirty shape needs it's graphics regenerated, because params have changed
- def __setattr__(self, name, val):
- self.__dict__[name] = val
- if name not in ('_sprite_dirty', 'x', 'y', 'rotation'):
- self._sprite_dirty = True
-
-
- def _draw(self, *args, **kwargs):
- if self._sprite_dirty:
- self.graphics.clear()
- self.draw_shape()
- self._color()
- self._sprite_dirty = False
-
- Sprite._draw(self, *args, **kwargs)
-
-
- def draw_shape(self):
- """implement this function in your subclassed object. leave out stroke
- and fill instructions - those will be performed by the shape itself,
- using the stroke and fill attributes"""
- raise NotImplementedError("expected draw_shape function in the class")
+class Label(Sprite):
+ def __init__(self, text = "", size = 10, color = None,
+ alignment = pango.ALIGN_LEFT, **kwargs):
+ Sprite.__init__(self, **kwargs)
+ self.width, self.height = None, None
- def _color(self):
- if self.line_width:
- self.graphics.set_line_style(self.line_width)
+ #: pango.FontDescription, default is the system's font
+ self.font_desc = pango.FontDescription(gtk.Style().font_desc.to_string())
+ self.font_desc.set_size(size * pango.SCALE)
- if self.fill:
- if self.stroke:
- self.graphics.fill_preserve(self.fill)
- else:
- self.graphics.fill(self.fill)
+ #: color of label either as hex string or an (r,g,b) tuple
+ self.color = color
- if self.stroke:
- self.graphics.stroke(self.stroke)
+ self._bounds_width = -1
+ #: wrapping method. Can be set to pango. [WRAP_WORD, WRAP_CHAR,
+ #: WRAP_WORD_CHAR]
+ self.wrap = None
-class Label(Shape):
- def __init__(self, text = "", size = 10, color = None,
- alignment = pango.ALIGN_LEFT, **kwargs):
- kwargs.setdefault('interactive', False)
- Shape.__init__(self, **kwargs)
- self.width, self.height = None, None
+ #: Ellipsize mode. Can be set to pango. [ELLIPSIZE_NONE,
+ #: ELLIPSIZE_START, ELLIPSIZE_MIDDLE, ELLIPSIZE_END]
+ self.ellipsize = None
- self._bounds_width = -1
- self.wrap = None # can be set to pango. [WRAP_WORD, WRAP_CHAR, WRAP_WORD_CHAR]
- self.ellipsize = None # can be set to pango. [ELLIPSIZE_NONE, ELLIPSIZE_START, ELLIPSIZE_MIDDLE, ELLIPSIZE_END]
+ #: alignment. one of pango.[ALIGN_LEFT, ALIGN_RIGHT, ALIGN_CENTER]
+ self.alignment = alignment
- self.font_desc = pango.FontDescription(gtk.Style().font_desc.to_string())
- self.font_desc.set_size(size * pango.SCALE)
+ #: label text
self.text = text
- self.color = color
+
+ #: font size
self.size = size
- self.alignment = alignment
+
+ self.connect("on-render", self.on_render)
def __setattr__(self, name, val):
- Shape.__setattr__(self, name, val)
+ Sprite.__setattr__(self, name, val)
if name == "width":
# setting width means consumer wants to contrain the label
if val is None or val == -1:
@@ -549,7 +599,8 @@ class Label(Shape):
self._set_dimensions()
- def draw_shape(self):
+ def on_render(self, sprite):
+ self.graphics.clear()
if self.interactive: #if label is interactive, draw invisible bounding box for simple hit calculations
self.graphics.set_color("#000", 0)
self.graphics.rectangle(0,0, self.width, self.height)
@@ -579,21 +630,56 @@ class Label(Shape):
self.__dict__['width'], self.height = layout.get_pixel_size()
-class Rectangle(Shape):
- def __init__(self, w, h, corner_radius = 0, **kwargs):
- self.width, self.height, self.corner_radius = w, h, corner_radius
- Shape.__init__(self, **kwargs)
+class Rectangle(Sprite):
+ def __init__(self, w, h, corner_radius = 0, fill = None, stroke = None, **kwargs):
+ Sprite.__init__(self, **kwargs)
+
+ #: width
+ self.width = w
+
+ #: height
+ self.height = h
+
+ #: fill color
+ self.fill = fill
+
+ #: stroke color
+ self.stroke = stroke
- def draw_shape(self):
+ #: stroke line width
+ self.line_width = 1
+
+ #: corner radius. Set bigger than 0 for rounded corners
+ self.corner_radius = corner_radius
+ self.connect("on-render", self.on_render)
+
+ def on_render(self, sprite):
+ self.graphics.clear()
self.graphics.rectangle(0, 0, self.width, self.height, self.corner_radius)
+ self.graphics.fill_stroke(self.fill, self.stroke, self.line_width)
+
+class Polygon(Sprite):
+ def __init__(self, points, fill = None, stroke = None, line_width = 1, **kwargs):
+ Sprite.__init__(self, **kwargs)
-class Polygon(Shape):
- def __init__(self, points, **kwargs):
+ #: list of (x,y) tuples that the line should go through. Polygon
+ #: will automatically close path.
self.points = points
- Shape.__init__(self, **kwargs)
- def draw_shape(self):
+ #: fill color
+ self.fill = fill
+
+ #: stroke color
+ self.stroke = stroke
+
+ #: stroke line width
+ self.line_width = line_width
+
+ self.connect("on-render", self.on_render)
+
+ def on_render(self, sprite):
+ self.graphics.clear()
if not self.points: return
self.graphics.move_to(*self.points[0])
@@ -601,16 +687,38 @@ class Polygon(Shape):
self.graphics.line_to(*point)
self.graphics.close_path()
-class Circle(Shape):
- def __init__(self, radius, **kwargs):
- self.radius = radius
- Shape.__init__(self, **kwargs)
+ self.graphics.fill_stroke(self.fill, self.stroke, self.line_width)
+
+
+class Circle(Sprite):
+ def __init__(self, width, height, fill = None, stroke = None, line_width = 1, **kwargs):
+ Sprite.__init__(self, **kwargs)
+
+ #: circle width
+ self.width = width
- def draw_shape(self):
- self.graphics.move_to(self.radius * 2, self.radius)
- self.graphics.arc(self.radius, self.radius, self.radius, 0, math.pi * 2)
+ #: circle height
+ self.height = height
+ #: fill color
+ self.fill = fill
+
+ #: stroke color
+ self.stroke = stroke
+
+ #: stroke line width
+ self.line_width = line_width
+
+ self.connect("on-render", self.on_render)
+
+ def on_render(self, sprite):
+ self.graphics.clear()
+ if self.width == self.height:
+ self.graphics.circle(self.width, self.width / 2.0, self.width / 2.0)
+ else:
+ self.graphics.ellipse(0, 0, self.width, self.height)
+ self.graphics.fill_stroke(self.fill, self.stroke, self.line_width)
class Scene(gtk.DrawingArea):
""" Widget for displaying sprites.
@@ -620,6 +728,7 @@ class Scene(gtk.DrawingArea):
"""
__gsignals__ = {
+ #: yes can haz instance attribute docstring
"expose-event": "override",
"configure_event": "override",
"on-enter-frame": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
@@ -631,6 +740,7 @@ class Scene(gtk.DrawingArea):
"on-mouse-up": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
"on-mouse-over": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
"on-mouse-out": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ "on-scroll": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
}
def __init__(self, interactive = True, framerate = 80):
@@ -638,24 +748,43 @@ class Scene(gtk.DrawingArea):
if interactive:
self.set_events(gtk.gdk.POINTER_MOTION_MASK
| gtk.gdk.LEAVE_NOTIFY_MASK | gtk.gdk.ENTER_NOTIFY_MASK
- | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
+ | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK
+ | gtk.gdk.SCROLL_MASK)
self.connect("motion_notify_event", self.__on_mouse_move)
self.connect("enter_notify_event", self.__on_mouse_enter)
self.connect("leave_notify_event", self.__on_mouse_leave)
self.connect("button_press_event", self.__on_button_press)
self.connect("button_release_event", self.__on_button_release)
+ self.connect("scroll-event", self.__on_scroll)
+ #: list of sprites in scene. use :func:`add_child` to add sprites
self.sprites = []
- self.framerate = framerate # frame rate
+ #: framerate of animation. This will limit how often call for
+ #: redraw will be performed (that is - not more often than the framerate). It will
+ #: also influence the smoothness of tweeners.
+ self.framerate = framerate
+
+ #: width and height of the scene. Will be `None` until first
+ #: expose (that is until first on-enter-frame signal below).
self.width, self.height = None, None
+ #: instance of :class:`pytweener.Tweener` that is used by
+ #: :func:`animate` function, but can be also accessed directly for advanced control.
+ self.tweener = None
if pytweener:
self.tweener = pytweener.Tweener(0.4, pytweener.Easing.Cubic.ease_in_out)
+ #: instance of :class:`Colors` class for color parsing
self.colors = Colors
+
+ #: last known coordinates of mouse cursor
self.mouse_x, self.mouse_y = None, None
+ #: read only info about current framerate (frames per second)
+ self.fps = 0 # inner frames per second counter
+
+
self._last_frame_time = None
self._mouse_sprites = set()
self._mouse_drag = None
@@ -666,7 +795,7 @@ class Scene(gtk.DrawingArea):
self.__drawing_queued = False
self.__drag_x, self.__drag_y = None, None
-
+ self.__last_expose_time = dt.datetime.now()
def add_child(self, *sprites):
"""Add one or several :class:`graphics.Sprite` sprites to scene """
@@ -682,7 +811,7 @@ class Scene(gtk.DrawingArea):
def redraw(self):
"""Queue redraw. The redraw will be performed not more often than
the `framerate` allows"""
- if not self.__drawing_queued: #if we are moving, then there is a timeout somewhere already
+ if self.__drawing_queued == False: #if we are moving, then there is a timeout somewhere already
self.__drawing_queued = True
self._last_frame_time = dt.datetime.now()
gobject.timeout_add(1000 / self.framerate, self.__interpolate)
@@ -692,34 +821,30 @@ class Scene(gtk.DrawingArea):
if not self.window: #will wait until window comes
return True
- time_since_last_frame = (dt.datetime.now() - self._last_frame_time).microseconds / 1000000.0
- if pytweener:
- self.tweener.update(time_since_last_frame)
+ if self.tweener:
+ self.tweener.update((dt.datetime.now() - self._last_frame_time).microseconds / 1000000.0)
- self.__drawing_queued = pytweener and self.tweener.has_tweens()
+ self.__drawing_queued = self.tweener.has_tweens()
+ self._last_frame_time = dt.datetime.now()
self.queue_draw() # this will trigger do_expose_event when the current events have been flushed
-
- self._last_frame_time = dt.datetime.now()
return self.__drawing_queued
- def animate(self, sprite, instant = True, duration = None, easing = None, on_complete = None, on_update = None, delay = None, **kwargs):
+ def animate(self, sprite, duration = None, easing = None, on_complete = None, on_update = None, delay = None, **kwargs):
"""Interpolate attributes of the given object using the internal tweener
and redrawing scene after every tweener update.
Specify the sprite and sprite's attributes that need changing.
`duration` defaults to 0.4 seconds and `easing` to cubic in-out
(for others see pytweener.Easing class).
- By default redraw is requested right after creating the animation.
- If you would like to add several tweens and only then redraw,
- set `instant` to False.
+ Redraw is requested right after creating the animation.
Example::
# tween some_sprite to coordinates (50,100) using default duration and easing
scene.animate(some_sprite, x = 50, y = 100)
"""
- if not pytweener: # here we complain
- raise Exception("pytweener not found. Include it to enable animations")
+ if not self.tweener: # here we complain
+ raise Exception("pytweener was not found. Include it to enable animations")
self.tweener.add_tween(sprite,
duration=duration,
@@ -728,8 +853,7 @@ class Scene(gtk.DrawingArea):
on_update=on_update,
delay=delay, **kwargs)
- if instant:
- self.redraw()
+ self.redraw()
# exposure events
def do_configure_event(self, event):
@@ -743,12 +867,15 @@ class Scene(gtk.DrawingArea):
event.area.width, event.area.height)
context.clip()
+ now = dt.datetime.now()
+ self.fps = 1 / ((now - self.__last_expose_time).microseconds / 1000000.0)
+ self.__last_expose_time = now
+
self.emit("on-enter-frame", context)
for sprite in self.sprites:
sprite._draw(context)
self._check_mouse(self.mouse_x, self.mouse_y)
-
self.emit("on-finish-frame", context)
@@ -765,6 +892,8 @@ class Scene(gtk.DrawingArea):
for child in self.all_sprites(sprite.sprites):
yield child
+ def __on_scroll(self, area, event):
+ self.emit("on-scroll", event)
def __on_mouse_move(self, area, event):
mouse_x = event.x
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]