#
# Kiwi: a Framework and Enhanced Widgets for Python
#
# Copyright (C) 2005,2006 Async Open Source
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
#
# Author(s): Johan Dahlin <jdahlin@async.com.br
#
"""
User interface event recorder and serializer.
This module provides an interface for creating, listening to
and saving events.
It uses the gobject introspection base class
:class:`kiwi.ui.test.common.WidgetIntrospecter` to gather widgets, windows and
other objects.
The user interfaces are saved in a format so they can easily be played
back by simply executing the script through a standard python interpreter.
"""
import atexit
import sys
import time
from gtk import gdk
import gtk
from kiwi.log import Logger
from kiwi.ui.test.common import WidgetIntrospecter
from kiwi.ui.objectlist import ObjectList
try:
from gobject import add_emission_hook
add_emission_hook # pyflakes
except ImportError:
try:
from kiwi._kiwi import add_emission_hook
add_emission_hook # pyflakes
except ImportError:
add_emission_hook = None
_events = []
log = Logger('recorder')
[docs]def register_event_type(event_type):
"""
Add an event type to a list of event types.
:param event_type: a :class:`Event` subclass
"""
if event_type in _events:
raise AssertionError("event %s already registered" % event_type)
_events.append(event_type)
[docs]def get_event_types():
"""
Returns the collection of event types.
:returns: the event types.
"""
return _events
[docs]class SkipEvent(Exception):
pass
[docs]class Event(object):
"""
Event is a base class for all events.
An event represent a user change of an interactive widget.
:cvar object_type: subclass for type, :class:`Recorder` uses this to
automatically attach events to objects when they appear
"""
object_type = None
def __init__(self, object, name=None):
"""
Create a new Event object.
:param object: a gobject subclass
:param name: name of the object, if None, the
method get_name() will be called
"""
self.object = object
if name is None:
name = object.get_name()
self.name = name
self.toplevel_name = self.get_toplevel(object).get_name()
# Override in subclass
[docs] def get_toplevel(self, widget):
"""
This fetches the toplevel widget for a specific object,
by default it assumes it's a wiget subclass and calls
get_toplevel() for the widget
Override this in a subclass.
"""
return widget.get_toplevel()
[docs] def serialize(self):
"""
Serialize the widget, write the code here which is
used to reproduce the event, for a button which is clicked
the implementation looks like this:
>>> def serialize(self):
>>> ... return '%s.clicked' % self.name
:returns: string to reproduce event
Override this in a subclass.
"""
pass
[docs]class SignalEvent(Event):
"""
A SignalEvent is an :class:`Event` which is tied to a GObject signal,
:class:`Recorder` uses this to automatically attach itself to a signal
at which point this object will be instantiated.
:cvar signal_name: signal to listen to
"""
signal_name = None
def __init__(self, object, name, args):
"""
Create a new SignalEvent object.
:param object:
:param name:
:param args:
"""
Event.__init__(self, object, name)
self.args = args
[docs] def connect(cls, object, signal_name, cb):
"""
Calls connect on I{object} for signal I{signal_name}.
:param object: object to connect on
:param signal_name: signal name to listen to
:param cb: callback
"""
object.connect(signal_name, cb, cls, object)
connect = classmethod(connect)
#
# Special Events
#
[docs]class WindowDeleteEvent(Event):
"""
This event represents a user click on the close button in the
window manager.
"""
#
# Signal Events
#
register_event_type(MenuItemActivateEvent)
register_event_type(ImageMenuItemButtonReleaseEvent)
register_event_type(ToolButtonReleaseEvent)
[docs]class EntrySetTextEvent(SignalEvent):
"""
This event represents a content modification of a GtkEntry.
When the user deletes, clears, adds, modifies the text this
event will be created.
"""
signal_name = 'notify::text'
object_type = gtk.Entry
def __init__(self, object, name, args):
SignalEvent.__init__(self, object, name, args)
self.text = self.object.get_text()
[docs] def serialize(self):
return '%s.set_text("%s")' % (self.name, self.text)
register_event_type(EntrySetTextEvent)
[docs]class EntryActivateEvent(SignalEvent):
"""
This event represents an activate event for a GtkEntry, eg when
the user presses enter in a GtkEntry.
"""
signal_name = 'activate'
object_type = gtk.Entry
[docs] def serialize(self):
return '%s.activate()' % (self.name)
register_event_type(EntryActivateEvent)
# Also works for Toggle, Radio and Check
register_event_type(ButtonClickedEvent)
# Kiwi widget support
[docs]class ObjectListSelectionChanged(SignalEvent):
"""
This event represents a selection change on a
:class:`kiwi.ui.objectlist.ObjectList`,
eg when the user selects or unselects a row.
It is actually tied to the signal changed on GtkTreeSelection object.
"""
object_type = ObjectList
signal_name = 'changed'
def __init__(self, objectlist, name, args):
self._objectlist = objectlist
SignalEvent.__init__(self, objectlist, name=objectlist.get_name(),
args=args)
self.rows = self._get_rows()
def _get_rows(self):
selection = self._objectlist.get_treeview().get_selection()
if selection.get_mode() == gtk.SELECTION_MULTIPLE:
# get_selected_rows() returns a list of paths
iters = selection.get_selected_rows()[1]
if iters:
return iters
else:
# while get_selected returns an iter, yay.
model, iter = selection.get_selected()
if iter is not None:
# so convert it to a path and put it in an empty list
return [model.get_string_from_iter(iter)]
return []
[docs] def connect(cls, orig, signal_name, cb):
object = orig.get_treeview().get_selection()
object.connect(signal_name, cb, cls, orig)
connect = classmethod(connect)
[docs] def get_toplevel(self, widget):
return self._objectlist.get_toplevel()
[docs] def serialize(self):
return '%s.select_paths(%s)' % (self.name, self.rows)
register_event_type(ObjectListSelectionChanged)
[docs]class ObjectListDoubleClick(SignalEvent):
"""
This event represents a double click on a row in objectlist
"""
signal_name = 'button-press-event'
object_type = ObjectList
def __init__(self, objectlist, name, args):
event, = args
if event.type != gdk._2BUTTON_PRESS:
raise SkipEvent
SignalEvent.__init__(self, objectlist, name, args)
self.row = objectlist.get_selected_row_number()
[docs] def connect(cls, orig, signal_name, cb):
object = orig.get_treeview()
object.connect(signal_name, cb, cls, orig)
connect = classmethod(connect)
[docs] def serialize(self):
return '%s.double_click(%s)' % (self.name, self.row)
register_event_type(ObjectListDoubleClick)
# XXX: ComboMixin -> ???
# class KiwiComboBoxChangedEvent(SignalEvent):
# """
# This event represents a a selection of an item
# in a :class:`kiwi.ui.widgets.combobox.ComboBoxEntry` or
# :class:`kiwi.ui.widgets.combobox.ComboBox`.
# """
# signal_name = 'changed'
# object_type = ComboMixin
# def __init__(self, combo, name, args):
# SignalEvent.__init__(self, combo, name, args)
# self.label = combo.get_selected_label()
# def serialize(self):
# return '%s.select_item_by_label("%s")' % (self.name, self.label)
# register_event_type(KiwiComboBoxChangedEvent)
[docs]class Recorder(WidgetIntrospecter):
"""
Recorder takes care of attaching events to widgets, when the appear,
and creates the events when the user is interacting with some widgets.
When the tracked program is closed the events are serialized into
a script which can be played back with help of
:class:`kiwi.ui.test.player.Player`.
"""
def __init__(self, filename):
"""
Create a new Recorder object.
:param filename: name of the script
"""
WidgetIntrospecter.__init__(self)
self.register_event_handler()
self.connect('window-removed', self.window_removed)
self._filename = filename
self._events = []
self._listened_objects = []
self._event_types = self._configure_event_types()
self._args = None
# This is sort of a hack, but there are no other realiable ways
# of actually having something executed after the application
# is finished
atexit.register(self.save)
# Register a hook that is called before normal delete-events
# because if it's connected using a normal callback it will not
# be called if the application returns True in it's signal handler.
if add_emission_hook:
add_emission_hook(gtk.Window, 'delete-event',
self._emission_window__delete_event)
[docs] def execute(self, args):
self._start_timestamp = time.time()
self._args = args
# Run the script
sys.argv = args
execfile(sys.argv[0], globals(), globals())
def _emission_window__delete_event(self, window, event, *args):
self._add_event(WindowDeleteEvent(window))
# Yes, please call us again
return True
def _configure_event_types(self):
event_types = {}
for event_type in get_event_types():
if event_type.object_type is None:
raise AssertionError
elist = event_types.setdefault(event_type.object_type, [])
elist.append(event_type)
return event_types
def _add_event(self, event):
log("Added event %s" % event.serialize())
self._events.append((event, time.time()))
def _listen_event(self, object, event_type):
if not issubclass(event_type, SignalEvent):
raise TypeError("Can only listen to SignalEvents, not %r"
% event_type)
if event_type.signal_name is None:
raise ValueError("signal_name cannot be None")
# This is horrible, but there's no good way of passing in
# more than one variable to the script and we really want to be
# able to connect it to any kind of signal, regardless of
# the number of parameters the signal has
def on_signal(object, *args):
event_type, orig = args[-2:]
try:
self._add_event(event_type(orig, None, args[:-2]))
except SkipEvent:
pass
event_type.connect(object, event_type.signal_name, on_signal)
[docs] def window_removed(self, wi, window, name):
# It'll already be trapped if we can use an emission hook
# skip it here to avoid duplicates
if not add_emission_hook:
return
self._add_event(WindowDeleteEvent(window))
[docs] def parse_one(self, toplevel, gobj):
WidgetIntrospecter.parse_one(self, toplevel, gobj)
# mark the object as "listened" to ensure we'll always
# receive unique objects
if gobj in self._listened_objects:
return
self._listened_objects.append(gobj)
for object_type, event_types in self._event_types.items():
if not isinstance(gobj, object_type):
continue
for event_type in event_types:
# These 3 hacks should move into the event class itself
if event_type == MenuItemActivateEvent:
if not isinstance(gobj.get_parent(), gtk.MenuBar):
continue
elif event_type == ToolButtonReleaseEvent:
if not isinstance(gobj.get_parent(), gtk.ToolButton):
continue
elif event_type == ButtonClickedEvent:
if isinstance(gobj.get_parent(), gtk.ToolButton):
continue
if issubclass(event_type, SignalEvent):
self._listen_event(gobj, event_type)
[docs] def save(self):
"""
Collect events and serialize them into a script and save
the script.
This should be called when the tracked program has
finished executing.
"""
if not self._events:
return
try:
fd = open(self._filename, 'w')
except IOError:
raise SystemExit("Could not write: %s" % self._filename)
fd.write("... -*- Mode: doctest -*-\n")
fd.write("... run: %s\n" % ' '.join(self._args))
fd.write(">>> from kiwi.ui.test.runner import runner\n")
fd.write(">>> runner.start()\n")
windows = {}
last = self._events[0][1]
fd.write('>>> runner.sleep(%2.1f)\n' % (last - self._start_timestamp,))
for event, timestamp in self._events:
toplevel = event.toplevel_name
if not toplevel in windows:
fd.write('>>> %s = runner.waitopen("%s")\n' % (toplevel,
toplevel))
windows[toplevel] = True
if isinstance(event, WindowDeleteEvent):
fd.write(">>> %s.delete()\n" % (event.name,))
fd.write(">>> runner.waitclose('%s')\n" % (event.name,))
if not event.name in windows:
# Actually a bug
continue
del windows[event.name]
else:
fd.write(">>> %s.%s\n" % (toplevel, event.serialize()))
delta = timestamp - last
if delta > 0.05:
fd.write('>>> runner.sleep(%2.1f)\n' % (delta,))
last = timestamp
fd.write('>>> runner.quit()\n')
fd.close()