News

( My personal website has gone to dust, any links thereto will fail. This site will be home for a while. )

Wednesday 17 February 2010

Clutter Mouse Events

Well, it has been a fairly good week in-between the segfaults and other scary things. I have a basic system to tame mouse events so that I can respond to 'click' and 'double-click' and stuff like 'dragging' and 'drag-over'.
This is all written in Python. The licence is GPLv3.
Save them both (states.py in a directory called helpers), then run two_squares.py -- you should be able to drag them around (with different mouse buttons. They dump info to the console.)
Module 1: helpers.states.py
import clutter
import gobject

class DragEnvelope(object):
 """Helps deal with drags."""
 def __init__(self, obj, event):
  self.event = event
  self.diffx = event.x - obj.get_x()
  self.diffy = event.y - obj.get_y()
 def calcPos(self, event):
  """Where to draw the Actor relative to cursor within it."""
  self.newx = event.x - self.diffx
  self.newy = event.y - self.diffy

class Mouserizer(object):
 ## User vars to form ui mask
 HITABLE = 1<<0
 DRAGABLE = 1<<1
 CLICKABLE = 1<<2
 SCROLLABLE = 1<<3
 PICK_UNDER = 1<<4

 LEFT = 1<<0
 MIDDLE = 1<<1
 RIGHT = 1<<2

 ## Private class vars
 __FLAG_ENTERED = 1<<0 # Not used in this version.
 __FLAG_PRESSING = 1<<1
 __FLAG_RELEASING = 1<<2
 __FLAG_MOVING = 1<<3
 __FLAG_DRAGGING = 1<<4

 __PATTERN_CLICK =  __FLAG_PRESSING | __FLAG_RELEASING
 __PATTERN_DRAG_START = __FLAG_PRESSING| __FLAG_MOVING
 __PATTERN_DRAG = __PATTERN_DRAG_START | __FLAG_DRAGGING
 __PATTERN_DROP =  __FLAG_PRESSING| __FLAG_DRAGGING | __FLAG_RELEASING

 YES_CONTINUE_EMITTING = False
 NO_STOP_EMITTING = True
 
 __clutter_mouse_event_types=[
   clutter.MOTION,
   clutter.ENTER,
   clutter.LEAVE,
   clutter.BUTTON_PRESS,
   clutter.BUTTON_RELEASE,
   clutter.SCROLL
   ]

 def __init__(self, ui=None, buttons=None):
  if ui is None: 
   return # Not going to watch for any mouse events, so bug out.
  self.buttons = buttons
  
  ## If we want HIT kind of events, then just connect the usual suspects.
  if (ui & Mouserizer.HITABLE) !=0: # test bit
   self.connect('enter-event', self.on_enter_event)
   self.connect('leave-event', self.on_leave_event)

  self.__PICK_UNDER = False
  if (ui & Mouserizer.PICK_UNDER) !=0:
   self.__PICK_UNDER = True

  ## Keep a record of what we are going to listen to.
  self.ui = ui

  ## Enable the actor (self) to receive events.
  self.set_reactive(True)

  ## This is the state of our situation -- it will be masked bitwise.
  self.ui_state = 0
  
  ## Route all events (for this Actor) through one function:
  self.connect('captured-event', self.event_central)

 def event_central(self, obj, event):
  ## This routine runs many times. Once for every kind
  ## of event the actor is getting.

  ## filter out only the mouse events.
  if event.type not in Mouserizer.__clutter_mouse_event_types: 
    return Mouserizer.YES_CONTINUE_EMITTING

  ## filter out buttons we are NOT going to deal with
  if hasattr(event, "button"):
   b = 1 << (event.button - 1)
   #print bin(b)," vs ", bin(self.buttons)
   if not(b & self.buttons !=0 ):
    return Mouserizer.NO_STOP_EMITTING # is this wise?

  ## event_central ONLY runs when cursor is
  ## over the actor -- thus ENTER is implied.
  
  ## Make a note of PRESS/RELEASE
  if event.type==clutter.BUTTON_PRESS:
   self.ui_state = self.ui_state | Mouserizer.__FLAG_PRESSING # set bit
  if event.type==clutter.BUTTON_RELEASE:
   self.ui_state = self.ui_state | Mouserizer.__FLAG_RELEASING # set bit

  ## Make a note of MOTION
  ## First, clear it.
  self.ui_state = self.ui_state & ~Mouserizer.__FLAG_MOVING # clear bit
  if event.type==clutter.MOTION:
   self.ui_state = self.ui_state | Mouserizer.__FLAG_MOVING # set bit

  ## Now, what kinds of stuff is this actor interested in?

  ## DO META EVENTS - "More than" events. e.g. 'Click' is press, then release.
  if (self.ui & Mouserizer.CLICKABLE) != 0: # test bit
   if self.ui_state == Mouserizer.__PATTERN_CLICK:
    if event.click_count > 1:
     self.emit('double-click', event)
    else:
     ## A single click is fired just before double-click...!
     self.emit('single-click', event)

  if (self.ui & Mouserizer.DRAGABLE) !=0: # test bit
   if self.ui_state == Mouserizer.__PATTERN_DRAG_START:
    self.ui_state=self.ui_state | Mouserizer.__FLAG_DRAGGING # set bit
    self.draglet = DragEnvelope( self, event )
    ## Phew! I thought I was fcuked! In order to get dragging to 
    ## work when the pointer is NOT ON the Actor, I had to revert
    ## to grab_pointer* -- and that needs connecting. I connected the
    ## two appropriate event to *this* same function! And it works :D
    ##
    ## * grab_pointer causes the entire window (stage?) to focus on the
    ##   Actor passed -- so I get all motion and release events even where
    ##   the Actor aint.
    ##   ! Not sure what kind of recursive issues this may throw at me :(
    clutter.grab_pointer( self )
    self.connect('motion-event', self.event_central)
    self.connect('button-release-event', self.event_central)
    self.emit('drag-start', self.draglet )
   elif self.ui_state == Mouserizer.__PATTERN_DRAG:
    self.draglet.calcPos( event ) # A 'draglet' is a little wrapper containing the event and some tricks.
    ## Who is under me? Only do if PICK_UNDER flag is set.
    if self.__PICK_UNDER:
     self.hide()
     a = self.stage.get_actor_at_pos(clutter.PICK_REACTIVE, int(event.x),int(event.y))
     self.show()    
     ## a is!
     ## Only emit if a has a drag-over signal:
     if gobject.signal_lookup('drag-over', a ):
      print a, " under me"
      a.emit('drag-over', self.draglet)
    self.emit('dragging', self.draglet)
    
   elif self.ui_state == Mouserizer.__PATTERN_DROP:
    self.draglet.calcPos( event )
    self.ui_state= self.ui_state & ~Mouserizer.__FLAG_DRAGGING # clear bit
    clutter.ungrab_pointer()
    self.emit("drop", self.draglet)
    del(self.draglet)
   
  ## META EVENTS are done.

  ## Flip opposites off.
  if event.type==clutter.BUTTON_PRESS:
   self.ui_state = self.ui_state & ~Mouserizer.__FLAG_RELEASING # clear bit
  if event.type==clutter.BUTTON_RELEASE:
   self.ui_state = self.ui_state & ~Mouserizer.__FLAG_PRESSING # clear bit
   self.ui_state = self.ui_state & ~Mouserizer.__FLAG_RELEASING # clear bit
   self.ui_state = self.ui_state & ~Mouserizer.__FLAG_DRAGGING # clear bit

  return Mouserizer.YES_CONTINUE_EMITTING

