# This file is part of MyPaint. # Copyright (C) 2009 by Martin Renold # # This program 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 2 of the License, or # (at your option) any later version. import gtk, gobject gdk = gtk.gdk class StateGroup(): """ Supervisor instance for GUI states. A GUI state is a mode which the GUI is in, for example an active popup window or a special (usually short-lived) view on the document. The application defines functions to be called when the state is entered or left. This class mainly deals with the various ways how the user can leave such a mode, eg. if the mode is entered by holding down a key long enough, it will be left when the key is released. """ def __init__(self): self.states = [] self.keys_pressed = {} def get_active_states(self): return [s for s in self.states if s.active] active_states = property(get_active_states) def create_state(self, enter, leave, popup=None): s = State(self, popup) s.popup = None # FIXME: who uses this? hack? s.on_enter = enter s.on_leave = leave self.states.append(s) return s def create_popup_state(self, popup): return self.create_state(popup.enter, popup.leave, popup) class State: "how long a key can be held down to go through as single hit (and not press-and-hold)" max_key_hit_duration = 0.250 "the state is automatically left after this time (ignored during press-and-hold)" autoleave_timeout = 0.800 #"popups only: how long the cursor is allowed outside before closing (ignored during press-and-hold)" #outside_popup_timeout = 0.050 "state to activate when this state is activated while already active (None = just leave this state)" next_state = None def __init__(self, stategroup, popup): self.sg = stategroup self.active = False self.popup = popup self.autoleave_timer = None self.outside_popup_timer = None if popup: popup.connect("enter-notify-event", self.popup_enter_notify_cb) popup.connect("leave-notify-event", self.popup_leave_notify_cb) popup.popup_state = self # FIXME: hacky? self.outside_popup_timeout = popup.outside_popup_timeout def enter(self): #print 'entering state, calling', self.on_enter.__name__ assert not self.active self.active = True self.enter_time = gtk.get_current_event_time()/1000.0 self.connected_motion_handler = None if self.autoleave_timeout: self.autoleave_timer = gobject.timeout_add(int(1000*self.autoleave_timeout), self.autoleave_timeout_cb) self.on_enter() def leave(self, reason=None): #print 'leaving state, calling', self.on_leave.__name__ assert self.active self.active = False if self.autoleave_timer: gobject.source_remove(self.autoleave_timer) self.autoleave_timer = None if self.outside_popup_timer: gobject.source_remove(self.outside_popup_timer) self.outside_popup_timer = None self.disconnect_motion_handler() self.on_leave(reason) def activate(self, action_or_event=None): """ Called from the GUI code, eg. when a gtk.Action is activated. The action is used to figure out the key. """ if self.active: # pressing the key again if self.next_state: self.leave() self.next_state.activate(action_or_event) return # first leave other active states from the same stategroup for state in self.sg.active_states: state.leave() self.keydown = False self.mouse_button = None if action_or_event: if isinstance(action_or_event, gdk.Event): e = action_or_event # currently, we only support mouse buttons being pressed here assert e.type == gdk.BUTTON_PRESS # let's just note down what mous button that was assert e.button if e.button in [1]: self.mouse_button = 1 else: a = action_or_event # register for key release events, see keyboard.py if a.keydown: a.keyup_callback = self.keyup_cb self.keydown = True self.activated_by_keyboard = self.keydown # FIXME: should probably be renamed (mouse button possible) self.enter() def toggle(self, action=None): if not self.active: self.activate(action) else: self.leave() def keyup_cb(self, widget, event): if not self.active: return self.keydown = False if event.time/1000.0 - self.enter_time < self.max_key_hit_duration: pass # accept as one-time hit else: if self.outside_popup_timer: self.leave('outside') else: self.leave('keyup') def autoleave_timeout_cb(self): if not self.keydown: self.leave('timeout') def outside_popup_timeout_cb(self): if not self.keydown: self.leave('outside') def popup_enter_notify_cb(self, widget, event): if not self.active: return if self.outside_popup_timer: gobject.source_remove(self.outside_popup_timer) self.outside_popup_timer = None def popup_leave_notify_cb(self, widget, event): if not self.active: return # allow to leave the window for a short time if self.outside_popup_timer: gobject.source_remove(self.outside_popup_timer) self.outside_popup_timer = gobject.timeout_add(int(1000*self.outside_popup_timeout), self.outside_popup_timeout_cb) def motion_notify_cb(self, widget, event): assert self.keydown pressure = event.get_axis(gdk.AXIS_PRESSURE) painting = (event.state & gdk.BUTTON1_MASK) or pressure if not painting: # we can leave the state without putting an accidental dab on the canvas self.disconnect_motion_handler() self.keyup_cb(widget, event) def disconnect_motion_handler(self): if not self.connected_motion_handler: return widget, handler_id = self.connected_motion_handler widget.disconnect(handler_id) self.connected_motion_handler = None def register_mouse_grab(self, widget): assert self.active # fix for https://gna.org/bugs/?14871 (problem with tablet and pointer grabs) widget.add_events(gdk.POINTER_MOTION_MASK # proximity mask might help with scrollwheel events (https://gna.org/bugs/index.php?16253) | gdk.PROXIMITY_OUT_MASK | gdk.PROXIMITY_IN_MASK ) widget.set_extension_events (gdk.EXTENSION_EVENTS_ALL) if self.keydown: # we are reacting to a keyboard event, we will not be # waiting for a mouse button release assert not self.mouse_button return if self.mouse_button: # we are able to wait for a button release now self.keydown = True # register for events assert self.mouse_button == 1 handler_id = widget.connect("motion-notify-event", self.motion_notify_cb) assert not self.connected_motion_handler self.connected_motion_handler = (widget, handler_id) else: # The user is neither pressing a key, nor holding down a button. # This happens when activating the color picker from the menu. # # Stop everything, release the pointer grab. # (TODO: wait for a click instead, or show an instruction dialog) print 'COV' self.leave(None)