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