(Always with the rectangles eh? :D)
import clutter
from helpers.states import Mouserizer as M
import gobject

meta_signals = {
 'single-click' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,) ),
 'double-click' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,) ),
 'drag-start' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,) ),
 'dragging' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,) ),
 'drop' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,) ),
 'drag-over' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,) ),
}

class DragHandle(clutter.Rectangle, M):
 ## Make this class a GObject type
 __gtype_name__ = 'DragHandle'
 ## Specify which signals it can receieve.
 __gsignals__ =  meta_signals

 def __init__(self, stage, col, ui=None, buttons=None):
  """
  You don't have to connect: enter-event, leave-event, single-click or double-click
  """
  if ui is None:
   ui = M.HITABLE
  if buttons is None:
   buttons = M.LEFT
  clutter.Actor.__init__(self)
  M.__init__(self, ui=ui, buttons=buttons)
  self.stage = stage
  
  self.set_color( clutter.color_from_string(col))
  self.set_size(50,50)

 def do_drag_over(self, draglet):
  print "DRAG OVER ", draglet
  return M.NO_STOP_EMITTING 

 def do_single_click(self, *args):
  print "HANDLER CLICK"
  return M.NO_STOP_EMITTING

 def do_double_click(self, *args):
  print "HANDLER DCLICK"
  clutter.main_quit()

 def do_drag_start(self, draglet):
  print "DRAG STARTS:"
  return True

 def do_dragging(self, draglet):
  self.set_position(draglet.newx, draglet.newy)
  return True

 def do_drop(self, *args):
  print "DROP!"
  return True

 def on_enter_event(self, obj, evt):
  print "ENTERING ",obj
  return True

 def on_leave_event(self,obj,evt):
  print "LEAVING ", obj
  return True
 
