1# This file is part of MyPaint. 2# Copyright (C) 2009-2013 by Martin Renold <martinxyz@gmx.ch> 3# Copyright (C) 2011-2015 by the MyPaint Development Team 4# 5# This program is free software; you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation; either version 2 of the License, or 8# (at your option) any later version. 9 10from __future__ import division, print_function 11 12import logging 13 14from lib.gibindings import Gtk 15from lib.gibindings import Gdk 16from lib.gibindings import GLib 17 18 19logger = logging.getLogger(__name__) 20 21 22class StateGroup (object): 23 """Supervisor instance for GUI states. 24 25 This class mainly deals with the various ways how the user can 26 leave such a mode, eg. if the mode is entered by holding down a 27 key long enough, it will be left when the key is released. 28 """ 29 30 def __init__(self): 31 super(StateGroup, self).__init__() 32 self.states = [] 33 self.keys_pressed = {} 34 35 @property 36 def active_states(self): 37 return [s for s in self.states if s.active] 38 39 def create_state(self, enter, leave, popup=None): 40 s = State(self, popup) 41 s.popup = None # FIXME: who uses this? hack? 42 s.on_enter = enter 43 s.on_leave = leave 44 self.states.append(s) 45 return s 46 47 def create_popup_state(self, popup): 48 return self.create_state(popup.enter, popup.leave, popup) 49 50 51class State (object): 52 """A GUI state. 53 54 A GUI state is a mode which the GUI is in, for example an active 55 popup window or a special (usually short-lived) view on the 56 document. The application defines functions to be called when the 57 state is entered or left. 58 """ 59 60 ## Class consts and instance defaults 61 62 #: How long a key can be held down to go through as single hit (and not 63 #: press-and-hold) 64 max_key_hit_duration = 0.250 65 66 #: The state is automatically left after this time (ignored during 67 #: press-and-hold) 68 autoleave_timeout = 0.800 69 70 # : popups only: how long the cursor is allowed outside before closing 71 # : (ignored during press-and-hold)" 72 #outside_popup_timeout = 0.050 73 74 #: state to activate when this state is activated while already active 75 #: (None = just leave this state) 76 next_state = None 77 78 #: Allowed buttons and their masks for starting and continuing states 79 #: triggered by gdk button press events. 80 allowed_buttons_masks = { 81 1: Gdk.ModifierType.BUTTON1_MASK, 82 2: Gdk.ModifierType.BUTTON2_MASK, 83 3: Gdk.ModifierType.BUTTON3_MASK, } 84 85 #: Human-readable display string for the state. 86 label = None 87 88 89 ## Methods 90 91 def __init__(self, stategroup, popup): 92 super(State, self).__init__() 93 self.sg = stategroup 94 self.active = False 95 self.popup = popup 96 self._autoleave_timeout_id = None 97 self._outside_popup_timeout_id = None 98 if popup: 99 popup.connect("enter-notify-event", self._popup_enter_notify_cb) 100 popup.connect("leave-notify-event", self._popup_leave_notify_cb) 101 popup.popup_state = self # FIXME: hacky? 102 self.outside_popup_timeout = popup.outside_popup_timeout 103 104 def enter(self, **kwargs): 105 logger.debug('Entering State, calling %s', self.on_enter.__name__) 106 assert not self.active 107 self.active = True 108 self._enter_time = Gtk.get_current_event_time()/1000.0 109 try: 110 self.on_enter(**kwargs) 111 except: 112 logger.exception("State on_enter method failed") 113 raise 114 self._restart_autoleave_timeout() 115 116 def leave(self, reason=None): 117 logger.debug( 118 'Leaving State (reason=%r), calling %s', 119 reason, 120 self.on_leave.__name__, 121 ) 122 assert self.active 123 self.active = False 124 self._stop_autoleave_timeout() 125 self._stop_outside_popup_timeout() 126 self._enter_time = None 127 try: 128 self.on_leave(reason) 129 except: 130 logger.exception("State on_leave method failed") 131 raise 132 133 def activate(self, action_or_event=None, **kwargs): 134 """Activate a State from an action or a button press event. 135 136 :param action_or_event: A Gtk.Action, or a Gdk.Event. 137 :param \*\*kwargs: passed to enter(). 138 139 For events, only button press events are supported by this code. 140 141 When a Gtk.Action is activated, custom attributes are used to 142 figure out whether the action was invoked from a menu, or using 143 a keypress. This requires the action to have been registered 144 with the app's keyboard manager. 145 146 See also `keyboard.KeyboardManager.takeover_event()`. 147 148 """ 149 if self.active: 150 # pressing the key again 151 if self.next_state: 152 self.leave() 153 self.next_state.activate(action_or_event) 154 return 155 156 # first leave other active states from the same stategroup 157 for state in self.sg.active_states: 158 state.leave() 159 160 self.keydown = False 161 self.mouse_button = None 162 163 if action_or_event: 164 if not isinstance(action_or_event, Gtk.Action): 165 e = action_or_event 166 # eat any multiple clicks. TODO should possibly try to e.g. 167 # split a triple click into three clicks in the future. 168 if (e.type == Gdk.EventType.DOUBLE_BUTTON_PRESS or 169 e.type == Gdk.EventType.TRIPLE_BUTTON_PRESS): 170 e.type = Gdk.EventType.BUTTON_PRESS 171 172 # currently we only support mouse buttons being single-pressed. 173 assert e.type == Gdk.EventType.BUTTON_PRESS 174 # let's just note down what mouse button that was 175 assert e.button 176 if e.button in self.allowed_buttons_masks: 177 self.mouse_button = e.button 178 179 else: 180 a = action_or_event 181 # register for key release events, see keyboard.py 182 if a.keydown: 183 a.keyup_callback = self._keyup_cb 184 self.keydown = True 185 self.activated_by_keyboard = self.keydown # FIXME: should probably be renamed (mouse button possible) 186 self.enter(**kwargs) 187 188 def toggle(self, action=None): 189 if isinstance(action, Gtk.ToggleAction): 190 want_active = action.get_active() 191 else: 192 want_active = not self.active 193 if want_active: 194 if not self.active: 195 self.activate(action) 196 else: 197 if self.active: 198 self.leave() 199 200 def _keyup_cb(self, widget, event): 201 if not self.active: 202 return 203 self.keydown = False 204 if event.time/1000.0 - self._enter_time < self.max_key_hit_duration: 205 pass # accept as one-time hit 206 else: 207 if self._outside_popup_timeout_id: 208 self.leave('outside') 209 else: 210 self.leave('keyup') 211 212 ## Auto-leave timeout 213 214 def _stop_autoleave_timeout(self): 215 if not self._autoleave_timeout_id: 216 return 217 GLib.source_remove(self._autoleave_timeout_id) 218 self._autoleave_timeout_id = None 219 220 def _restart_autoleave_timeout(self): 221 if not self.autoleave_timeout: 222 return 223 self._stop_autoleave_timeout() 224 self._autoleave_timeout_id = GLib.timeout_add( 225 int(1000*self.autoleave_timeout), 226 self._autoleave_timeout_cb, 227 ) 228 229 def _autoleave_timeout_cb(self): 230 if not self.keydown: 231 self.leave('timeout') 232 return False 233 234 ## Outside-popup timer 235 236 def _stop_outside_popup_timeout(self): 237 if not self._outside_popup_timeout_id: 238 return 239 GLib.source_remove(self._outside_popup_timeout_id) 240 self._outside_popup_timeout_id = None 241 242 def _restart_outside_popup_timeout(self): 243 if not self.outside_popup_timeout: 244 return 245 self._stop_outside_popup_timeout() 246 self._outside_popup_timeout_id = GLib.timeout_add( 247 int(1000*self.outside_popup_timeout), 248 self._outside_popup_timeout_cb, 249 ) 250 251 def _outside_popup_timeout_cb(self): 252 if not self._outside_popup_timeout_id: 253 return False 254 self._outside_popup_timeout_id = None 255 if not self.keydown: 256 self.leave('outside') 257 return False 258 259 def _popup_enter_notify_cb(self, widget, event): 260 if not self.active: 261 return 262 if self._outside_popup_timeout_id: 263 GLib.source_remove(self._outside_popup_timeout_id) 264 self._outside_popup_timeout_id = None 265 266 def _popup_leave_notify_cb(self, widget, event): 267 if not self.active: 268 return 269 # allow to leave the window for a short time 270 if self._outside_popup_timeout_id: 271 GLib.source_remove(self._outside_popup_timeout_id) 272 self._outside_popup_timeout_id = None 273 self._outside_popup_timeout_id = GLib.timeout_add( 274 int(1000*self.outside_popup_timeout), 275 self._outside_popup_timeout_cb, 276 ) 277