Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/objectarea.py
diff options
context:
space:
mode:
authorWade Brainerd <wadetb@gmail.com>2009-01-03 02:53:53 (GMT)
committer Wade Brainerd <wadetb@gmail.com>2009-01-03 02:53:53 (GMT)
commit054626904b8413a6264e9cc621a00f10d0d8084d (patch)
treeface136809c1bc2aa7a23c9cd62a4efdfd4c2b32 /objectarea.py
parentf04417ac26fb6087e45067fd938647e163b5bba8 (diff)
Split mathactivity.py into multiple source files.
Diffstat (limited to 'objectarea.py')
-rw-r--r--objectarea.py436
1 files changed, 436 insertions, 0 deletions
diff --git a/objectarea.py b/objectarea.py
new file mode 100644
index 0000000..5e0c465
--- /dev/null
+++ b/objectarea.py
@@ -0,0 +1,436 @@
+# Copyright 2008 by Peter Moxhay and Wade Brainerd.
+# This file is part of Math.
+#
+# Math 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.
+#
+# Math 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 Math. If not, see <http://www.gnu.org/licenses/>.
+from vector import Vector
+
+import gtk, math
+
+# The global grid unit size, in pixels. Objects will snap to multiples of this value.
+GRID_SIZE = 50
+GRID_VISIBLE = True
+
+# Width and height of region within which we can drag objects.
+DRAGGING_RECT_WIDTH = 24*GRID_SIZE
+DRAGGING_RECT_HEIGHT = 16*GRID_SIZE
+
+# The global grid angle size, in radians.
+RADIAL_GRID_SIZE = math.pi/4
+
+# Class containing various standard colors. Each color is a 3 element tuple.
+class Color:
+ BLUE = (0.25, 0.25, 0.75)
+ GREEN = (0.25, 0.75, 0.25)
+ RED = (0.75, 0.25, 0.25)
+
+class Object:
+ """
+ Objects are shapes with which the user can interact.
+ They are contained within ObjectArea widgets.
+
+ Object subclasses must implement the following methods:
+ draw(self, cr): Draw's the object using a Cairo graphics context.
+ contains_point(self, pos): Returns True if pos is within the object.
+ """
+
+ def __init__(self):
+ pass
+
+ def draw(self, cr):
+ pass
+
+class ObjectArea(gtk.Layout):
+ """Widget containing interactive Objects."""
+
+ def __init__(self, activity):
+ gtk.Layout.__init__(self)
+
+ self.activity = activity
+
+ self.objects = []
+ self.shape_objects = []
+
+ # Object currently selected.
+ self.selected_object = None
+
+ # Object being hovered over.
+ self.hover_object = None
+
+ # Object currently being dragged.
+ self.drag_object = None
+ self.drag_offset = Vector(0, 0)
+
+ # Set up mouse events.
+ self.add_events(gtk.gdk.POINTER_MOTION_MASK|gtk.gdk.BUTTON_PRESS_MASK|gtk.gdk.BUTTON_RELEASE_MASK)
+ self.connect('motion-notify-event', self.on_mouse)
+ self.connect('button-press-event', self.on_mouse)
+ self.connect('button-release-event', self.on_mouse)
+
+ # Set up keyboard events.
+ self.add_events(gtk.gdk.KEY_PRESS_MASK)
+ self.activity.connect('key-press-event', self.on_key)
+
+ # Set up drawing events.
+ self.modify_bg(gtk.STATE_NORMAL, self.get_colormap().alloc_color('#ffffff'))
+ self.connect('expose-event', self.expose_cb)
+
+ # Determine whether to draw the line segment targets.
+ self.line_segments_target_visible = False
+
+ # Capture the last drag coordinates
+ self.x_drag = 0
+ self.y_drag = 0
+
+ # The letters representing the two quantities
+ self.letter_1= 'A'
+ self.letter_2 = 'B'
+ self.question_mark = '?'
+ self.letter1_pos = Vector(895, 510)
+ self.letter2_pos = Vector(1006, 510)
+ self.question_mark_pos = Vector(952, 510)
+ self.question_mark_visible = True
+
+ def check_problem_solved(self):
+ pass
+
+ def add_object(self, obj):
+ self.objects.append(obj)
+ self.queue_draw()
+
+ def add_shape_object(self, obj):
+ self.objects.append(obj)
+ self.shape_objects.append(obj)
+ self.queue_draw()
+ # Make sure the shape is placed correctly on the grid.
+ self.snap_object_to_grid(obj)
+
+ def clear_selection(self):
+ if self.selected_object:
+ self.selected_object.selected = False
+
+ self.queue_draw()
+
+ def select_object(self, object):
+ if self.selected_object:
+ self.selected_object.selected = False
+
+ self.selected_object = object
+ object.selected = True
+
+ # Move to front of list so it draws on top.
+ # TODO: Screws up tab key. Maybe we need separate draw & tab lists?
+ #self.objects.remove(object)
+ #self.objects.insert(0, object)
+
+ self.queue_draw()
+
+ def move_object(self, object, pos):
+ x, y = pos.x, pos.y
+
+ # Return if the object is not draggable
+ if object.draggable == False:
+ return
+
+ # Tentatively place in the object in the new position
+ object.pos = Vector(x, y)
+
+ # Check whether any point of the object is outside the screen
+ for i in range (0, len(object.points) ):
+ p1 = object.points[i]
+ p1 = object.transform_point(p1)
+
+ if p1.x < -2 or p1.x > DRAGGING_RECT_WIDTH + 2 or p1.y < -1 or p1.y > DRAGGING_RECT_HEIGHT:
+ # If any point is outside, move object back to last position.
+ object.pos = object.last_pos
+
+ self.queue_draw()
+
+ def rotate_object(self, object, delta):
+ angle = delta
+
+ # Return if the object is not draggable
+ if object.draggable == False:
+ return
+
+ # Tentatively rotate the object to the new angle
+ object.angle = object.angle + angle
+
+ # Check whether any point of the object is outside the screen
+ for i in range (0, len(object.points) ):
+ p1 = object.points[i]
+ p1 = object.transform_point(p1)
+
+ if p1.x < -1 or p1.x > DRAGGING_RECT_WIDTH or p1.y < -1 or p1.y > DRAGGING_RECT_HEIGHT:
+ # If any point is outside, move object back to last position.
+ object.angle = object.last_angle
+
+ self.queue_draw()
+
+ def snap_object_to_grid(self, object):
+ x, y = object.pos.x, object.pos.y
+ angle = object.angle
+
+ # Return if the object is not draggable
+ if object.draggable == False:
+ return
+
+ # Convert angle to a positive value
+ angle = (angle + 4 * math.pi) % (2 * math.pi)
+
+ # Snap position to grid.
+ x = int((x + GRID_SIZE/2 + 1)/ GRID_SIZE) * GRID_SIZE
+ y = int((y + GRID_SIZE/2 + 1)/ GRID_SIZE) * GRID_SIZE
+
+ # Snap angle to "radial grid"
+ angle = (int((angle + RADIAL_GRID_SIZE/2)/ RADIAL_GRID_SIZE) * RADIAL_GRID_SIZE) % (2 * math.pi)
+
+ object.pos = Vector(x, y)
+ object.angle = angle
+
+ # Get the current width and height of the object's bounding rectangle.
+ object.calculate_width_and_height()
+
+ self.queue_draw()
+
+ # Give the derived class an opportunity to advance to the next stage, if the problem has been solved
+ # by this movement.
+ self.check_problem_solved()
+
+ def move_object_rel(self, object, delta):
+ self.move_object(object, object.pos + delta)
+
+ def on_mouse(self, widget, event):
+ epos = Vector(event.x, event.y)
+
+ # Process in progress dragging, if active.
+ if self.drag_object:
+
+ if event.type == gtk.gdk.MOTION_NOTIFY:
+ # Rotate while dragging if option key is pressed
+ if event.state & gtk.gdk.MOD1_MASK:
+ # Save the previous angle in case we have to undo the rotation.
+ self.selected_object.last_angle = self.selected_object.angle
+ self.rotate_object(self.drag_object,
+ math.atan2(epos.y - self.drag_object.pos.y,
+ epos.x - self.drag_object.pos.x) -
+ math.atan2(self.y_drag - self.drag_object.pos.y,
+ self.x_drag - self.drag_object.pos.x))
+ else:
+ self.selected_object.last_pos = self.selected_object.pos
+ self.move_object(self.drag_object, epos - self.drag_offset)
+
+ elif event.type == gtk.gdk.BUTTON_RELEASE:
+ # Snap the object to the grid after drag ends.
+ self.snap_object_to_grid(self.drag_object)
+ self.drag_object = None
+
+ # Remember the coordinates of the drag
+ self.x_drag = epos.x
+ self.y_drag = epos.y
+
+ self.queue_draw()
+ return
+
+ # Process button clicks.
+ if event.type == gtk.gdk.BUTTON_PRESS:
+ # Any mouse movement over the canvas grabs focus, so we can receive
+ # keyboard events.
+ if not widget.is_focus():
+ widget.grab_focus()
+
+ # Remember the coordinates of the press
+ self.x_drag = epos.x
+ self.y_drag = epos.y
+
+ # Select clicked object.
+ for o in self.objects:
+ if o.contains_point(epos):
+ self.select_object(o)
+
+ self.drag_object = o
+ self.drag_offset = epos - o.pos
+ break
+
+ self.queue_draw()
+ return
+
+ # Process hovering.
+ if event.type == gtk.gdk.MOTION_NOTIFY:
+
+ hover = None
+ for o in self.objects:
+ if o.contains_point(epos):
+ hover = o
+ break
+
+ if hover != self.hover_object:
+ if hover:
+ self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
+ else:
+ self.window.set_cursor(None)
+ self.hover_object = hover
+
+ self.queue_draw()
+ return
+
+ def on_key(self, widget, event):
+ key_name = gtk.gdk.keyval_name(event.keyval)
+
+ # Useful print for determining the names of keys.
+ #print key_name
+
+ # Tab key selects the next object.
+ if key_name == 'Tab':
+ if len(self.objects):
+ if self.selected_object:
+ index = self.objects.index(self.selected_object)
+ index = (index + 1) % len(self.objects)
+ self.select_object(self.objects[index])
+ else:
+ self.select_object(self.objects[0])
+ return True
+
+ if key_name == 'Up':
+ if self.selected_object:
+ # Rotate if option key is pressed.
+ if event.state & MOD1_MASK:
+ # Save the previous angle in case we have to undo the rotation.
+ self.selected_object.last_angle = self.selected_object.angle
+ self.rotate_object(self.selected_object, -RADIAL_GRID_SIZE)
+ else:
+ # Save the previous position in case we have to undo the move.
+ self.selected_object.last_pos = self.selected_object.pos
+ # Move the object.
+ self.move_object_rel(self.selected_object, Vector(0, -GRID_SIZE))
+
+ self.snap_object_to_grid(self.selected_object)
+
+ return True
+
+ if key_name == 'Down':
+ if self.selected_object:
+ if event.state & MOD1_MASK:
+ # Save the previous angle in case we have to undo the rotation.
+ self.selected_object.last_angle = self.selected_object.angle
+ self.rotate_object(self.selected_object, RADIAL_GRID_SIZE)
+ else:
+ # Save the previous position in case we have to undo the move.
+ self.selected_object.last_pos = self.selected_object.pos
+ # Move the object.
+ self.move_object_rel(self.selected_object, Vector(0, GRID_SIZE))
+
+ self.snap_object_to_grid(self.selected_object)
+
+ return True
+
+ if key_name == 'Left':
+ if self.selected_object:
+ if event.state & MOD1_MASK:
+ # Save the previous angle in case we have to undo the rotation.
+ self.selected_object.last_angle = self.selected_object.angle
+ self.rotate_object(self.selected_object, -RADIAL_GRID_SIZE)
+ else:
+ # Save the previous position in case we have to undo the move.
+ self.selected_object.last_pos = self.selected_object.pos
+ # Move the object.
+ self.move_object_rel(self.selected_object, Vector(-GRID_SIZE, 0))
+
+ self.snap_object_to_grid(self.selected_object)
+
+ return True
+
+ if key_name == 'Right':
+ if self.selected_object:
+ if event.state & MOD1_MASK:
+ # Save the previous angle in case we have to undo the rotation.
+ self.selected_object.last_angle = self.selected_object.angle
+ self.rotate_object(self.selected_object, RADIAL_GRID_SIZE)
+ else:
+ # Save the previous position in case we have to undo the move.
+ self.selected_object.last_pos = self.selected_object.pos
+ # Move the object.
+ self.move_object_rel(self.selected_object, Vector(GRID_SIZE, 0))
+
+ self.snap_object_to_grid(self.selected_object)
+
+ return True
+
+ def expose_cb(self, widget, event):
+ cr = self.bin_window.cairo_create()
+
+ cr.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
+ cr.clip()
+
+ # Draw the grid.
+ if GRID_VISIBLE:
+ cr.set_source_rgb(0, 0, 0)
+ cr.set_line_width(1.0)
+
+ # Draw the horizontal lines.
+ for i in range(DRAGGING_RECT_HEIGHT/GRID_SIZE + 1):
+ cr.move_to(0, i * GRID_SIZE)
+ cr.line_to(int(DRAGGING_RECT_WIDTH/GRID_SIZE) * GRID_SIZE, i * GRID_SIZE)
+
+ #Draw the vertical lines.
+ for i in range(DRAGGING_RECT_WIDTH/GRID_SIZE + 1):
+ cr.move_to(i * GRID_SIZE, 0)
+ cr.line_to(i * GRID_SIZE, int(DRAGGING_RECT_HEIGHT/GRID_SIZE) * GRID_SIZE)
+ cr.stroke()
+
+ # Draw the line segment targets.
+ if (self.line_segments_target_visible):
+ line_segment_pos1 = Vector(900, 150)
+ line_segment_pos2 = Vector(1000, 150)
+ line_segment_length = 300
+ cr.rectangle(line_segment_pos1.x - 30, line_segment_pos1.y - 10, 60, line_segment_length + 20)
+ cr.rectangle(line_segment_pos2.x - 30, line_segment_pos1.y - 10, 60, line_segment_length + 20)
+ cr.set_line_width(1.0)
+ cr.set_source_rgb(0, 0, 0)
+
+ cr.stroke()
+
+ # Draw the letters.
+ text = self.letter_1
+ cr.set_source_rgb(0, 0, 0)
+ cr.set_font_size(50)
+ x_bearing, y_bearing, width, height = cr.text_extents(text)[:4]
+ x_bearing = 16
+ y_bearing = -16
+ cr.move_to(self.letter1_pos.x - x_bearing, self.letter1_pos.y - y_bearing)
+ cr.show_text(text)
+ text = self.letter_2
+ cr.set_source_rgb(0, 0, 0)
+ cr.set_font_size(50)
+ x_bearing, y_bearing, width, height = cr.text_extents(text)[:4]
+ x_bearing = 16
+ y_bearing = -16
+ cr.move_to(self.letter2_pos.x - x_bearing, self.letter2_pos.y - y_bearing)
+ cr.show_text(text)
+
+ #Draw the question mark between the letters.
+ if self.question_mark_visible:
+ text = self.question_mark
+ cr.set_source_rgb(0, 0, 0)
+ cr.set_font_size(50)
+ x_bearing, y_bearing, width, height = cr.text_extents(text)[:4]
+ x_bearing = 16
+ y_bearing = -16
+ cr.move_to(self.question_mark_pos.x - x_bearing, self.question_mark_pos.y - y_bearing)
+ cr.show_text(text)
+
+ # Draw objects in reverse order, so that first in the list is drawn on top.
+ for o in reversed(self.objects):
+ cr.save()
+ o.draw(cr)
+ cr.restore()