diff --git a/doc/autobuild.py b/doc/autobuild.py index 057c150ab..3b4fcef3c 100644 --- a/doc/autobuild.py +++ b/doc/autobuild.py @@ -40,8 +40,10 @@ import kivy.modules.keybinding import kivy.modules.monitor import kivy.modules.touchring import kivy.modules.inspector +import kivy.modules.recorder import kivy.network.urlrequest import kivy.support +import kivy.input.recorder from kivy.factory import Factory # force loading of all classes from factory diff --git a/kivy/input/recorder.py b/kivy/input/recorder.py new file mode 100644 index 000000000..830175094 --- /dev/null +++ b/kivy/input/recorder.py @@ -0,0 +1,277 @@ +''' +Input recorder +============== + +.. versionadded:: 1.0.10 + +.. warning:: + + This part of Kivy is still experimental, and his API is subject to change in + a future version. + +This is a class that can record and replay some part of input events. This can +be used for test case, screen saver etc. + +Once activated, the recorder will listen to any input event, and save some +properties in a file + the delta time. Later, you can play the input file: it +will generate fake touch with saved properties, and dispatch it to the event +loop. + +By default, only the position are saved ('pos' profile and 'sx', 'sy', +attributes). Changes it only if you understand how input is working. + +Recording events +---------------- + +The best way is to use the "recorder" module. Check the :doc:`api-kivy.modules` +documentation for learning about how to activate a module. + +When activated, you can press F8 to start the recording. By default, events will +be written at `/recorder.kvi`. When you want to stop recording, +press F8 again. + +You can replay the file by pressing F7. + +Check the :doc:`api-kivy.modules.recorder` module for more information. + +Manual play +----------- + +You can manually open a recorder file, and play it by doing:: + + from kivy.input.recorder import Recorder + + rec = Recorder(filename='myrecorder.kvi') + rec.play = True + +If you want to loop over that file, you can do:: + + + from kivy.input.recorder import Recorder + + def recorder_loop(instance, value): + if value is False: + instance.play = True + + rec = Recorder(filename='myrecorder.kvi') + rec.bind(play=recorder_loop) + rec.play = True + +Recording more attributes +------------------------- + +You can extend the attributes to save, at one condition: attributes values must +be simple value, not instance of complex class. Aka, saving shape will not work. + +Let's say you want to save angle and pressure of the touch, if available:: + + from kivy.input.recorder import Recorder + + rec = Recorder(filename='myrecorder.kvi', + record_attrs=['is_touch', 'sx', 'sy', 'angle', 'pressure'], + record_profile_mask=['pos', 'angle', 'pressure']) + rec.record = True + +Or with modules variables:: + + $ python main.py -m recorder,attrs=is_touch:sx:sy:angle:pressure,profile_mask=pos:angle:pressure + +Known limitations +----------------- + + - Unable to save attributes with instance of complex class + - Values that represent time will be not adjusted + - Can replay only complete record, if a begin/update/end event is missing, + this could lead to ghost touch. + - Stopping the replay before the end can lead to ghost touch. + +''' + +__all__ = ('Recorder', ) + +from os.path import exists +from time import time +from kivy.event import EventDispatcher +from kivy.properties import ObjectProperty, BooleanProperty, StringProperty, \ + NumericProperty, ListProperty +from kivy.input.motionevent import MotionEvent +from kivy.base import EventLoop +from kivy.logger import Logger +from ast import literal_eval + + +class RecorderMotionEvent(MotionEvent): + + def depack(self, args): + for key, value in args.iteritems(): + setattr(self, key, value) + super(RecorderMotionEvent, self).depack(args) + + +class Recorder(EventDispatcher): + '''Recorder class, check module documentation for more information. + ''' + + window = ObjectProperty(None) + '''Window instance to attach the recorder. If None set, it will use the + default one. + + :data:`window` is a :class:`~kivy.properties.ObjectProperty`, default to + None. + ''' + + counter = NumericProperty(0) + '''Number of events recorded in the last session. + + :data:`counter` is a :class:`~kivy.properties.NumericProperty`, default to + 0, read-only. + ''' + + play = BooleanProperty(False) + '''Boolean to start/stop the replay of the current file (if exist.) + + :data:`play` is a :class:`~kivy.properties.BooleanProperty`, default to + False. + ''' + + record = BooleanProperty(False) + '''Boolean to start/stop the recording of input events. + + :data:`record` is a :class:`~kivy.properties.BooleanProperty`, default to + False. + ''' + + filename = StringProperty('recorder.kvi') + '''Filename to save the output of recorder. + + :data:`filename` is a :class:`~kivy.properties.StringProperty`, default to + 'recorder.kvi'. + ''' + + record_attrs = ListProperty(['is_touch', 'sx', 'sy']) + '''Attributes to record from the motion event. + + :data:`record_attrs` is a :class:`~kivy.properties.ListProperty`, default to + ['is_touch', 'sx', 'sy']. + ''' + + record_profile_mask = ListProperty(['pos']) + '''Profile to save in the fake motion event when replayed. + + :data:`record_profile_mask` is a :class:`~kivy.properties.ListProperty`, + default to ['pos']. + ''' + + # internals + record_fd = ObjectProperty(None) + record_time = NumericProperty(0.) + + def __init__(self, **kwargs): + super(Recorder, self).__init__(**kwargs) + if self.window is None: + # manually set the current window + from kivy.core.window import Window + self.window = Window + self.window.bind(on_motion=self.on_motion) + + def on_motion(self, window, etype, motionevent): + if not self.record: + return + args = {} + for arg in self.record_attrs: + if hasattr(motionevent, arg): + args[arg] = getattr(motionevent, arg) + args['profile'] = [x for x in motionevent.profile if x in + self.record_profile_mask] + self.record_fd.write('%r\n' % ( + (time() - self.record_time, etype, motionevent.uid, args), )) + self.counter += 1 + + def release(self): + self.window.unbind(on_motion=self.on_motion) + + def on_record(self, instance, value): + if value: + # generate a record filename + self.counter = 0 + self.record_time = time() + self.record_fd = open(self.filename, 'w') + self.record_fd.write('#RECORDER1.0\n') + Logger.info('Recorder: Recording inputs to %r' % self.filename) + else: + self.record_fd.close() + Logger.info('Recorder: Recorded %d events in %r' % (self.counter, + self.filename)) + + # needed for acting as an input provider + def stop(self): + pass + def start(self): + pass + + def on_play(self, instance, value): + if not value: + Logger.info('Recorder: Stop playing %r' % self.filename) + EventLoop.remove_input_provider(self) + return + if not exists(self.filename): + Logger.error('Recorder: Unable to found %r file, play aborted.' % ( + self.filename)) + return + + with open(self.filename, 'r') as fd: + data = fd.read().splitlines() + + if len(data) < 2: + Logger.error('Recorder: Unable to play %r, file truncated.' % ( + self.filename)) + return + + if data[0] != '#RECORDER1.0': + Logger.error('Recorder: Unable to play %r, invalid header.' % ( + self.filename)) + return + + # decompile data + self.play_data = [literal_eval(x) for x in data[1:]] + self.play_time = time() + self.play_me = {} + Logger.info('Recorder: Start playing %d events from %r' % + (len(self.play_data), self.filename)) + EventLoop.add_input_provider(self) + + def update(self, dispatch_fn): + if len(self.play_data) == 0: + Logger.info('Recorder: Playing finished.') + self.play = False + + dt = time() - self.play_time + while len(self.play_data): + event = self.play_data[0] + assert(len(event) == 4) + if event[0] > dt: + return + + etype, uid, args = event[1:] + if etype == 'begin': + me = RecorderMotionEvent('recorder', uid, args) + self.play_me[uid] = me + elif etype == 'update': + me = self.play_me[uid] + me.depack(args) + elif etype == 'end': + me = self.play_me.pop(uid) + me.depack(args) + + dispatch_fn(etype, me) + + self.play_data.pop(0) + + +def start(win, ctx): + ctx.recorder = Recorder(window=win) + + +def stop(win, ctx): + if hasattr(ctx, 'recorder'): + ctx.recorder.release() diff --git a/kivy/modules/recorder.py b/kivy/modules/recorder.py new file mode 100644 index 000000000..4dff9227d --- /dev/null +++ b/kivy/modules/recorder.py @@ -0,0 +1,77 @@ +''' +Recorder module +=============== + +.. versionadded:: 1.0.10 + +Create an instance of :class:`~kivy.input.recorder.Recorder`, attach to the +class, and bind some keys to record / play sequences: + + - F7: read the latest recording + - F8: record input events + +Configuration +------------- + +:Parameters: + `attrs`: str, default to :data:`~kivy.input.recorder.Recorder.record_attrs` + value. + + Attributes to record from the motion event + + `profile_mask`: str, default to + :data:`~kivy.input.recorder.Recorder.record_profile_mask` value. + + Mask for motion event profile. Used to filter which profile will appear + in the fake motion event when replayed. + + `filename`: str, default to 'recorder.kvi' + + Name of the file to record / play with + +''' + +from kivy.input.recorder import Recorder +from kivy.logger import Logger +from functools import partial + + +def on_recorder_key(recorder, window, key, *largs): + if key == 289: # F8 + if recorder.play: + Logger.error('Recorder: Cannot start recording while playing.') + return + recorder.record = not recorder.record + elif key == 288: # F7 + if recorder.record: + Logger.error('Recorder: Cannot start playing while recording.') + return + recorder.play = not recorder.play + + +def start(win, ctx): + keys = {} + + # attributes + value = ctx.config.get('attrs', None) + if value is not None: + keys['record_attrs'] = value.split(':') + + # profile mask + value = ctx.config.get('profile_mask', None) + if value is not None: + keys['record_profile_mask'] = value.split(':') + + # filename + value = ctx.config.get('filename', None) + if value is not None: + keys['filename'] = value + + ctx.recorder = Recorder(window=win, **keys) + win.bind(on_key_down=partial(on_recorder_key, ctx.recorder)) + + +def stop(win, ctx): + if hasattr(ctx, 'recorder'): + ctx.recorder.release() +