if __name__ == '__main__':
 stage = clutter.Stage()
 stage.set_size(640, 480)
 stage.set_color(clutter.color_from_string('Black'))

 stage.connect('destroy', clutter.main_quit)

 test=DragHandle( stage, "Green",  ui=M.HITABLE | M.CLICKABLE | M.DRAGABLE, buttons= M.RIGHT )
 stage.add(test)
 test.set_position(100,100)
 test.show()

 test2=DragHandle(stage, "Red",  ui=M.HITABLE | M.DRAGABLE | M.PICK_UNDER , buttons= M.LEFT )
 stage.add(test2)
 test2.show()
 
 stage.show()
 clutter.main()

Well, I hope that helps someone.
\d

Tuesday 9 February 2010

PyClutter recipes.

Here are a few scripts using pyClutter 1.0 that I have recently finished. They are only for demonstrating some of the abilities of Clutter -- which I must say is just awesome!

Clipping actors (and groups of groups of... etc.) into a path shape
So, you have a shape and you want to force all the children in that Group (groups are Actors) to show within that path? Here you go:
import clutter
from clutter import cogl

"""
pyClutter 1.0
Demonstration of how to clip groups to a path.
"""

zoom = 1
def zoomify(obj,evt):
global zoom
if evt.direction == clutter.SCROLL_UP:
zoom += .1
else:
zoom -= .1
# When the zoom gets small, the cogl clip goes all wonky...
obj.set_scale(zoom, zoom)

class ClipGroup(clutter.Group):
""" Custom Group to perform clipping from a path."""
# Vital line.
# Registers the specified Python class as a PyGTK type.
# Also enables the do_paint method. I am not sure why...
__gtype_name__ = 'ClipGroup'

def __init__(self,w,h):
clutter.Group.__init__(self)#,*args)
self.width, self.height = w, h

def do_paint(self):
# Draw a triangle.
cogl.path_move_to(self.width / 2, 0)
cogl.path_line_to(self.width, self.height)
cogl.path_line_to(0, self.height)
cogl.path_line_to(self.width / 2, 0)
cogl.path_close()
# Start the clip
cogl.clip_push_from_path()

# errr.. the idea is to have the other stuff in the group
# get painted -- I assume Group knows what to do.
clutter.Group.do_paint(self)

# Finish the clip
cogl.clip_pop()

def main():
stage = clutter.Stage()
stage.set_size(500, 500)
stage.set_color(clutter.color_from_string("#000"))

rect_red, rect_green, rect_blue, rect_bounce = \
clutter.Rectangle(), clutter.Rectangle(), clutter.Rectangle(), clutter.Rectangle()

# We make two of our special groups.
clipped_group = ClipGroup(300,300)
inner_group = ClipGroup(100,100)

rect_red.set_position(0, 0)
rect_red.set_size(550, 550) #Big to be backdrop.
rect_red.set_color(clutter.color_from_string("#FF0000FF"))

rect_green.set_position(120, 10)
rect_green.set_size(50, 50)
rect_green.set_color(clutter.color_from_string("#00FF0090"))

rect_bounce.set_position(0,0)
rect_bounce.set_size(500,20)
rect_bounce.set_color(clutter.color_from_string("#AABBCCFF"))

