[hamster-applet] updated to the most recent hamster graphics. work in progress; breaks interactivity, might be other
- From: Toms Baugis <tbaugis src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [hamster-applet] updated to the most recent hamster graphics. work in progress; breaks interactivity, might be other
- Date: Mon, 5 Apr 2010 16:24:59 +0000 (UTC)
commit a0cfc6486883fcc917e28834f225b170019e23c0
Author: Toms Bauģis <toms baugis gmail com>
Date: Mon Apr 5 17:24:02 2010 +0100
updated to the most recent hamster graphics. work in progress; breaks interactivity, might be other breakages.
src/hamster/charting.py | 149 ++++---
src/hamster/graphics.py | 949 +++++++++++++++++++++++++++-----------
src/hamster/pytweener.py | 287 ++++++-------
src/hamster/stats.py | 6 +-
src/hamster/widgets/dayline.py | 10 +-
src/hamster/widgets/tags.py | 14 +-
src/hamster/widgets/timechart.py | 63 ++--
7 files changed, 947 insertions(+), 531 deletions(-)
---
diff --git a/src/hamster/charting.py b/src/hamster/charting.py
index 24ecd60..446dc00 100644
--- a/src/hamster/charting.py
+++ b/src/hamster/charting.py
@@ -86,7 +86,7 @@ class Bar(object):
return str((self.value, self.size))
-class Chart(graphics.Area):
+class Chart(graphics.Scene):
"""Chart constructor. Optional arguments:
self.max_bar_width = pixels. Maximal width of bar. If not specified,
bars will stretch to fill whole area
@@ -119,7 +119,7 @@ class Chart(graphics.Area):
"bar-clicked": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
}
def __init__(self, **args):
- graphics.Area.__init__(self)
+ graphics.Scene.__init__(self)
# options
self.max_bar_width = args.get("max_bar_width", 500)
@@ -137,7 +137,6 @@ class Chart(graphics.Area):
self.show_stack_labels = args.get("show_stack_labels", False)
self.labels_at_end = args.get("labels_at_end", False)
- self.framerate = args.get("framerate", 60)
self.interactive = args.get("interactive", False) # if the bars are clickable
@@ -157,8 +156,10 @@ class Chart(graphics.Area):
self.mouse_bar = None
if self.interactive:
- self.connect("mouse-over", self.on_mouse_over)
- self.connect("button-release", self.on_clicked)
+ self.connect("on-mouse-over", self.on_mouse_over)
+ self.connect("on-click", self.on_clicked)
+
+ self.connect("on-enter-frame", self.on_enter_frame)
self.bars_selected = []
@@ -169,7 +170,7 @@ class Chart(graphics.Area):
else:
self.mouse_bar = None
- self.redraw_canvas()
+ self.redraw()
def on_clicked(self, area, bar):
self.emit("bar-clicked", self.mouse_bar)
@@ -191,11 +192,6 @@ class Chart(graphics.Area):
base_hls[2])
- def draw_bar(self, x, y, w, h, color = None):
- """ draws a simple bar"""
- base_color = color or self.bar_base_color or (220, 220, 220)
- self.fill_area(x, y, w, h, base_color)
-
def plot(self, keys, data, stack_keys = None):
"""Draw chart with given data"""
@@ -204,7 +200,7 @@ class Chart(graphics.Area):
self.show()
if not data: #if there is no data, just draw blank
- self.redraw_canvas()
+ self.redraw()
return
@@ -215,13 +211,14 @@ class Chart(graphics.Area):
if not self.animation:
self.tweener.finish()
- self.redraw_canvas()
+ self.redraw()
- def on_expose(self):
+ def on_enter_frame(self, scene, context):
# fill whole area
if self.background:
- self.fill_area(0, 0, self.width, self.height, self.background)
+ g = graphics.Graphics(context)
+ g.fill_area(0, 0, self.width, self.height, self.background)
def _update_targets(self):
@@ -240,9 +237,9 @@ class Chart(graphics.Area):
bars[i] = Bar(new_values[i], 0)
else:
bars[i].value = new_values[i]
- self.tweener.killTweensOf(bars[i])
+ self.tweener.kill_tweens(bars[i])
- self.tweener.addTween(bars[i], size = bars[i].value / float(max_value))
+ self.tweener.add_tween(bars[i], size = bars[i].value / float(max_value))
return bars
retarget(self.bars, self.data)
@@ -263,13 +260,20 @@ class Chart(graphics.Area):
class BarChart(Chart):
- def on_expose(self):
- Chart.on_expose(self)
+ def on_enter_frame(self, scene, context):
+ Chart.on_enter_frame(self, scene, context)
if not self.data:
return
- context = self.context
+ g = graphics.Graphics(context)
+
+ # TODO - should handle the layout business in graphics
+ self.layout = context.create_layout()
+ default_font = pango.FontDescription(gtk.Style().font_desc.to_string())
+ default_font.set_size(8 * pango.SCALE)
+ self.layout.set_font_description(default_font)
+
context.set_line_width(1)
@@ -299,11 +303,11 @@ class BarChart(Chart):
self.graph_height = self.height - 15
if self.chart_background:
- self.fill_area(self.graph_x, self.graph_y,
+ g.fill_area(self.graph_x, self.graph_y,
self.graph_width, self.graph_height,
self.chart_background)
- self.context.stroke()
+ g.stroke()
# bars and keys
max_bar_size = self.graph_height
@@ -335,14 +339,14 @@ class BarChart(Chart):
for key, bar, data in zip(self.keys, self.bars, self.data):
- self.set_color(label_color);
+ g.set_color(label_color);
self.layout.set_text(key)
label_w, label_h = self.layout.get_pixel_size()
intended_x = exes[key][0] + (exes[key][1] - label_w) / 2
if not prev_label_end or intended_x > prev_label_end:
- self.context.move_to(intended_x, self.graph_height + 4)
+ g.move_to(intended_x, self.graph_height + 4)
context.show_layout(self.layout)
prev_label_end = intended_x + label_w + 3
@@ -372,17 +376,17 @@ class BarChart(Chart):
bar_start += bar_size
last_color = self.stack_key_colors.get(self.stack_keys[j]) or self.get_bar_color(j)
- self.draw_bar(exes[key][0],
- self.graph_height - bar_start,
- exes[key][1],
- bar_size,
- last_color)
+ g.fill_area(exes[key][0],
+ self.graph_height - bar_start,
+ exes[key][1],
+ bar_size,
+ last_color)
else:
bar_size = round(max_bar_size * bar.size)
bar_start = bar_size
last_color = self.key_colors.get(key) or base_color
- self.draw_bar(exes[key][0],
+ g.fill_area(exes[key][0],
self.graph_y + self.graph_height - bar_size,
exes[key][1],
bar_size,
@@ -410,9 +414,9 @@ class BarChart(Chart):
# we are in the bar so make sure that the font color is distinguishable
if self.colors.is_light(last_color):
- self.set_color(label_color)
+ g.set_color(label_color)
else:
- self.set_color(self.colors.almost_white)
+ g.set_color(self.colors.almost_white)
context.show_layout(self.layout)
@@ -422,7 +426,7 @@ class BarChart(Chart):
grid_color = self.background
else:
grid_color = self.get_style().bg[gtk.STATE_NORMAL].to_string()
-
+
self.layout.set_width(-1)
if self.grid_stride and self.max_value:
# if grid stride is less than 1 then we consider it to be percentage
@@ -440,18 +444,18 @@ class BarChart(Chart):
label_w, label_h = self.layout.get_pixel_size()
context.move_to(legend_width - label_w - 8,
y - label_h / 2)
- self.set_color(self.colors.aluminium[4])
+ g.set_color(self.colors.aluminium[4])
context.show_layout(self.layout)
- self.set_color(grid_color)
- self.context.move_to(legend_width, y)
- self.context.line_to(self.width, y)
+ g.set_color(grid_color)
+ g.move_to(legend_width, y)
+ g.line_to(self.width, y)
#stack keys
if self.show_stack_labels:
#put series keys
- self.set_color(label_color);
+ g.set_color(label_color);
y = self.graph_height
label_y = None
@@ -512,13 +516,13 @@ class BarChart(Chart):
class HorizontalBarChart(Chart):
- def on_expose(self):
- Chart.on_expose(self)
+ def on_enter_frame(self, scene, context):
+ Chart.on_enter_frame(self, scene, context)
+ g = graphics.Graphics(context)
if not self.data:
return
- context = self.context
rowcount, keys = len(self.keys), self.keys
# push graph to the right, so it doesn't overlap
@@ -532,7 +536,7 @@ class HorizontalBarChart(Chart):
if self.chart_background:
- self.fill_area(self.graph_x, self.graph_y, self.graph_width, self.graph_height, self.chart_background)
+ g.fill_area(self.graph_x, self.graph_y, self.graph_width, self.graph_height, self.chart_background)
if not self.data: # go home if we have nothing
@@ -551,6 +555,13 @@ class HorizontalBarChart(Chart):
max_bar_size = self.graph_width - 15
+ # TODO - should handle the layout business in graphics
+ self.layout = context.create_layout()
+ default_font = pango.FontDescription(gtk.Style().font_desc.to_string())
+ default_font.set_size(8 * pango.SCALE)
+ self.layout.set_font_description(default_font)
+
+
self.layout.set_alignment(pango.ALIGN_RIGHT)
self.layout.set_ellipsize(pango.ELLIPSIZE_END)
@@ -583,7 +594,7 @@ class HorizontalBarChart(Chart):
for i, label in enumerate(keys):
- if self.interactive:
+ if 1 == 2 and self.interactive: # TODO - put interaction back
self.register_mouse_region(0,
positions[label][0],
self.width,
@@ -596,9 +607,9 @@ class HorizontalBarChart(Chart):
label_y = positions[label][0] + (positions[label][1] - label_h) / 2
if i == self.mouse_bar:
- self.set_color(self.get_style().fg[gtk.STATE_PRELIGHT])
+ g.set_color(self.get_style().fg[gtk.STATE_PRELIGHT])
else:
- self.set_color(label_color)
+ g.set_color(label_color)
context.move_to(0, label_y)
@@ -617,7 +628,7 @@ class HorizontalBarChart(Chart):
remaining_pixels -= bar_size
last_color = self.stack_key_colors.get(self.stack_keys[j]) or self.get_bar_color(j)
- self.draw_bar(self.graph_x + bar_start,
+ g.fill_area(self.graph_x + bar_start,
positions[label][0],
bar_size,
positions[label][1],
@@ -634,7 +645,7 @@ class HorizontalBarChart(Chart):
else:
last_color = self.key_colors.get(self.keys[i]) or base_color
- self.draw_bar(self.graph_x,
+ g.fill_area(self.graph_x,
positions[label][0],
bar_size,
positions[label][1],
@@ -659,26 +670,26 @@ class HorizontalBarChart(Chart):
# avoid zero selected bars without any hints
if not self.stack_keys and i in self.bars_selected and self.bars[i].value == 0:
- self.set_color(self.get_style().bg[gtk.STATE_SELECTED])
+ g.set_color(self.get_style().bg[gtk.STATE_SELECTED])
self.draw_rect(label_x - 2,
label_y - 2,
label_w + 4,
label_h + 4, 4)
- self.context.fill()
- self.set_color(self.get_style().fg[gtk.STATE_SELECTED])
+ g.fill()
+ g.set_color(self.get_style().fg[gtk.STATE_SELECTED])
else:
- self.set_color(label_color)
+ g.set_color(label_color)
else:
label_x = self.graph_x + bar_start - label_w - vertical_padding
if i in self.bars_selected:
- self.set_color(self.get_style().fg[gtk.STATE_SELECTED].to_string())
+ g.set_color(self.get_style().fg[gtk.STATE_SELECTED].to_string())
else:
# we are in the bar so make sure that the font color is distinguishable
if self.colors.is_light(last_color):
- self.set_color(label_color)
+ g.set_color(label_color)
else:
- self.set_color(self.colors.almost_white)
+ g.set_color(self.colors.almost_white)
context.move_to(label_x, label_y)
@@ -700,12 +711,10 @@ class HorizontalDayChart(Chart):
self.keys, self.data = keys, data
self.start_time, self.end_time = start_time, end_time
self.show()
- self.redraw_canvas()
-
- def on_expose(self):
- context = self.context
+ self.redraw()
- Chart.on_expose(self)
+ def on_enter_frame(self, scene, context):
+ Chart.on_enter_frame(self, scene, context)
rowcount, keys = len(self.keys), self.keys
start_hour = 0
@@ -732,7 +741,7 @@ class HorizontalDayChart(Chart):
if self.chart_background:
- self.fill_area(self.graph_x, self.graph_y, self.graph_width, self.graph_height, self.chart_background)
+ g.fill_area(self.graph_x, self.graph_y, self.graph_width, self.graph_height, self.chart_background)
if not self.data: #if we have nothing, let's go home
return
@@ -780,7 +789,7 @@ class HorizontalDayChart(Chart):
tick_color = self.colors.darker(bg_color, -50)
for i, label in enumerate(keys):
- self.set_color(label_color)
+ g.set_color(label_color)
self.layout.set_text(label)
label_w, label_h = self.layout.get_pixel_size()
@@ -795,7 +804,7 @@ class HorizontalDayChart(Chart):
bar_x = round((row[0]- start_hour) * factor)
bar_size = round((row[1] - start_hour) * factor - bar_x)
- self.draw_bar(round(self.graph_x + bar_x),
+ g.fill_area(round(self.graph_x + bar_x),
positions[label][0],
bar_size,
positions[label][1],
@@ -808,13 +817,13 @@ class HorizontalDayChart(Chart):
pace = ((end_hour - start_hour) / 3) / 60 * 60
last_position = positions[keys[-1]]
-
-
+
+
if self.background:
grid_color = self.background
else:
grid_color = self.get_style().bg[gtk.STATE_NORMAL].to_string()
-
+
for i in range(start_hour + 60, end_hour, pace):
x = round((i - start_hour) * factor)
@@ -825,13 +834,13 @@ class HorizontalDayChart(Chart):
context.move_to(self.graph_x + x - label_w / 2,
last_position[0] + last_position[1] + 4)
- self.set_color(self.colors.aluminium[4])
+ g.set_color(self.colors.aluminium[4])
context.show_layout(self.layout)
- self.set_color(grid_color)
- self.context.move_to(round(self.graph_x + x) + 0.5, self.graph_y)
- self.context.line_to(round(self.graph_x + x) + 0.5,
+ g.set_color(grid_color)
+ g.move_to(round(self.graph_x + x) + 0.5, self.graph_y)
+ g.line_to(round(self.graph_x + x) + 0.5,
last_position[0] + last_position[1])
@@ -856,7 +865,7 @@ class BasicWindow:
self.series = ["One", "Two", "Three", "Four", "Five", "Six", "Seven"]
- self.stacks = ["x", "y", "z", "a", "b", "c", "d"]
+ self.stacks = ["x"]
self.stack_colors = dict([(stack, None) for stack in self.stacks])
import random
diff --git a/src/hamster/graphics.py b/src/hamster/graphics.py
index 6d32834..d79463d 100644
--- a/src/hamster/graphics.py
+++ b/src/hamster/graphics.py
@@ -1,36 +1,24 @@
# - coding: utf-8 -
-# Copyright (C) 2008-2009 Toms Bauģis <toms.baugis at gmail.com>
+# Copyright (C) 2008-2010 Toms Bauģis <toms.baugis at gmail.com>
+# Dual licensed under the MIT or GPL Version 2 licenses.
+# See http://github.com/tbaugis/hamster_experiments/blob/master/README.textile
-# This file is part of Project Hamster.
-
-# Project Hamster is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# Project Hamster is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-
-# You should have received a copy of the GNU General Public License
-# along with Project Hamster. If not, see <http://www.gnu.org/licenses/>.
import math
-import time, datetime as dt
+import datetime as dt
import gtk, gobject
import pango, cairo
-import pytweener
-from pytweener import Easing
+try:
+ import pytweener
+except: # we can also live without tweener. Scene.animate won't work in this case
+ pytweener = None
+
import colorsys
+from collections import deque
class Colors(object):
- aluminium = [(238, 238, 236), (211, 215, 207), (186, 189, 182),
- (136, 138, 133), (85, 87, 83), (46, 52, 54)]
- almost_white = (250, 250, 250)
-
def parse(self, color):
assert color is not None
@@ -62,59 +50,545 @@ class Colors(object):
# returns color darker by step (where step is in range 0..255)
hls = colorsys.rgb_to_hls(*self.rgb(color))
return colorsys.hls_to_rgb(hls[0], hls[1] - step, hls[2])
+Colors = Colors() # this is a static class, so an instance will do -- TODO - could be bad practice
+
+class Graphics(object):
+ """If context is given upon contruction, will perform drawing
+ operations on context instantly. Otherwise queues up the drawing
+ instructions and performs them in passed-in order when _draw is called
+ with context.
+
+ Most of instructions are mapped to cairo functions by the same name.
+ Where there are differences, documenation is provided.
+
+ See http://www.cairographics.org/documentation/pycairo/reference/context.html#class-context
+ for detailed description of the cairo drawing functions.
+ """
+ def __init__(self, context = None):
+ self._instructions = deque() # instruction set until it is converted into path-based instructions
+ self.instructions = [] # paths colors and operations
+ self.colors = Colors
+ self.extents = None
+ self.opacity = 1.0
+ self.paths = None
+ self.last_matrix = None
+ self.context = context
+
+ def clear(self):
+ """clear all instructions"""
+ self._instructions = deque()
+ self.paths = []
+
+ def _stroke(self, context): context.stroke()
+ def stroke(self, color = None, alpha = 1):
+ """stroke the line with given color and opacity"""
+ if color or alpha < 1:self.set_color(color, alpha)
+ self._add_instruction(self._stroke,)
+
+ def _fill(self, context): context.fill()
+ def fill(self, color = None, alpha = 1):
+ """fill path with given color and opacity"""
+ if color or alpha < 1:self.set_color(color, alpha)
+ self._add_instruction(self._fill,)
+
+ def _stroke_preserve(self, context): context.stroke_preserve()
+ def stroke_preserve(self, color = None, alpha = 1):
+ """same as stroke, only after stroking, don't discard the path"""
+ if color or alpha < 1:self.set_color(color, alpha)
+ self._add_instruction(self._stroke_preserve,)
+
+ def _fill_preserve(self, context): context.fill_preserve()
+ def fill_preserve(self, color = None, alpha = 1):
+ """same as fill, only after filling, don't discard the path"""
+ if color or alpha < 1:self.set_color(color, alpha)
+ self._add_instruction(self._fill_preserve,)
+
+ def _new_path(self, context): context.new_path()
+ def new_path(self):
+ """discard current path"""
+ self._add_instruction(self._new_path,)
+
+ def _paint(self, context): context.paint()
+ def paint(self):
+ """errrm. paint"""
+ self._add_instruction(self._paint,)
+
+ def _set_source_surface(self, context, image, x, y): context.set_source_surface(image, x, y)
+ def set_source_surface(self, image, x = 0, y = 0): self._add_instruction(self._set_source_surface, image, x, y)
+
+ def _move_to(self, context, x, y): context.move_to(x, y)
+ def move_to(self, x, y):
+ """change current position"""
+ self._add_instruction(self._move_to, x, y)
+
+ def _line_to(self, context, x, y): context.line_to(x, y)
+ def line_to(self, x, y):
+ """draw line"""
+ self._add_instruction(self._line_to, x, y)
+
+ def _curve_to(self, 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):
+ """draw curve. (x2, y2) is the middle point of the curve"""
+ self._add_instruction(self._curve_to, x, y, x2, y2, x3, y3)
+
+ def _close_path(self, context): context.close_path()
+ def close_path(self):
+ """connect end with beginning of path"""
+ self._add_instruction(self._close_path,)
+
+ def _set_line_width(self, context, width):
+ context.set_line_width(width)
+ def set_line_style(self, width = None):
+ """change the width of the line"""
+ if width is not None:
+ self._add_instruction(self._set_line_width, width)
+
+ def _set_color(self, context, r, g, b, a):
+ if a * self.opacity >= 1:
+ context.set_source_rgb(r, g, b)
+ else:
+ context.set_source_rgba(r, g, b, a * self.opacity)
+
+ def set_color(self, color, alpha = 1):
+ """set active color. You can use hex colors like "#aaa", or you can use
+ normalized RGB tripplets (where every value is in range 0..1), or
+ you can do the same thing in range 0..65535"""
+ color = self.colors.parse(color) #parse whatever we have there into a normalized triplet
+ if len(color) == 4 and alpha is None:
+ alpha = color[3]
+ r, g, b = color[:3]
+ self._add_instruction(self._set_color, r, g, b, alpha)
+
+ def _arc(self, context, x, y, radius, start_angle, end_angle): context.arc(x, y, radius, start_angle, end_angle)
+ def arc(self, x, y, radius, start_angle, end_angle):
+ """draw arc going counter-clockwise from start_angle to end_angle"""
+ self._add_instruction(self._arc, x, y, radius, start_angle, end_angle)
+
+ def circle(self, x, y, radius):
+ """draw circle"""
+ self._add_instruction(self._arc, x, y, radius, 0, math.pi * 2)
+
+ def ellipse(self, x, y, width, height, edges):
+ """draw 'perfect' ellipse, opposed to squashed circle. works also for
+ equilateral polygons"""
+ steps = edges or max((32, width, height)) / 3 # the automatic edge case is somewhat arbitrary
+
+ angle = 0
+ step = math.pi * 2 / steps
+ points = []
+ while angle < math.pi * 2:
+ points.append((self.width / 2.0 * math.cos(angle),
+ self.height / 2.0 * math.sin(angle)))
+ angle += step
+
+ min_x = min((point[0] for point in points))
+ min_y = min((point[1] for point in points))
+
+ self._move_to(points[0][0] - min_x, points[0][1] - min_y)
+ for x, y in points:
+ self._line_to(x - min_x, y - min_y)
+ self._line_to(points[0][0] - min_x, points[0][1] - min_y)
+
+
+ def _arc_negative(self, context, x, y, radius, start_angle, end_angle): context.arc_negative(x, y, radius, start_angle, end_angle)
+ def arc_negative(self, x, y, radius, start_angle, end_angle):
+ """draw arc going clockwise from start_angle to end_angle"""
+ self._add_instruction(self._arc_negative, x, y, radius, start_angle, end_angle)
+
+ def _rounded_rectangle(self, context, x, y, x2, y2, corner_radius):
+ half_corner = corner_radius / 2
+
+ context.move_to(x + corner_radius, y)
+ context.line_to(x2 - corner_radius, y)
+ context.curve_to(x2 - half_corner, y, x2, y + half_corner, x2, y + corner_radius)
+ context.line_to(x2, y2 - corner_radius)
+ context.curve_to(x2, y2 - half_corner, x2 - half_corner, y2, x2 - corner_radius,y2)
+ context.line_to(x + corner_radius, y2)
+ context.curve_to(x + half_corner, y2, x, y2 - half_corner, x, y2 - corner_radius)
+ context.line_to(x, y + corner_radius)
+ context.curve_to(x, y + half_corner, x + half_corner, y, x + corner_radius,y)
+
+ def _rectangle(self, context, x, y, w, h): context.rectangle(x, y, w, h)
+ def rectangle(self, x, y, width, height, corner_radius = 0):
+ "draw a rectangle. if corner_radius is specified, will draw rounded corners"
+ if corner_radius <=0:
+ self._add_instruction(self._rectangle, x, y, width, height)
+ return
+
+ # make sure that w + h are larger than 2 * corner_radius
+ corner_radius = min(corner_radius, min(width, height) / 2)
+ x2, y2 = x + width, y + height
+ self._add_instruction(self._rounded_rectangle, x, y, x2, y2, corner_radius)
+
+ def fill_area(self, x, y, width, height, color, opacity = 1):
+ """fill rectangular area with specified color"""
+ self.rectangle(x, y, width, height)
+ self.fill(color, opacity)
+
+
+ def _show_layout(self, context, text, font_desc):
+ layout = context.create_layout()
+ layout.set_font_description(font_desc)
+ layout.set_text(text)
+ context.show_layout(layout)
+
+ def show_text(self, text):
+ """display text with system's default font"""
+ font_desc = pango.FontDescription(gtk.Style().font_desc.to_string())
+ self.show_layout(text, font_desc)
+
+ def show_layout(self, text, font_desc):
+ """display text. font_desc is string of pango font description
+ often handier than calling this function directly, is to create
+ a class:Label object
+ """
+ self._add_instruction(self._show_layout, text, font_desc)
+
+ def _remember_path(self, context):
+ context.save()
+ context.identity_matrix()
+ matrix = context.get_matrix()
+
+ new_extents = context.path_extents()
+ self.extents = self.extents or new_extents
+ self.extents = (min(self.extents[0], new_extents[0]),
+ min(self.extents[1], new_extents[1]),
+ max(self.extents[2], new_extents[2]),
+ max(self.extents[3], new_extents[3]))
+
+ self.paths.append(context.copy_path_flat())
+
+ context.restore()
+
+
+ def _add_instruction(self, function, *params):
+ if self.context:
+ function(self.context, *params)
+ else:
+ self.paths = None
+ self._instructions.append((function, params))
+
+
+ def _draw(self, context, with_extents = False):
+ """draw accumulated instructions in context"""
+
+ if self._instructions: #new stuff!
+ self.instructions = deque()
+ current_color = None
+ current_line = None
+ instruction_cache = []
+
+ while self._instructions:
+ instruction, args = self._instructions.popleft()
+
+ if instruction in (self._set_source_surface, self._paint):
+ self.instructions.append((None, None, None, instruction, args))
+
+ elif instruction == self._show_layout:
+ x,y = context.get_current_point() or (0,0)
+ self.instructions.append((None, None, None, self._move_to, (x,y))) #previous move_to call will be actually executed after this
+ self.instructions.append((None, current_color, None, instruction, args))
+
+ else:
+ if instruction == self._set_color:
+ current_color = args
+
+ if instruction == self._set_line_width:
+ current_line = args
+
+ elif instruction in (self._stroke, self._fill, self._stroke_preserve, self._fill_preserve):
+ self.instructions.append((context.copy_path(), current_color, current_line, instruction, ()))
+ context.new_path() # reset even on preserve as the instruction will preserve it instead
+ instruction_cache = []
+ else:
+ instruction(context, *args)
+ instruction_cache.append((instruction, args))
+
+
+ while instruction_cache: # stroke's missing so we just cache
+ instruction, args = instruction_cache.pop(0)
+ self.instructions.append((None, None, None, instruction, args))
+
+
+ # if we have been moved around, we should update bounds
+ check_extents = with_extents and context.get_matrix() != self.last_matrix
+ if check_extents:
+ self.paths = deque()
+ self.extents = None
+
+ for path, color, line, instruction, args in self.instructions:
+ if color: self._set_color(context, *color)
+ if line: self._set_line_width(context, *line)
+
+ if path:
+ context.append_path(path)
+ if check_extents:
+ self._remember_path(context)
+
+ if instruction:
+ instruction(context, *args)
+
+ self.last_matrix = context.get_matrix()
+
+
+
+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"""
+
+ __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-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):
+ gtk.Widget.__init__(self)
+ self.child_sprites = []
+ self.graphics = Graphics()
+ self.interactive = interactive
+ self.draggable = draggable
+ self.pivot_x, self.pivot_y = pivot_x, pivot_y # rotation point in sprite's coordinates
+ self.opacity = opacity
+ self.visible = visible
+ self.parent = None
+ self.x, self.y = x, y
+ self.rotation = rotation
+
+ def add_child(self, *sprites):
+ """Add child sprite. Child will be nested within parent"""
+ for sprite in sprites:
+ self.child_sprites.append(sprite)
+ sprite.parent = self
+
+ def _draw(self, context, opacity = 1):
+ if self.visible is False:
+ return
+
+ if self.x or self.y or self.rotation:
+ context.save()
+
+ if self.x or self.y or self.pivot_x or self.pivot_y:
+ context.translate(self.x + self.pivot_x, self.y + self.pivot_y)
+
+ if self.rotation:
+ context.rotate(self.rotation)
+
+ if self.pivot_x or self.pivot_y:
+ context.translate(-self.pivot_x, -self.pivot_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._draw(context, self.interactive or self.draggable)
+
+ for sprite in self.child_sprites:
+ sprite._draw(context, self.opacity * opacity)
+
+ if self.x or self.y or self.rotation:
+ context.restore()
+
+ def _on_click(self, button_state):
+ self.emit("on-click", button_state)
+
+ def _on_mouse_over(self):
+ # scene will call us when there is mouse
+ self.emit("on-mouse-over")
+
+ def _on_mouse_out(self):
+ # scene will call us when there is mouse
+ self.emit("on-mouse-out")
+
+ def _on_drag(self, x, y):
+ # scene will call us when there is mouse
+ self.emit("on-drag", (x, y))
+
+
+"""a few shapes"""
+class Label(Sprite):
+ def __init__(self, text = "", size = 10, color = None, **kwargs):
+ kwargs.setdefault('interactive', False)
+ Sprite.__init__(self, **kwargs)
+ self.text = text
+ self.color = color
+ self.width, self.height = None, None
+
+ self.font_desc = pango.FontDescription(gtk.Style().font_desc.to_string())
+ self.font_desc.set_size(size * pango.SCALE)
+ self._draw_label()
+
+ def _draw_label(self):
+ self._set_dimensions()
+ self.graphics.move_to(0, 0) #make sure we don't wander off somewhere nowhere
+
+ if self.color:
+ self.graphics.set_color(self.color)
+ self.graphics.show_layout(self.text, self.font_desc)
+
+ 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)
+ self.graphics.stroke()
+
+
+ def _set_dimensions(self):
+ context = gtk.gdk.CairoContext(cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 500, 2000)))
+ layout = context.create_layout()
+ layout.set_font_description(self.font_desc)
+ layout.set_text(self.text)
+
+ self.width, self.height = layout.get_pixel_size()
+
+
+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 functoin in the class")
+
+ def _color(self):
+ if self.line_width:
+ self.graphics.set_line_style(self.line_width)
+
+ if self.fill:
+ if self.stroke:
+ self.graphics.fill_preserve(self.fill)
+ else:
+ self.graphics.fill(self.fill)
+
+ if self.stroke:
+ self.graphics.stroke(self.stroke)
+
+
+
+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)
+
+ def draw_shape(self):
+ self.graphics.rectangle(0, 0, self.width, self.height, self.corner_radius)
+
+
+class Polygon(Shape):
+ def __init__(self, points, **kwargs):
+ self.points = points
+ Shape.__init__(self, **kwargs)
+
+ def draw_shape(self):
+ if not self.points: return
+
+ self.graphics.move_to(*self.points[0])
+ for point in self.points:
+ self.graphics.line_to(*point)
+ self.graphics.close_path()
+
+class Circle(Shape):
+ def __init__(self, radius, **kwargs):
+ self.radius = radius
+ Shape.__init__(self, **kwargs)
+
+ 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)
+
+
+
+class Scene(gtk.DrawingArea):
+ """ Widget for displaying sprites.
+ Add sprites to the Scene by calling :func:`add_child`.
+ Scene is descendant of `gtk.DrawingArea <http://www.pygtk.org/docs/pygtk/class-gtkdrawingarea.html>`_
+ and thus inherits all it's methods and everything.
+ """
-class Area(gtk.DrawingArea):
- """Abstraction on top of DrawingArea to work specifically with cairo"""
__gsignals__ = {
"expose-event": "override",
"configure_event": "override",
- "mouse-over": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
- "button-press": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
- "button-release": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
- "mouse-move": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
- "mouse-click": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
+ "on-enter-frame": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
+ "on-finish-frame": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, )),
+ "on-click": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
+ "on-drag": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
+ "on-mouse-move": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ "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,)),
}
- def __init__(self):
+ def __init__(self, interactive = True, framerate = 80):
gtk.DrawingArea.__init__(self)
- self.set_events(gtk.gdk.EXPOSURE_MASK
- | gtk.gdk.LEAVE_NOTIFY_MASK
- | gtk.gdk.BUTTON_PRESS_MASK
- | gtk.gdk.BUTTON_RELEASE_MASK
- | gtk.gdk.POINTER_MOTION_MASK
- | gtk.gdk.POINTER_MOTION_HINT_MASK)
- self.connect("button_press_event", self.__on_button_press)
- self.connect("button_release_event", self.__on_button_release)
- self.connect("motion_notify_event", self.__on_mouse_move)
- self.connect("leave_notify_event", self.__on_mouse_out)
-
- self.font_size = 8
- self.mouse_regions = [] #regions of drawing that respond to hovering/clicking
-
- self.context, self.layout = None, None
+ if interactive:
+ self.set_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
+ self.connect("motion_notify_event", self.__on_mouse_move)
+ self.connect("button_press_event", self.__on_button_press)
+ self.connect("button_release_event", self.__on_button_release)
+
+ self.sprites = []
+ self.framerate = framerate # frame rate
+
self.width, self.height = None, None
- self.__prev_mouse_regions = None
- self.tweener = pytweener.Tweener(0.4, pytweener.Easing.Cubic.easeInOut)
- self.framerate = 80
- self.last_frame_time = None
+ if pytweener:
+ self.tweener = pytweener.Tweener(0.4, pytweener.Easing.Cubic.ease_in_out)
+
+ self.colors = Colors
+
self.__drawing_queued = False
- self.mouse_drag = None
+ self._last_frame_time = None
+ self._mouse_sprites = set()
+ self._mouse_drag = None
+ self._drag_sprite = None
+ self._drag_x, self._drag_y = None, None
+ self._button_press_time = None # to distinguish between click and drag
+ self.mouse_x, self.mouse_y = None, None
+
+ self._debug_bounds = False
+
- self.colors = Colors() # handier this way
+ def add_child(self, *sprites):
+ """Add one or several :class:`graphics.Sprite` sprites to scene """
+ for sprite in sprites:
+ self.sprites.append(sprite)
- def on_expose(self):
- """ on_expose event is where you hook in all your drawing
- canvas has been initialized for you """
- raise NotImplementedError
+ def clear(self):
+ """Remove all sprites from scene"""
+ self.sprites = []
- def redraw_canvas(self):
- """Redraw canvas. Triggers also to do all animations"""
+ 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
self.__drawing_queued = True
- self.last_frame_time = dt.datetime.now()
+ self._last_frame_time = dt.datetime.now()
gobject.timeout_add(1000 / self.framerate, self.__interpolate)
""" animation bits """
@@ -122,93 +596,39 @@ class Area(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)
- time_since_last_frame = (dt.datetime.now() - self.last_frame_time).microseconds / 1000000.0
- self.tweener.update(time_since_last_frame)
- self.__drawing_queued = self.tweener.hasTweens()
+ self.__drawing_queued = pytweener and self.tweener.has_tweens()
self.queue_draw() # this will trigger do_expose_event when the current events have been flushed
- self.last_frame_time = dt.datetime.now()
+ self._last_frame_time = dt.datetime.now()
return self.__drawing_queued
- def animate(self, object, params = {}, duration = None, easing = None, callback = None, instant = True):
- if duration: params["tweenTime"] = duration # if none will fallback to tweener default
- if easing: params["tweenType"] = easing # if none will fallback to tweener default
- if callback: params["onCompleteFunction"] = callback
- self.tweener.addTween(object, **params)
-
- if instant:
- self.redraw_canvas()
-
-
- """ drawing on canvas bits """
- def draw_rect(self, x, y, w, h, corner_radius = 0):
- if corner_radius <=0:
- self.context.rectangle(x, y, w, h)
- return
-
- # make sure that w + h are larger than 2 * corner_radius
- corner_radius = min(corner_radius, min(w, h) / 2)
-
- x2, y2 = x + w, y + h
-
- half_corner = corner_radius / 2
-
- self.context.move_to(x + corner_radius, y);
- self.context.line_to(x2 - corner_radius, y);
- # top-right
- self.context.curve_to(x2 - half_corner, y,
- x2, y + half_corner,
- x2, y + corner_radius)
-
- self.context.line_to(x2, y2 - corner_radius);
- # bottom-right
- self.context.curve_to(x2, y2 - half_corner,
- x2 - half_corner, y+h,
- x2 - corner_radius,y+h)
-
- self.context.line_to(x + corner_radius, y2);
- # bottom-left
- self.context.curve_to(x + half_corner, y2,
- x, y2 - half_corner,
- x,y2 - corner_radius)
-
- self.context.line_to(x, y + corner_radius);
- # top-left
- self.context.curve_to(x, y + half_corner,
- x + half_corner, y,
- x + corner_radius,y)
-
-
- def rectangle(self, x, y, w, h, color = None, opacity = 0):
- if color:
- self.set_color(color, opacity)
- self.context.rectangle(x, y, w, h)
-
- def fill_area(self, x, y, w, h, color, opacity = 0):
- self.rectangle(x, y, w, h, color, opacity)
- self.context.fill()
-
- def set_text(self, text):
- # sets text and returns width and height of the layout
- self.layout.set_text(text)
- return self.layout.get_pixel_size()
-
- def set_color(self, color, opacity = None):
- color = self.colors.parse(color) #parse whatever we have there into a normalized triplet
+ def animate(self, sprite, instant = True, 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).
- if opacity:
- self.context.set_source_rgba(color[0], color[1], color[2], opacity)
- elif len(color) == 3:
- self.context.set_source_rgb(*color)
- else:
- self.context.set_source_rgba(*color)
+ 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.
+ 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")
+ self.tweener.add_tween(sprite, duration = duration, easing = easing, on_complete = on_complete, on_update = on_update, delay = delay, **kwargs)
- def register_mouse_region(self, x1, y1, x2, y2, region_name):
- self.mouse_regions.append((x1, y1, x2, y2, region_name))
+ if instant:
+ self.redraw()
""" exposure events """
def do_configure_event(self, event):
@@ -216,181 +636,184 @@ class Area(gtk.DrawingArea):
def do_expose_event(self, event):
self.width, self.height = self.window.get_size()
- self.context = self.window.cairo_create()
+ context = self.window.cairo_create()
- self.context.rectangle(event.area.x, event.area.y,
- event.area.width, event.area.height)
- self.context.clip()
+ context.rectangle(event.area.x, event.area.y,
+ event.area.width, event.area.height)
+ context.clip()
- self.layout = self.context.create_layout()
- default_font = pango.FontDescription(gtk.Style().font_desc.to_string())
- default_font.set_size(self.font_size * pango.SCALE)
- self.layout.set_font_description(default_font)
alloc = self.get_allocation() #x, y, width, height
self.width, self.height = alloc.width, alloc.height
- self.mouse_regions = [] #reset since these can move in each redraw
- self.on_expose()
+ self.emit("on-enter-frame", context)
+ for sprite in self.sprites:
+ sprite._draw(context)
- """ mouse events """
- def __on_mouse_move(self, area, event):
- if event.is_hint:
- x, y, state = event.window.get_pointer()
- else:
- x = event.x
- y = event.y
- state = event.state
+ self._check_mouse(self.mouse_x, self.mouse_y)
- self.emit("mouse-move", (x, y), state)
- if not self.mouse_regions:
- return
+ if self._debug_bounds:
+ context.set_line_width(1)
+ context.set_source_rgb(.2, .2, .5)
+ for sprite in self.all_sprites():
+ if sprite.graphics.extents:
+ x,y,x2,y2 = sprite.graphics.extents
+ context.rectangle(x, y, x2-x, y2-y)
+ context.stroke()
- mouse_regions = []
- for region in self.mouse_regions:
- if region[0] < x < region[2] and region[1] < y < region[3]:
- mouse_regions.append(region[4])
- if mouse_regions:
- area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
- else:
- area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
- if mouse_regions != self.__prev_mouse_regions:
- self.emit("mouse-over", mouse_regions)
+ self.emit("on-finish-frame", context)
- self.__prev_mouse_regions = mouse_regions
- def __on_mouse_out(self, area, event):
- self.__prev_mouse_regions = None
- self.emit("mouse-over", [])
+ """ mouse events """
+ def all_sprites(self, sprites = None):
+ """returns flat list of the sprite tree for simplified iteration"""
+ if sprites is None:
+ sprites = self.sprites
- def __on_button_press(self, area, event):
- x = event.x
- y = event.y
- state = event.state
- self.mouse_drag = (x, y)
+ for sprite in sprites:
+ yield sprite
+ if sprite.child_sprites:
+ for child in self.all_sprites(sprite.child_sprites):
+ yield child
- if not self.mouse_regions:
- return
- mouse_regions = []
- for region in self.mouse_regions:
- if region[0] < x < region[2] and region[1] < y < region[3]:
- mouse_regions.append(region[4])
+ def __on_mouse_move(self, area, event):
+ if event.is_hint:
+ mouse_x, mouse_y, state = event.window.get_pointer()
+ else:
+ mouse_x = event.x
+ mouse_y = event.y
+ state = event.state
- if mouse_regions:
- self.emit("button-press", mouse_regions)
+ self.mouse_x, self.mouse_y = mouse_x, mouse_y
- def __on_button_release(self, area, event):
- x = event.x
- y = event.y
- state = event.state
+ if self._drag_sprite and self._drag_sprite.draggable and gtk.gdk.BUTTON1_MASK & event.state:
+ self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
- click = False
- drag_distance = 5
- if self.mouse_drag and (self.mouse_drag[0] - x) ** 2 + (self.mouse_drag[1] - y) ** 2 < drag_distance ** 2:
- #if the drag is less than the drag distance, then we have a click
- click = True
- self.mouse_drag = None
+ # dragging around
+ drag = self._mouse_drag and (self._mouse_drag[0] - event.x) ** 2 + \
+ (self._mouse_drag[1] - event.y) ** 2 > 5 ** 2
+ if drag:
+ matrix = cairo.Matrix()
+ if self._drag_sprite.parent:
+ # TODO - this currently works only until second level - take all parents into account
+ matrix.rotate(self._drag_sprite.parent.rotation)
+ matrix.invert()
- if not self.mouse_regions:
- self.emit("mouse-click", (x,y), [])
- return
+ if not self._drag_x:
+ x1,y1 = matrix.transform_point(self._mouse_drag[0], self._mouse_drag[1])
- mouse_regions = []
- for region in self.mouse_regions:
- if region[0] < x < region[2] and region[1] < y < region[3]:
- mouse_regions.append(region[4])
+ self._drag_x = self._drag_sprite.x - x1
+ self._drag_y = self._drag_sprite.y - y1
- if mouse_regions:
- self.emit("button-release", mouse_regions)
+ mouse_x, mouse_y = matrix.transform_point(mouse_x, mouse_y)
+ new_x = mouse_x + self._drag_x
+ new_y = mouse_y + self._drag_y
- self.emit("mouse-click", (x,y), mouse_regions)
+ self._drag_sprite.x, self._drag_sprite.y = new_x, new_y
+ self._drag_sprite._on_drag(new_x, new_y)
+ self.emit("on-drag", self._drag_sprite, (new_x, new_y))
+ self.redraw()
+ return
+ else:
+ if not self.__drawing_queued: # avoid double mouse checks - the redraw will also check for mouse!
+ self._check_mouse(event.x, event.y)
-""" simple example """
-class SampleArea(Area):
- def __init__(self):
- Area.__init__(self)
- self.rect_x, self.rect_y = 100, -100
- self.rect_width, self.rect_height = 90, 90
+ self.emit("on-mouse-move", event)
- self.text_y = -100
+ def _check_mouse(self, mouse_x, mouse_y):
+ if mouse_x is None: return
- def on_expose(self):
- # on expose is called when we are ready to draw
+ #check if we have a mouse over
+ over = set()
- # fill_area is just a shortcut function
- # feel free to use self.context. move_to, line_to and others
- self.font_size = 32
- self.layout.set_text("Hello, World!")
+ cursor = gtk.gdk.ARROW
- self.draw_rect(round(self.rect_x),
- round(self.rect_y),
- self.rect_width,
- self.rect_height,
- 10)
+ for sprite in self.all_sprites():
+ if sprite.interactive and self._check_hit(sprite, mouse_x, mouse_y):
+ if sprite.draggable:
+ cursor = gtk.gdk.FLEUR
+ else:
+ cursor = gtk.gdk.HAND2
- self.set_color("#ff00ff")
- self.context.fill()
+ over.add(sprite)
- self.context.move_to((self.width - self.layout.get_pixel_size()[0]) / 2,
- self.text_y)
- self.set_color("#333")
- self.context.show_layout(self.layout)
+ new_mouse_overs = over - self._mouse_sprites
+ if new_mouse_overs:
+ for sprite in new_mouse_overs:
+ sprite._on_mouse_over()
+ self.emit("on-mouse-over", list(new_mouse_overs))
-class BasicWindow:
- def __init__(self):
- window = gtk.Window(gtk.WINDOW_TOPLEVEL)
- window.set_title("Graphics Module")
- window.set_size_request(300, 300)
- window.connect("delete_event", lambda *args: gtk.main_quit())
- self.graphic = SampleArea()
+ gone_mouse_overs = self._mouse_sprites - over
+ if gone_mouse_overs:
+ for sprite in gone_mouse_overs:
+ sprite._on_mouse_out()
+ self.emit("on-mouse-out", list(gone_mouse_overs))
- box = gtk.VBox()
- box.pack_start(self.graphic)
- button = gtk.Button("Hello")
- button.connect("clicked", self.on_go_clicked)
+ self._mouse_sprites = over
+ self.window.set_cursor(gtk.gdk.Cursor(cursor))
- box.add_with_properties(button, "expand", False)
- window.add(box)
- window.show_all()
+ def _check_hit(self, sprite, x, y):
+ if sprite == self._drag_sprite:
+ return True
+
+ if not sprite.graphics.extents:
+ return False
- # drop the hello on init
- self.graphic.animate(self.graphic,
- dict(text_y = 120),
- duration = 0.7,
- easing = Easing.Bounce.easeOut)
+ sprite_x, sprite_y, sprite_x2, sprite_y2 = sprite.graphics.extents
+ if sprite_x <= x <= sprite_x2 and sprite_y <= y <= sprite_y2:
+ paths = sprite.graphics.paths
+ if not paths:
+ return True
- def on_go_clicked(self, widget):
- import random
+ context = cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, self.width, self.height))
+ for path in paths:
+ context.append_path(path)
+ return context.in_fill(x, y)
+ else:
+ return False
- # set x and y to random position within the drawing area
- x = round(min(random.random() * self.graphic.width,
- self.graphic.width - self.graphic.rect_width))
- y = round(min(random.random() * self.graphic.height,
- self.graphic.height - self.graphic.rect_height))
- # here we call the animate function with parameters we would like to change
- # the easing functions outside graphics module can be accessed via
- # graphics.Easing
- self.graphic.animate(self.graphic,
- dict(rect_x = x, rect_y = y),
- duration = 0.8,
- easing = Easing.Elastic.easeOut)
+ def __on_button_press(self, area, event):
+ x = event.x
+ y = event.y
+ state = event.state
+ self._mouse_drag = (x, y)
+ over = None
+ for sprite in self.all_sprites():
+ if sprite.interactive and self._check_hit(sprite, event.x, event.y):
+ over = sprite # last one will take precedence
+ self._drag_sprite = over
+ self._button_press_time = dt.datetime.now()
-if __name__ == "__main__":
- example = BasicWindow()
- gtk.main()
+ def __on_button_release(self, area, event):
+ #if the drag is less than 5 pixles, then we have a click
+ click = self._button_press_time and (dt.datetime.now() - self._button_press_time) < dt.timedelta(milliseconds = 300)
+ self._button_press_time = None
+ self._mouse_drag = None
+ self._drag_x, self._drag_y = None, None
+ self._drag_sprite = None
+
+ if click:
+ targets = []
+ for sprite in self.all_sprites():
+ if sprite.interactive and self._check_hit(sprite, event.x, event.y):
+ targets.append(sprite)
+ sprite._on_click(event.state)
+
+ self.emit("on-click", event, targets)
+ self.emit("on-mouse-up")
diff --git a/src/hamster/pytweener.py b/src/hamster/pytweener.py
index c8c3876..64b1235 100644
--- a/src/hamster/pytweener.py
+++ b/src/hamster/pytweener.py
@@ -8,172 +8,145 @@
# Python version by Ben Harling 2009
# All kinds of slashing and dashing by Toms Baugis 2010
import math
+import collections, itertools
class Tweener(object):
def __init__(self, default_duration = None, tween = None):
"""Tweener
This class manages all active tweens, and provides a factory for
creating and spawning tween motions."""
- self.currentTweens = {}
- self.defaultTweenType = tween or Easing.Cubic.easeInOut
- self.defaultDuration = default_duration or 1.0
-
- def hasTweens(self):
- return len(self.currentTweens) > 0
-
-
- def addTween(self, obj, **kwargs):
- """ addTween( object, **kwargs) -> tweenObject or False
-
- Example:
- tweener.addTween( myRocket, throttle=50, setThrust=400, tweenTime=5.0, tweenType=tweener.OUT_QUAD )
-
- You must first specify an object, and at least one property or function with a corresponding
- change value. The tween will throw an error if you specify an attribute the object does
- not possess. Also the data types of the change and the initial value of the tweened item
- must match. If you specify a 'set' -type function, the tweener will attempt to get the
- starting value by call the corresponding 'get' function on the object. If you specify a
- property, the tweener will read the current state as the starting value. You add both
- functions and property changes to the same tween.
-
- in addition to any properties you specify on the object, these keywords do additional
- setup of the tween.
-
- tweenTime = the duration of the motion
- tweenType = one of the predefined tweening equations or your own function
- onComplete = specify a function to call on completion of the tween
- onUpdate = specify a function to call every time the tween updates
- tweenDelay = specify a delay before starting.
- """
- if "tweenTime" in kwargs:
- t_time = kwargs.pop("tweenTime")
- else: t_time = self.defaultDuration
-
- if "tweenType" in kwargs:
- t_type = kwargs.pop("tweenType")
- else: t_type = self.defaultTweenType
-
- if "onComplete" in kwargs:
- t_completeFunc = kwargs.pop("onComplete")
- else: t_completeFunc = None
-
- if "onUpdate" in kwargs:
- t_updateFunc = kwargs.pop("onUpdate")
- else: t_updateFunc = None
-
- if "tweenDelay" in kwargs:
- t_delay = kwargs.pop("tweenDelay")
- else: t_delay = 0
-
- tw = Tween( obj, t_time, t_type, t_completeFunc, t_updateFunc, t_delay, **kwargs )
- if tw:
- tweenlist = self.currentTweens.setdefault(obj, [])
- tweenlist.append(tw)
- return tw
-
- def removeTween(self, tweenObj):
- tweenObj.complete = True
-
- def getTweensAffectingObject(self, obj):
+ self.current_tweens = collections.defaultdict(set)
+ self.default_easing = tween or Easing.Cubic.ease_in_out
+ self.default_duration = default_duration or 1.0
+
+ def has_tweens(self):
+ return len(self.current_tweens) > 0
+
+
+ def add_tween(self, obj, duration = None, easing = None, on_complete = None, on_update = None, delay = None, **kwargs):
+ """
+ Add tween for the object to go from current values to set ones.
+ Example: add_tween(sprite, x = 500, y = 200, duration = 0.4)
+ This will move the sprite to coordinates (500, 200) in 0.4 seconds.
+ For parameter "easing" you can use one of the pytweener.Easing
+ functions, or specify your own.
+ """
+ duration = duration or self.default_duration
+ easing = easing or self.default_easing
+ delay = delay or 0
+
+ tw = Tween(obj, duration, easing, on_complete, on_update, delay, **kwargs )
+ self.current_tweens[obj].add(tw)
+
+
+ def get_tweens(self, obj):
"""Get a list of all tweens acting on the specified object
Useful for manipulating tweens on the fly"""
- return self.currentTweens.get(obj, [])
+ return self.current_tweens.get(obj, None)
+
+ def kill_tweens(self, obj = None):
+ """Stop tweening an object, without completing the motion or firing the
+ on_complete"""
+ if obj:
+ try:
+ del self.current_tweens[obj]
+ except:
+ pass
+ else:
+ self.current_tweens = collections.defaultdict(set)
+
+ def finish(self):
+ """jump the the last frame of all tweens"""
+ for obj in self.current_tweens:
+ for t in self.current_tweens[obj]:
+ t._update(t.duration)
+ self.current_tweens = {}
- def killTweensOf(self, obj):
- """Stop tweening an object, without completing the motion
- or firing the completeFunction"""
- try:
- del self.currentTweens[obj]
- except:
- pass
+ def update(self, delta_seconds):
+ """update tweeners. delta_seconds is time in seconds since last frame"""
+ done_list = set()
+ for obj in self.current_tweens:
+ for tween in self.current_tweens[obj]:
+ done = tween._update(delta_seconds)
+ if done:
+ done_list.add(tween)
+
+ # remove all the completed tweens
+ for tween in done_list:
+ if tween.on_complete:
+ tween.on_complete(tween.target)
+
+ self.current_tweens[tween.target].remove(tween)
+ if not self.current_tweens[tween.target]:
+ del self.current_tweens[tween.target]
- def finish(self):
- #go to last frame for all tweens
- for obj in self.currentTweens:
- for t in self.currentTweens[obj]:
- t.update(t.duration)
- self.currentTweens = {}
-
- def update(self, timeSinceLastFrame):
- for obj in self.currentTweens.keys():
- # updating tweens from last to first and deleting while at it
- # in order to not confuse the index
- for i, t in reversed(list(enumerate(self.currentTweens[obj]))):
- t.update(timeSinceLastFrame)
- if t.complete:
- del self.currentTweens[obj][i]
-
- if not self.currentTweens[obj]:
- del self.currentTweens[obj]
class Tween(object):
- __slots__ = ['duration', 'delay', 'target', 'tween', 'tweenables', 'delta',
- 'target', 'ease', 'tweenables', 'delta', 'completeFunction',
- 'updateFunction', 'complete', 'paused']
+ __slots__ = ('tweenables', 'target', 'delta', 'duration', 'delay',
+ 'ease', 'delta', 'on_complete',
+ 'on_update', 'complete', 'paused')
def __init__(self, obj, duration, easing, on_complete, on_update, delay, **kwargs):
- """Tween object use Tweener.addTween( ... ) to create"""
+ """Tween object use Tweener.add_tween( ... ) to create"""
self.duration = duration
self.delay = delay
self.target = obj
self.ease = easing
- # list of (property, start_value, end_value)
- self.tweenables = [(k, self.target.__dict__[k], v) for k, v in kwargs.items()]
+ # list of (property, start_value, delta)
+ self.tweenables = set(((key, self.target.__dict__[key], value - self.target.__dict__[key]) for key, value in kwargs.items()))
self.delta = 0
- self.completeFunction = on_complete
- self.updateFunction = on_update
+ self.on_complete = on_complete
+ self.on_update = on_update
self.complete = False
self.paused = self.delay > 0
- def pause( self, numSeconds=-1 ):
+ def pause(self, seconds = -1):
"""Pause this tween
do tween.pause( 2 ) to pause for a specific time
or tween.pause() which pauses indefinitely."""
self.paused = True
- self.delay = numSeconds
+ self.delay = seconds
- def resume( self ):
+ def resume(self):
"""Resume from pause"""
if self.paused:
self.paused=False
- def update(self, ptime):
+ def _update(self, ptime):
"""Update tween with the time since the last frame
if there is an update callback, it is always called
whether the tween is running or paused"""
- if self.complete:
- return
+ if self.complete: return
if self.paused:
if self.delay > 0:
- self.delay = max( 0, self.delay - ptime )
+ self.delay = max(0, self.delay - ptime)
if self.delay == 0:
self.paused = False
self.delay = -1
- if self.updateFunction:
- self.updateFunction()
+ if self.on_update:
+ self.on_update()
return
self.delta = self.delta + ptime
if self.delta > self.duration:
self.delta = self.duration
-
- for prop, start_value, end_value in self.tweenables:
- self.target.__dict__[prop] = self.ease(self.delta, start_value, end_value - start_value, self.duration)
+ for prop, start_value, delta_value in self.tweenables:
+ self.target.__dict__[prop] = self.ease(self.delta, start_value, delta_value, self.duration)
if self.delta == self.duration:
self.complete = True
- if self.completeFunction:
- self.completeFunction()
- if self.updateFunction:
- self.updateFunction()
+ if self.on_update:
+ self.on_update(self.target)
+
+ return self.complete
"""Robert Penner's easing classes ported over from actionscript by Toms Baugis (at gmail com).
@@ -216,19 +189,23 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
class Easing(object):
+ """Class containing easing classes to use together with the tweener.
+ All of the classes have :func:`ease_in`, :func:`ease_out` and
+ :func:`ease_in_out` functions."""
+
class Back(object):
@staticmethod
- def easeIn(t, b, c, d, s = 1.70158):
+ def ease_in(t, b, c, d, s = 1.70158):
t = t / d
return c * t * t * ((s+1) * t - s) + b
@staticmethod
- def easeOut (t, b, c, d, s = 1.70158):
+ def ease_out (t, b, c, d, s = 1.70158):
t = t / d - 1
return c * (t * t * ((s + 1) * t + s) + 1) + b
@staticmethod
- def easeInOut (t, b, c, d, s = 1.70158):
+ def ease_in_out (t, b, c, d, s = 1.70158):
t = t / (d * 0.5)
s = s * 1.525
@@ -240,7 +217,7 @@ class Easing(object):
class Bounce(object):
@staticmethod
- def easeOut (t, b, c, d):
+ def ease_out (t, b, c, d):
t = t / d
if t < 1 / 2.75:
return c * (7.5625 * t * t) + b
@@ -255,31 +232,31 @@ class Easing(object):
return c * (7.5625 * t * t + 0.984375) + b
@staticmethod
- def easeIn (t, b, c, d):
- return c - Easing.Bounce.easeOut(d-t, 0, c, d) + b
+ def ease_in (t, b, c, d):
+ return c - Easing.Bounce.ease_out(d-t, 0, c, d) + b
@staticmethod
- def easeInOut (t, b, c, d):
+ def ease_in_out (t, b, c, d):
if t < d * 0.5:
- return Easing.Bounce.easeIn (t * 2, 0, c, d) * .5 + b
+ return Easing.Bounce.ease_in (t * 2, 0, c, d) * .5 + b
- return Easing.Bounce.easeOut (t * 2 -d, 0, c, d) * .5 + c*.5 + b
+ return Easing.Bounce.ease_out (t * 2 -d, 0, c, d) * .5 + c*.5 + b
class Circ(object):
@staticmethod
- def easeIn (t, b, c, d):
+ def ease_in (t, b, c, d):
t = t / d
return -c * (math.sqrt(1 - t * t) - 1) + b
@staticmethod
- def easeOut (t, b, c, d):
+ def ease_out (t, b, c, d):
t = t / d - 1
return c * math.sqrt(1 - t * t) + b
@staticmethod
- def easeInOut (t, b, c, d):
+ def ease_in_out (t, b, c, d):
t = t / (d * 0.5)
if t < 1:
return -c * 0.5 * (math.sqrt(1 - t * t) - 1) + b
@@ -290,17 +267,17 @@ class Easing(object):
class Cubic(object):
@staticmethod
- def easeIn (t, b, c, d):
+ def ease_in (t, b, c, d):
t = t / d
return c * t * t * t + b
@staticmethod
- def easeOut (t, b, c, d):
+ def ease_out (t, b, c, d):
t = t / d - 1
return c * (t * t * t + 1) + b
@staticmethod
- def easeInOut (t, b, c, d):
+ def ease_in_out (t, b, c, d):
t = t / (d * 0.5)
if t < 1:
return c * 0.5 * t * t * t + b
@@ -311,7 +288,7 @@ class Easing(object):
class Elastic(object):
@staticmethod
- def easeIn (t, b, c, d, a = 0, p = 0):
+ def ease_in (t, b, c, d, a = 0, p = 0):
if t==0: return b
t = t / d
@@ -330,7 +307,7 @@ class Easing(object):
@staticmethod
- def easeOut (t, b, c, d, a = 0, p = 0):
+ def ease_out (t, b, c, d, a = 0, p = 0):
if t == 0: return b
t = t / d
@@ -348,7 +325,7 @@ class Easing(object):
@staticmethod
- def easeInOut (t, b, c, d, a = 0, p = 0):
+ def ease_in_out (t, b, c, d, a = 0, p = 0):
if t == 0: return b
t = t / (d * 0.5)
@@ -372,21 +349,21 @@ class Easing(object):
class Expo(object):
@staticmethod
- def easeIn(t, b, c, d):
+ def ease_in(t, b, c, d):
if t == 0:
return b
else:
return c * math.pow(2, 10 * (t / d - 1)) + b - c * 0.001
@staticmethod
- def easeOut(t, b, c, d):
+ def ease_out(t, b, c, d):
if t == d:
return b + c
else:
return c * (-math.pow(2, -10 * t / d) + 1) + b
@staticmethod
- def easeInOut(t, b, c, d):
+ def ease_in_out(t, b, c, d):
if t==0:
return b
elif t==d:
@@ -402,35 +379,35 @@ class Easing(object):
class Linear(object):
@staticmethod
- def easeNone(t, b, c, d):
+ def ease_none(t, b, c, d):
return c * t / d + b
@staticmethod
- def easeIn(t, b, c, d):
+ def ease_in(t, b, c, d):
return c * t / d + b
@staticmethod
- def easeOut(t, b, c, d):
+ def ease_out(t, b, c, d):
return c * t / d + b
@staticmethod
- def easeInOut(t, b, c, d):
+ def ease_in_out(t, b, c, d):
return c * t / d + b
class Quad(object):
@staticmethod
- def easeIn (t, b, c, d):
+ def ease_in (t, b, c, d):
t = t / d
return c * t * t + b
@staticmethod
- def easeOut (t, b, c, d):
+ def ease_out (t, b, c, d):
t = t / d
return -c * t * (t-2) + b
@staticmethod
- def easeInOut (t, b, c, d):
+ def ease_in_out (t, b, c, d):
t = t / (d * 0.5)
if t < 1:
return c * 0.5 * t * t + b
@@ -441,17 +418,17 @@ class Easing(object):
class Quart(object):
@staticmethod
- def easeIn (t, b, c, d):
+ def ease_in (t, b, c, d):
t = t / d
return c * t * t * t * t + b
@staticmethod
- def easeOut (t, b, c, d):
+ def ease_out (t, b, c, d):
t = t / d - 1
return -c * (t * t * t * t - 1) + b
@staticmethod
- def easeInOut (t, b, c, d):
+ def ease_in_out (t, b, c, d):
t = t / (d * 0.5)
if t < 1:
return c * 0.5 * t * t * t * t + b
@@ -462,17 +439,17 @@ class Easing(object):
class Quint(object):
@staticmethod
- def easeIn (t, b, c, d):
+ def ease_in (t, b, c, d):
t = t / d
return c * t * t * t * t * t + b
@staticmethod
- def easeOut (t, b, c, d):
+ def ease_out (t, b, c, d):
t = t / d - 1
return c * (t * t * t * t * t + 1) + b
@staticmethod
- def easeInOut (t, b, c, d):
+ def ease_in_out (t, b, c, d):
t = t / (d * 0.5)
if t < 1:
return c * 0.5 * t * t * t * t * t + b
@@ -482,29 +459,29 @@ class Easing(object):
class Sine(object):
@staticmethod
- def easeIn (t, b, c, d):
+ def ease_in (t, b, c, d):
return -c * math.cos(t / d * (math.pi / 2)) + c + b
@staticmethod
- def easeOut (t, b, c, d):
+ def ease_out (t, b, c, d):
return c * math.sin(t / d * (math.pi / 2)) + b
@staticmethod
- def easeInOut (t, b, c, d):
+ def ease_in_out (t, b, c, d):
return -c * 0.5 * (math.cos(math.pi * t / d) - 1) + b
class Strong(object):
@staticmethod
- def easeIn(t, b, c, d):
+ def ease_in(t, b, c, d):
return c * (t/d)**5 + b
@staticmethod
- def easeOut(t, b, c, d):
+ def ease_out(t, b, c, d):
return c * ((t / d - 1)**5 + 1) + b
@staticmethod
- def easeInOut(t, b, c, d):
+ def ease_in_out(t, b, c, d):
t = t / (d * 0.5)
if t < 1:
@@ -535,7 +512,7 @@ if __name__ == "__main__":
t = dt.datetime.now()
for i, o in enumerate(objects):
- tweener.addTween(o, a = i, b = i, c = i, tweenTime = 1.0)
+ tweener.add_tween(o, a = i, b = i, c = i, duration = 1.0)
print "add", dt.datetime.now() - t
t = dt.datetime.now()
@@ -548,10 +525,8 @@ if __name__ == "__main__":
for i in range(10):
for i, o in enumerate(objects):
- tweener.killTweensOf(o)
- tweener.addTween(o, a = i, b = i, c = i, tweenTime = 1.0)
+ tweener.kill_tweens(o)
+ tweener.add_tween(o, a = i, b = i, c = i, duration = 1.0)
print "kill-add", dt.datetime.now() - t
print "total", dt.datetime.now() - total
-
-
diff --git a/src/hamster/stats.py b/src/hamster/stats.py
index 57ba3c5..2662d78 100644
--- a/src/hamster/stats.py
+++ b/src/hamster/stats.py
@@ -126,15 +126,15 @@ class Stats(object):
#ah, just want summary look just like all the other text on the page
- class CairoText(graphics.Area):
+ class CairoText(graphics.Scene):
def __init__(self, fontsize = 10):
- graphics.Area.__init__(self)
+ graphics.Scene.__init__(self)
self.text = ""
self.fontsize = fontsize
def set_text(self, text):
self.text = text
- self.redraw_canvas()
+ self.redraw()
def on_expose(self):
# now for the text - we want reduced contrast for relaxed visuals
diff --git a/src/hamster/widgets/dayline.py b/src/hamster/widgets/dayline.py
index 05525d1..572943c 100644
--- a/src/hamster/widgets/dayline.py
+++ b/src/hamster/widgets/dayline.py
@@ -28,7 +28,7 @@ import datetime as dt
import colorsys
-class DayLine(graphics.Area):
+class DayLine(graphics.Scene):
def get_value_at_pos(self, x):
"""returns mapped value at the coordinates x,y"""
return x / float(self.width / self.view_minutes)
@@ -36,7 +36,7 @@ class DayLine(graphics.Area):
#normal stuff
def __init__(self):
- graphics.Area.__init__(self)
+ graphics.Scene.__init__(self)
self.set_events(gtk.gdk.EXPOSURE_MASK
| gtk.gdk.LEAVE_NOTIFY_MASK
@@ -87,7 +87,7 @@ class DayLine(graphics.Area):
self.show()
- self.redraw_canvas()
+ self.redraw()
def on_button_release(self, area, event):
@@ -179,7 +179,7 @@ class DayLine(graphics.Area):
if end - start > 1:
self.highlight = (self.get_time(start), self.get_time(end))
- self.redraw_canvas()
+ self.redraw()
self.__call_parent_time_changed()
else:
@@ -202,7 +202,7 @@ class DayLine(graphics.Area):
return delta.days * 24 * 60 + delta.seconds / 60
def scroll_to_range_start(self):
- self.tweener.killTweensOf(self)
+ self.tweener.kill_tweens(self)
self.animate(self, {"range_start_int": int(time.mktime(self.range_start.timetuple())),
"tweenType": graphics.Easing.Expo.easeOut,
"tweenTime": 0.4})
diff --git a/src/hamster/widgets/tags.py b/src/hamster/widgets/tags.py
index e49583c..5476bf0 100644
--- a/src/hamster/widgets/tags.py
+++ b/src/hamster/widgets/tags.py
@@ -212,7 +212,7 @@ class TagsEntry(gtk.Entry):
runtime.dispatcher.del_handler('new_tags_added', self.refresh_tags)
-class TagBox(graphics.Area):
+class TagBox(graphics.Scene):
__gsignals__ = {
'tag-selected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (str,)),
'tag-unselected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (str,)),
@@ -223,13 +223,13 @@ class TagBox(graphics.Area):
self.hover_tag = None
self.tags = []
self.selected_tags = []
- graphics.Area.__init__(self)
+ graphics.Scene.__init__(self)
self.font_size = 10 #override default font size
if self.interactive:
- self.connect("mouse-over", self.on_tag_hover)
- self.connect("button-release", self.on_tag_click)
+ self.connect("on-mouse-over", self.on_tag_hover)
+ self.connect("on-click", self.on_tag_click)
def on_tag_hover(self, widget, regions):
if regions:
@@ -237,7 +237,7 @@ class TagBox(graphics.Area):
else:
self.hover_tag = None
- self.redraw_canvas()
+ self.redraw()
def on_tag_click(self, widget, regions):
tag = regions[0]
@@ -248,13 +248,13 @@ class TagBox(graphics.Area):
#self.selected_tags.append(tag)
self.emit("tag-selected", tag)
- self.redraw_canvas()
+ self.redraw()
def draw(self, tags):
"""Draw chart with given data"""
self.tags = tags
self.show()
- self.redraw_canvas()
+ self.redraw()
def tag_size(self, label):
text_w, text_h = self.set_text(label)
diff --git a/src/hamster/widgets/timechart.py b/src/hamster/widgets/timechart.py
index 3ae07ab..51d4c75 100644
--- a/src/hamster/widgets/timechart.py
+++ b/src/hamster/widgets/timechart.py
@@ -30,11 +30,11 @@ from bisect import bisect
DAY = dt.timedelta(1)
WEEK = dt.timedelta(7)
-class TimeChart(graphics.Area):
+class TimeChart(graphics.Scene):
"""this widget is kind of half finished"""
def __init__(self):
- graphics.Area.__init__(self)
+ graphics.Scene.__init__(self)
self.start_time, self.end_time = None, None
self.durations = []
@@ -45,6 +45,8 @@ class TimeChart(graphics.Area):
self.tick_totals = []
+ self.connect("on-enter-frame", self.on_enter_frame)
+
def draw(self, durations, start_date, end_date):
self.durations = durations
@@ -107,32 +109,33 @@ class TimeChart(graphics.Area):
self.count_hours()
- self.redraw_canvas()
+ self.redraw()
- def on_expose(self):
+ def on_enter_frame(self, scene, context):
if not self.start_time or not self.end_time:
return
+ g = graphics.Graphics(context)
+
# figure out colors
bg_color = self.get_style().bg[gtk.STATE_NORMAL].to_string()
- if self.colors.is_light(bg_color):
- bar_color = self.colors.darker(bg_color, 30)
- tick_color = self.colors.darker(bg_color, 50)
+ if g.colors.is_light(bg_color):
+ bar_color = g.colors.darker(bg_color, 30)
+ tick_color = g.colors.darker(bg_color, 50)
else:
- bar_color = self.colors.darker(bg_color, -30)
- tick_color = self.colors.darker(bg_color, -50)
+ bar_color = g.colors.darker(bg_color, -30)
+ tick_color = g.colors.darker(bg_color, -50)
# now for the text - we want reduced contrast for relaxed visuals
fg_color = self.get_style().fg[gtk.STATE_NORMAL].to_string()
- if self.colors.is_light(fg_color):
- label_color = self.colors.darker(fg_color, 70)
+ if g.colors.is_light(fg_color):
+ label_color = g.colors.darker(fg_color, 70)
else:
- label_color = self.colors.darker(fg_color, -70)
+ label_color = g.colors.darker(fg_color, -70)
-
- self.context.set_line_width(1)
+ g.set_line_style(width=1)
# major ticks
if self.end_time - self.start_time < dt.timedelta(days=1): # about the same day
@@ -177,10 +180,10 @@ class TimeChart(graphics.Area):
def line(x, color):
- self.context.move_to(round(x) + 0.5, 0)
- self.set_color(color)
- self.context.line_to(round(x) + 0.5, self.height)
- self.context.stroke()
+ g.move_to(round(x) + 0.5, 0)
+ g.set_color(color)
+ g.line_to(round(x) + 0.5, self.height)
+ g.stroke()
def somewhere_in_middle(time, color):
# draws line somewhere in middle of the minor tick
@@ -212,15 +215,15 @@ class TimeChart(graphics.Area):
bar_size = max(round(self.height * total * 0.8), 1)
x, bar_width = exes[current_time]
- self.set_color(bar_color)
+ g.set_color(bar_color)
# rounded corners
- self.draw_rect(x, self.height - bar_size, bar_width - 1, bar_size, 3)
+ g.rectangle(x, self.height - bar_size, bar_width - 1, bar_size, 3)
# straighten out bottom rounded corners
- self.context.rectangle(x, self.height - min(bar_size, 2), bar_width - 1, min(bar_size, 2))
+ g.rectangle(x, self.height - min(bar_size, 2), bar_width - 1, min(bar_size, 2))
- self.context.fill()
+ g.fill()
# tick label format
@@ -239,6 +242,12 @@ class TimeChart(graphics.Area):
# tick labels
+ # TODO - should handle the layout business in graphics
+ layout = context.create_layout()
+ default_font = pango.FontDescription(gtk.Style().font_desc.to_string())
+ default_font.set_size(8 * pango.SCALE)
+ layout.set_font_description(default_font)
+
for current_time, total in self.tick_totals:
# if we are on the day level, show label only on week start
if (self.end_time - self.start_time) > dt.timedelta(10) \
@@ -247,11 +256,11 @@ class TimeChart(graphics.Area):
x, bar_width = exes[current_time]
- self.set_color(label_color)
- self.layout.set_width(int((self.width - x) * pango.SCALE))
- self.layout.set_markup(current_time.strftime(step_format))
- self.context.move_to(x + 2, 0)
- self.context.show_layout(self.layout)
+ g.set_color(label_color)
+ layout.set_width(int((self.width - x) * pango.SCALE))
+ layout.set_markup(current_time.strftime(step_format))
+ g.move_to(x + 2, 0)
+ context.show_layout(layout)
def count_hours(self):
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]