clipped_group.add(rect_red, rect_green)
clipped_group.add(inner_group) # Add entire inner_group into clipped_group

rect_blue.set_color(clutter.color_from_string("#0000FF90"))
rect_blue.set_size(50, 50)
rect_blue.set_position(50,50) #relative to inner_group...
inner_group.add(rect_blue) # even though this comes after set_position...

clipped_group.set_position(100, 100)
inner_group.set_position(100,50)

stage.add(rect_bounce)
stage.add(clipped_group)

# Make rect_blue move around somewhat.
path = clutter.Path('M 0 0 L 40 0 L 40 40 L 0 40 Z')
timeline = clutter.Timeline(4000)
timeline.set_loop(True)
alpha = clutter.Alpha(timeline,clutter.EASE_OUT_SINE)
p_behaviour = clutter.BehaviourPath(alpha, path)
p_behaviour.apply(rect_blue)
timeline.start()

# Make rect_bounce go up and down
animation = rect_bounce.animate(clutter.EASE_IN_OUT_BOUNCE, 2000, "y", 500)
animation.set_loop(True)

# Start the main clipped_group rotating around the Y axis. For giggles.
timeline = clutter.Timeline(15000)
timeline.set_loop(True)
alpha = clutter.Alpha(timeline, clutter.LINEAR)
r_behave = clutter.BehaviourRotate(clutter.Y_AXIS, 0.0, 360.0, alpha=alpha)
r_behave.set_center(150, 0, 0)
r_behave.apply(clipped_group)
timeline.start()

stage.show_all()

stage.connect('key-press-event', clutter.main_quit)
stage.connect('destroy', clutter.main_quit)
stage.connect('scroll-event', zoomify)

clutter.main()

if __name__ == '__main__':
main()
Adventures in Time(line)
I wanted to see for myself how Timelines could be controlled from a Score. This is very cool. Here, three rectangles fire in sequence given markers in their timelines. It sounds complicated, but the code should be easy to follow:
import clutter
from clutter import cogl

def main():
stage = clutter.Stage()
stage.set_size(500, 500)
stage.set_color(clutter.color_from_string("#000"))

rect_red, rect_green, rect_blue = \
clutter.Rectangle(), clutter.Rectangle(), clutter.Rectangle()

rect_red.set_position(30, 30)
rect_red.set_size(20, 20)
rect_red.set_color(clutter.color_from_string("#FF0000FF"))

rect_green.set_position(100, 100)
rect_green.set_size(50, 50)
rect_green.set_color(clutter.color_from_string("#00FF0090"))

rect_blue.set_color(clutter.color_from_string("#0000FF90"))
rect_blue.set_size(80, 80)
rect_blue.set_position(200,200)

stage.add(rect_red, rect_green, rect_blue)

t_red=clutter.Timeline(10000)
t_green=clutter.Timeline(5000)
t_blue=clutter.Timeline(5000)

a_blue=clutter.Alpha(t_blue,clutter.EASE_OUT_SINE)
a_green=clutter.Alpha(t_green,clutter.EASE_OUT_SINE)
a_red=clutter.Alpha(t_red,clutter.EASE_OUT_SINE)

b_red = clutter.BehaviourRotate(clutter.Z_AXIS, 0.0, 360.0, alpha=a_red)
b_green = clutter.BehaviourRotate(clutter.Z_AXIS, 0.0, 360.0, alpha=a_green)
b_blue = clutter.BehaviourRotate(clutter.Z_AXIS, 0.0, 360.0, alpha=a_blue)

b_blue.apply(rect_blue)
b_green.apply(rect_green)
b_red.apply(rect_red)

s=clutter.Score()

t_red.set_loop(True)
s.append(t_red)
t_red.add_marker_at_time("go_green",2000)
s.append_at_marker(t_red,"go_green",t_green)
t_green.add_marker_at_time("go_blue",5000)
s.append_at_marker(t_green,"go_blue", t_blue)

stage.show_all()
s.start()

stage.connect('key-press-event', clutter.main_quit)
stage.connect('destroy', clutter.main_quit)

clutter.main()

if __name__ == '__main__':
main()