Beispiel #1
0
    def __init__(self, wp):
        self._wp = wp
        self._accessible = None
        self._can_insert_text = False

        self._text_domains = TextDomains()
        self._text_domain = self._text_domains.get_nop_domain()

        self._changes = TextChanges()
        self._entering_text = False
        self._text_changed = False

        self._context = ""
        self._line = ""
        self._line_caret = 0
        self._selection_span = TextSpan()
        self._begin_of_text = False        # context starts at begin of text?
        self._begin_of_text_offset = None  # offset of text begin

        self._pending_separator_span = None
        self._last_text_change_time = 0
        self._last_caret_move_time = 0
        self._last_caret_move_position = 0

        self._last_context = None
        self._last_line = None

        self._update_context_timer = Timer()
        self._update_context_delay_normal = 0.01
        self._update_context_delay = self._update_context_delay_normal
Beispiel #2
0
    def __init__(self):
        WindowRectTracker.__init__(self)
        self._screen_orientation = None
        self._save_position_timer = Timer()

        # init detection of screen "rotation"
        screen = self.get_screen()
        screen.connect('size-changed', self.on_screen_size_changed)
Beispiel #3
0
    def __init__(self, keyboard, notify_done_callback):
        self._layout = None
        self._notify_done_callback = notify_done_callback
        self._drag_selected = False  # grazed by the pointer?

        KeyboardPopup.__init__(self)
        LayoutView.__init__(self, keyboard)
        TouchInput.__init__(self)

        self.connect("destroy", self._on_destroy_event)

        self._close_timer = Timer()
        self.start_close_timer()
Beispiel #4
0
    def __init__(self, redraw_callback, activate_callback):
        super(ScanMode, self).__init__()

        logger.debug("ScanMode.__init__()")
        """ Activation timer instance """
        self._activation_timer = Timer()
        """ Counter for key flash animation """
        self._flash = 0
        """ Callback for key redraws """
        self._redraw_callback = redraw_callback
        """ Callback for key activation """
        self._activate_callback = activate_callback
        """ A Chunker instance """
        self.chunker = None
Beispiel #5
0
    def on_screen_size_changed(self, screen):
        """ detect screen rotation (tablets)"""

        # Give the screen time to settle, the window manager
        # may block the move to previously invalid positions and
        # when docked, the slide animation may be drowned out by all
        # the action in other processes.
        Timer(1.5, self.on_screen_size_changed_delayed, screen)
Beispiel #6
0
    def __init__(self):
        InputEventSource.__init__(self)

        self._input_sequences = {}

        self._touch_events_enabled = False
        self._multi_touch_enabled = False
        self._gestures_enabled = False

        self._last_event_was_touch = False
        self._last_sequence_time = 0

        self._gesture = NO_GESTURE
        self._gesture_begin_point = (0, 0)
        self._gesture_begin_time = 0
        self._gesture_detected = False
        self._gesture_cancelled = False
        self._num_tap_sequences = 0
        self._gesture_timer = Timer()
Beispiel #7
0
    def on_input_sequence_end(self, sequence):
        key = sequence.active_key
        if key:
            keyboard = self.keyboard
            keyboard.key_up(key, self, sequence)

        if key and \
           not self._drag_selected:
            Timer(config.UNPRESS_DELAY, self.close_window)
        else:
            self.close_window()
Beispiel #8
0
    def init(self):
        self.keyboard_state = None
        self._last_mod_mask = 0
        self.vk_timer = None
        self.reset_vk()
        self._connections = []
        self._window = None
        self.status_icon = None
        self.service_keyboard = None
        self._reload_layout_timer = Timer()

        # finish config initialization
        config.init()

        # Optionally wait a little before proceeding.
        # When the system starts up, the docking strut can be unreliable
        # on Compiz (Yakkety) and the keyboard may end up at unexpected initial
        # positions. Delaying the startup when launched by
        # onboard-autostart.desktop helps to prevent this.
        delay = config.startup_delay
        if delay:
            Timer(delay, self._init_delayed)
        else:
            self._init_delayed()
Beispiel #9
0
    def __init__(self):
        InputEventSource.__init__(self)

        self._input_sequences = {}

        self._touch_events_enabled = False
        self._multi_touch_enabled  = False
        self._gestures_enabled     = False

        self._last_event_was_touch = False
        self._last_sequence_time = 0

        self._gesture = NO_GESTURE
        self._gesture_begin_point = (0, 0)
        self._gesture_begin_time = 0
        self._gesture_detected = False
        self._gesture_cancelled = False
        self._num_tap_sequences = 0
        self._gesture_timer = Timer()
Beispiel #10
0
    def __init__(self, redraw_callback, activate_callback):
        super(ScanMode, self).__init__()

        logger.debug("ScanMode.__init__()")

        """ Activation timer instance """
        self._activation_timer = Timer()

        """ Counter for key flash animation """
        self._flash = 0

        """ Callback for key redraws """
        self._redraw_callback = redraw_callback

        """ Callback for key activation """
        self._activate_callback = activate_callback

        """ A Chunker instance """
        self.chunker = None
Beispiel #11
0
class TouchInput(InputEventSource):
    """
    Unified handling of multi-touch sequences and conventional pointer input.
    """
    GESTURE_DETECTION_SPAN = 100 # [ms] until two finger tap&drag is detected
    GESTURE_DELAY_PAUSE = 3000   # [ms] Suspend delayed sequence begin for this
                                 # amount of time after the last key press.
    DELAY_SEQUENCE_BEGIN = True  # No delivery, i.e. no key-presses after
                                 # gesture detection, but delays press-down.

    def __init__(self):
        InputEventSource.__init__(self)

        self._input_sequences = {}

        self._touch_events_enabled = False
        self._multi_touch_enabled  = False
        self._gestures_enabled     = False

        self._last_event_was_touch = False
        self._last_sequence_time = 0

        self._gesture = NO_GESTURE
        self._gesture_begin_point = (0, 0)
        self._gesture_begin_time = 0
        self._gesture_detected = False
        self._gesture_cancelled = False
        self._num_tap_sequences = 0
        self._gesture_timer = Timer()

    def set_touch_input_mode(self, touch_input):
        """ Call this to enable single/multi-touch """
        self._touch_events_enabled = touch_input != TouchInputEnum.NONE
        self._multi_touch_enabled  = touch_input == TouchInputEnum.MULTI
        self._gestures_enabled     = self._touch_events_enabled
        if self._device_manager:
            self._device_manager.update_devices() # reset touch_active

        _logger.debug("setting touch input mode {}: "
                      "touch_events_enabled={}, "
                      "multi_touch_enabled={}, "
                      "gestures_enabled={}" \
                      .format(touch_input,
                              self._touch_events_enabled,
                              self._multi_touch_enabled,
                              self._gestures_enabled))

    def has_input_sequences(self):
        """ Are any clicks/touches still ongoing? """
        return bool(self._input_sequences)

    def last_event_was_touch(self):
        """ Was there just a touch event? """
        return self._last_event_was_touch

    @staticmethod
    def _get_event_source(event):
        device = event.get_source_device()
        return device.get_source()

    def _can_handle_pointer_event(self, event):
        """
        Rely on pointer events? True for non-touch devices
        and wacom touch-screens with gestures enabled.
        """
        device = event.get_source_device()
        source = device.get_source()

        return not self._touch_events_enabled or \
               source != Gdk.InputSource.TOUCHSCREEN or \
               not self.is_device_touch_active(device)

    def _can_handle_touch_event(self, event):
        """
        Rely on touch events? True for touch devices
        and wacom touch-screens with gestures disabled.
        """
        return not self._can_handle_pointer_event(event)

    def _on_button_press_event(self, widget, event):
        self.log_event("_on_button_press_event1 {} {} {} ",
                       self._touch_events_enabled,
                       self._can_handle_pointer_event(event),
                       self._get_event_source(event))

        if not self._can_handle_pointer_event(event):
            self.log_event("_on_button_press_event2 abort")
            return

        # - Ignore double clicks (GDK_2BUTTON_PRESS),
        #   we're handling those ourselves.
        # - Ignore mouse wheel button events
        self.log_event("_on_button_press_event3 {} {}",
                       event.type, event.button)
        if event.type == Gdk.EventType.BUTTON_PRESS and \
           1 <= event.button <= 3:
            sequence = InputSequence()
            sequence.init_from_button_event(event)
            sequence.primary = True
            self._last_event_was_touch = False

            self.log_event("_on_button_press_event4")
            self._input_sequence_begin(sequence)

        return True

    def _on_button_release_event(self, widget, event):
        sequence = self._input_sequences.get(POINTER_SEQUENCE)
        self.log_event("_on_button_release_event", sequence)
        if not sequence is None:
            sequence.point      = (event.x, event.y)
            sequence.root_point = (event.x_root, event.y_root)
            sequence.time       = event.get_time()

            self._input_sequence_end(sequence)

        return True

    def _on_motion_event(self, widget, event):
        if not self._can_handle_pointer_event(event):
            return

        sequence = self._input_sequences.get(POINTER_SEQUENCE)
        if sequence is None and \
           not event.state & BUTTON123_MASK:
            sequence = InputSequence()
            sequence.primary = True

        if sequence:
            sequence.init_from_motion_event(event)

            self._last_event_was_touch = False
            self._input_sequence_update(sequence)

        return True

    def _on_enter_notify(self, widget, event):
        self.on_enter_notify(widget, event)
        return True

    def _on_leave_notify(self, widget, event):
        self.on_leave_notify(widget, event)
        return True

    def _on_touch_event(self, widget, event):
        self.log_event("_on_touch_event1 {}", self._get_event_source(event))

        event_type = event.type

        # Set source_device touch-active to block processing of pointer events.
        # "touch-screens" that don't send touch events will keep having pointer
        # events handled (Wacom devices with gestures enabled).
        # This assumes that for devices that emit both touch and pointer
        # events, the touch event comes first. Else there will be a dangling
        # touch sequence. _discard_stuck_input_sequences would clean that up,
        # but a key might get still get stuck in pressed state.
        device = event.get_source_device()
        self.set_device_touch_active(device)

        if not self._can_handle_touch_event(event):
            self.log_event("_on_touch_event2 abort")
            return

        touch = event.touch if hasattr(event, "touch") else event
        id = str(touch.sequence)
        self._last_event_was_touch = True

        if event_type == Gdk.EventType.TOUCH_BEGIN:
            sequence = InputSequence()
            sequence.init_from_touch_event(touch, id)
            if len(self._input_sequences) == 0:
                sequence.primary = True

            self._input_sequence_begin(sequence)

        elif event_type == Gdk.EventType.TOUCH_UPDATE:
            sequence = self._input_sequences.get(id)
            if not sequence is None:
                sequence.point       = (touch.x, touch.y)
                sequence.root_point  = (touch.x_root, touch.y_root)
                sequence.time        = event.get_time()
                sequence.update_time = time.time()

                self._input_sequence_update(sequence)

        else:
            if event_type == Gdk.EventType.TOUCH_END:
                pass

            elif event_type == Gdk.EventType.TOUCH_CANCEL:
                pass

            sequence = self._input_sequences.get(id)
            if not sequence is None:
                sequence.time = event.get_time()
                self._input_sequence_end(sequence)

        return True

    def _input_sequence_begin(self, sequence):
        """ Button press/touch begin """
        self.log_event("_input_sequence_begin1 {}", sequence)
        self._gesture_sequence_begin(sequence)
        first_sequence = len(self._input_sequences) == 0

        if first_sequence or \
           self._multi_touch_enabled:
            self._input_sequences[sequence.id] = sequence

            if not self._gesture_detected:
                if first_sequence and \
                   self._multi_touch_enabled and \
                   self.DELAY_SEQUENCE_BEGIN and \
                   sequence.time - self._last_sequence_time > \
                                   self.GESTURE_DELAY_PAUSE and \
                   self.can_delay_sequence_begin(sequence): # ask Keyboard
                    # Delay the first tap; we may have to stop it
                    # from reaching the keyboard.
                    self._gesture_timer.start(self.GESTURE_DETECTION_SPAN / 1000.0,
                                              self.on_delayed_sequence_begin,
                                              sequence, sequence.point)

                else:
                    # Tell the keyboard right away.
                    self.deliver_input_sequence_begin(sequence)

        self._last_sequence_time = sequence.time

    def can_delay_sequence_begin(self, sequence):
        """ Overloaded in LayoutView to veto delay for move buttons. """
        return True

    def on_delayed_sequence_begin(self, sequence, point):
        if not self._gesture_detected: # work around race condition
            sequence.point = point # return to the original begin point
            self.deliver_input_sequence_begin(sequence)
            self._gesture_cancelled = True
        return False

    def deliver_input_sequence_begin(self, sequence):
        self.log_event("deliver_input_sequence_begin {}", sequence)
        self.on_input_sequence_begin(sequence)
        sequence.delivered = True

    def _input_sequence_update(self, sequence):
        """ Pointer motion/touch update """
        self._gesture_sequence_update(sequence)
        if not sequence.state & BUTTON123_MASK or \
           not self.in_gesture_detection_delay(sequence):
            self._gesture_timer.finish()  # run delayed begin before update
            self.on_input_sequence_update(sequence)

    def _input_sequence_end(self, sequence):
        """ Button release/touch end """
        self.log_event("_input_sequence_end1 {}", sequence)
        self._gesture_sequence_end(sequence)
        self._gesture_timer.finish()  # run delayed begin before end
        if sequence.id in self._input_sequences:
            del self._input_sequences[sequence.id]

            if sequence.delivered:
                self.log_event("_input_sequence_end2 {}", sequence)
                self.on_input_sequence_end(sequence)

        if self._input_sequences:
            self._discard_stuck_input_sequences()

        self._last_sequence_time = sequence.time

    def _discard_stuck_input_sequences(self):
        """
        Input sequence handling requires guaranteed balancing of
        begin, update and end events. There is no indication yet this
        isn't always the case, but still, at this time it seems like a
        good idea to prepare for the worst.
        -> Clear out aged input sequences, so Onboard can start from a
        fresh slate and not become terminally unresponsive.
        """
        expired_time = time.time() - 30
        for id, sequence in list(self._input_sequences.items()):
            if sequence.update_time < expired_time:
                _logger.warning("discarding expired input sequence " + str(id))
                del self._input_sequences[id]

    def in_gesture_detection_delay(self, sequence):
        """
        Are we still in the time span where sequence begins aren't delayed
        and can't be undone after gesture detection?
        """
        span = sequence.time - self._gesture_begin_time
        return span < self.GESTURE_DETECTION_SPAN

    def _gesture_sequence_begin(self, sequence):
        # first tap?
        if self._num_tap_sequences == 0:
            self._gesture = NO_GESTURE
            self._gesture_detected = False
            self._gesture_cancelled = False
            self._gesture_begin_point = sequence.point
            self._gesture_begin_time = sequence.time # event time
        else: # subsequent taps
            if self.in_gesture_detection_delay(sequence) and \
               not self._gesture_cancelled:
                self._gesture_timer.stop()  # cancel delayed sequence begin
                self._gesture_detected = True
        self._num_tap_sequences += 1

    def _gesture_sequence_update(self, sequence):
        if self._gesture_detected and \
           sequence.state & BUTTON123_MASK and \
           self._gesture == NO_GESTURE:
            point = sequence.point
            dx = self._gesture_begin_point[0] - point[0]
            dy = self._gesture_begin_point[1] - point[1]
            d2 = dx * dx + dy * dy

            # drag gesture?
            if d2 >= DRAG_GESTURE_THRESHOLD2:
                num_touches = len(self._input_sequences)
                self._gesture = DRAG_GESTURE
                self.on_drag_gesture_begin(num_touches)
        return True

    def _gesture_sequence_end(self, sequence):
        if len(self._input_sequences) == 1: # last sequence of the gesture?
            if self._gesture_detected:
                gesture = self._gesture

                if gesture == NO_GESTURE:
                    # tap gesture?
                    elapsed = sequence.time - self._gesture_begin_time
                    if elapsed <= 300:
                        self.on_tap_gesture(self._num_tap_sequences)

                elif gesture == DRAG_GESTURE:
                    self.on_drag_gesture_end(0)

            self._num_tap_sequences = 0

    def on_tap_gesture(self, num_touches):
        return False

    def on_drag_gesture_begin(self, num_touches):
        return False

    def on_drag_gesture_end(self, num_touches):
        return False

    def redirect_sequence_update(self, sequence, func):
        """ redirect input sequence update to self. """
        sequence = self._get_redir_sequence(sequence)
        func(sequence)

    def redirect_sequence_end(self, sequence, func):
        """ Redirect input sequence end to self. """
        sequence = self._get_redir_sequence(sequence)

        # Make sure has_input_sequences() returns False inside of func().
        # Class Keyboard needs this to detect the end of input.
        if sequence.id in self._input_sequences:
            del self._input_sequences[sequence.id]

        func(sequence)

    def _get_redir_sequence(self, sequence):
        """ Return a copy of <sequence>, managed in the target window. """
        redir_sequence = self._input_sequences.get(sequence.id)
        if redir_sequence is None:
            redir_sequence = sequence.copy()
            redir_sequence.initial_active_key = None
            redir_sequence.active_key = None
            redir_sequence.cancel_key_action = False # was canceled by long press

            self._input_sequences[redir_sequence.id] = redir_sequence

        # convert to the new window client coordinates
        pos = self.get_position()
        rp = sequence.root_point
        redir_sequence.point = (rp[0] - pos[0], rp[1] - pos[1])

        return redir_sequence
Beispiel #12
0
class IconPalette(WindowRectPersist, WindowManipulator, Gtk.Window):
    """
    Class that creates a movable and resizable floating window without
    decorations. The window shows the icon of onboard scaled to fit to the
    window and a resize grip that honors the desktop theme in use.

    Onboard offers an option to the user to make the window appear
    whenever the user hides the onscreen keyboard. The user can then
    click on the window to hide it and make the onscreen keyboard
    reappear.
    """

    __gsignals__ = {
        str('activated'): (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ())
    }
    """ Minimum size of the IconPalette """
    MINIMUM_SIZE = 20

    OPACITY = 0.75

    _layout_view = None

    def __init__(self, keyboard):
        self._visible = False
        self._force_to_top = False
        self._last_pos = None

        self._dwell_progress = DwellProgress()
        self._dwell_begin_timer = None
        self._dwell_timer = None
        self._no_more_dwelling = False

        self._menu = ContextMenu(keyboard)

        args = {
            "type_hint": self._get_window_type_hint(),
            "skip_taskbar_hint": True,
            "skip_pager_hint": True,
            "urgency_hint": False,
            "decorated": False,
            "accept_focus": False,
            "width_request": self.MINIMUM_SIZE,
            "height_request": self.MINIMUM_SIZE,
            "app_paintable": True,
        }
        if gtk_has_resize_grip_support():
            args["has_resize_grip"] = False

        Gtk.Window.__init__(self, **args)

        WindowRectPersist.__init__(self)
        WindowManipulator.__init__(self)

        self.set_keep_above(True)

        self.drawing_area = Gtk.DrawingArea()
        self.add(self.drawing_area)
        self.drawing_area.connect("draw", self._on_draw)
        self.drawing_area.show()

        # use transparency if available
        visual = Gdk.Screen.get_default().get_rgba_visual()
        if visual:
            self.set_visual(visual)
            self.drawing_area.set_visual(visual)

        # set up event handling
        self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK
                        | Gdk.EventMask.BUTTON_RELEASE_MASK
                        | Gdk.EventMask.POINTER_MOTION_MASK)

        # self.connect("draw", self._on_draw)
        self.connect("button-press-event", self._on_button_press_event)
        self.connect("motion-notify-event", self._on_motion_notify_event)
        self.connect("button-release-event", self._on_button_release_event)
        self.connect("configure-event", self._on_configure_event)
        self.connect("realize", self._on_realize_event)
        self.connect("unrealize", self._on_unrealize_event)
        self.connect("enter-notify-event", self._on_mouse_enter)
        self.connect("leave-notify-event", self._on_mouse_leave)

        # default coordinates of the iconpalette on the screen
        self.set_min_window_size(self.MINIMUM_SIZE, self.MINIMUM_SIZE)
        # no flashing on left screen edge in unity
        # self.set_default_size(1, 1)
        self.restore_window_rect()

        # Realize the window. Test changes to this in all supported
        # environments. It's all too easy to have the icp not show up reliably.
        self.update_window_options()
        self.hide()

        once = CallOnce(100).enqueue  # call at most once per 100ms
        config.icp.position_notify_add(
            lambda x: once(self._on_config_rect_changed))
        config.icp.size_notify_add(
            lambda x: once(self._on_config_rect_changed))
        config.icp.window_handles_notify_add(
            lambda x: self.update_window_handles())

        self.update_sticky_state()
        self.update_window_handles()

    def cleanup(self):
        WindowRectPersist.cleanup(self)

    def set_layout_view(self, view):
        self._layout_view = view
        self.queue_draw()

    def get_color_scheme(self):
        if self._layout_view:
            return self._layout_view.get_color_scheme()
        return None

    def get_menu(self):
        return self._menu

    def _on_configure_event(self, widget, event):
        self.update_window_rect()

    def on_drag_initiated(self):
        self.stop_save_position_timer()
        self._stop_dwelling()

    def on_drag_done(self):
        self.update_window_rect()
        self.start_save_position_timer()
        self._no_more_dwelling = True

    def _on_realize_event(self, user_data):
        """ Gdk window created """
        set_unity_property(self)
        if config.is_force_to_top():
            self.set_override_redirect(True)
        self.restore_window_rect(True)

    def _on_unrealize_event(self, user_data):
        """ Gdk window destroyed """
        self.set_type_hint(self._get_window_type_hint())

    def _get_window_type_hint(self):
        if config.is_force_to_top():
            return Gdk.WindowTypeHint.NORMAL
        else:
            return Gdk.WindowTypeHint.UTILITY

    def update_window_options(self, startup=False):
        if not config.xid_mode:  # not when embedding

            # (re-)create the gdk window?
            force_to_top = config.is_force_to_top()

            if self._force_to_top != force_to_top:
                self._force_to_top = force_to_top

                visible = self._visible  # visible before?

                if self.get_realized():  # not starting up?
                    self.hide()
                    self.unrealize()

                self.realize()

                if visible:
                    self.show()

    def update_sticky_state(self):
        if not config.xid_mode:
            if config.get_sticky_state():
                self.stick()
            else:
                self.unstick()

    def update_window_handles(self):
        """ Tell WindowManipulator about the active resize/move handles """
        self.set_drag_handles(config.icp.window_handles)

    def get_drag_threshold(self):
        """ Overload for WindowManipulator """
        return config.get_drag_threshold()

    def _on_button_press_event(self, widget, event):
        if event.window == self.get_window():
            if Gdk.Event.triggers_context_menu(event):
                self._menu.popup(event.button, event.get_time())

            elif event.button == Gdk.BUTTON_PRIMARY:
                self.enable_drag_protection(True)
                sequence = InputSequence()
                sequence.init_from_button_event(event)
                self.handle_press(sequence, move_on_background=True)
                if self.is_moving():
                    self.reset_drag_protection()  # force threshold

        return True

    def _on_motion_notify_event(self, widget, event):
        """
        Move the window if the pointer has moved more than the DND threshold.
        """
        sequence = InputSequence()
        sequence.init_from_motion_event(event)
        self.handle_motion(sequence, fallback=True)
        self.set_drag_cursor_at((event.x, event.y))

        # start dwelling if nothing else is going on
        point = (event.x, event.y)
        hit = self.hit_test_move_resize(point)
        if hit is None:
            if not self.is_drag_initiated() and \
               not self._is_dwelling() and \
               not self._no_more_dwelling and \
               not config.is_hover_click_active() and \
               not config.lockdown.disable_dwell_activation:
                self._start_dwelling()
        else:
            self._stop_dwelling()  # allow resizing in peace

        return True

    def _on_button_release_event(self, widget, event):
        """
        Save the window geometry, hide the IconPalette and
        emit the "activated" signal.
        """
        if event.window == self.get_window():
            if event.button == 1 and \
               event.window == self.get_window() and \
               not self.is_drag_active():
                self.emit("activated")

            self.stop_drag()
            self.set_drag_cursor_at((event.x, event.y))

        return True

    def _on_mouse_enter(self, widget, event):
        pass

    def _on_mouse_leave(self, widget, event):
        self._stop_dwelling()
        self._no_more_dwelling = False

    def _on_draw(self, widget, cr):
        """
        Draw the onboard icon.
        """
        if not Gtk.cairo_should_draw_window(cr, widget.get_window()):
            return False

        rect = Rect(0.0, 0.0, float(self.get_allocated_width()),
                    float(self.get_allocated_height()))
        color_scheme = self.get_color_scheme()

        # clear background
        cr.save()
        cr.set_operator(cairo.OPERATOR_CLEAR)
        cr.paint()
        cr.restore()

        composited = Gdk.Screen.get_default().is_composited()
        if composited:
            cr.push_group()

        # draw background color
        background_rgba = list(color_scheme.get_icon_rgba("background"))

        if Gdk.Screen.get_default().is_composited():
            background_rgba[3] *= 0.75
            cr.set_source_rgba(*background_rgba)

            corner_radius = min(rect.w, rect.h) * 0.1

            roundrect_arc(cr, rect, corner_radius)
            cr.fill()

            # decoration frame
            line_rect = rect.deflate(2)
            cr.set_line_width(2)
            roundrect_arc(cr, line_rect, corner_radius)
            cr.stroke()
        else:
            cr.set_source_rgba(*background_rgba)
            cr.paint()

        # draw themed icon
        self._draw_themed_icon(cr, rect, color_scheme)

        # draw dwell progress
        rgba = [0.8, 0.0, 0.0, 0.5]
        bg_rgba = [0.1, 0.1, 0.1, 0.5]
        if color_scheme:
            # take dwell color from the first icon "key"
            key = RectKey("icon0")
            rgba = color_scheme.get_key_rgba(key, "dwell-progress")
            rgba[3] = min(0.75, rgba[3])  # more transparency

            key = RectKey("icon1")
            bg_rgba = color_scheme.get_key_rgba(key, "fill")
            bg_rgba[3] = min(0.75, rgba[3])  # more transparency

        dwell_rect = rect.grow(0.5)
        self._dwell_progress.draw(cr, dwell_rect, rgba, bg_rgba)

        if composited:
            cr.pop_group_to_source()
            cr.paint_with_alpha(self.OPACITY)

        return True

    def _draw_themed_icon(self, cr, icon_rect, color_scheme):
        """ draw themed icon """
        keys = [RectKey("icon" + str(i)) for i in range(4)]

        # Default colors for the case when none of the icon keys
        # are defined in the color scheme.
        # background_rgba =  [1.0, 1.0, 1.0, 1.0]
        fill_rgbas = [[0.9, 0.7, 0.0, 0.75], [1.0, 1.0, 1.0, 1.0],
                      [1.0, 1.0, 1.0, 1.0], [0.0, 0.54, 1.0, 1.0]]
        stroke_rgba = [0.0, 0.0, 0.0, 1.0]
        label_rgba = [0.0, 0.0, 0.0, 1.0]

        themed = False
        if color_scheme:
            if any(color_scheme.is_key_in_scheme(key) for key in keys):
                themed = True

        # four rounded rectangles
        rects = Rect(0.0, 0.0, 100.0, 100.0).deflate(5) \
                                            .subdivide(2, 2, 6)
        cr.save()
        cr.scale(icon_rect.w / 100., icon_rect.h / 100.0)
        cr.translate(icon_rect.x, icon_rect.y)
        cr.select_font_face("sans-serif")
        cr.set_line_width(2)

        for i, key in enumerate(keys):
            rect = rects[i]

            if themed:
                fill_rgba = color_scheme.get_key_rgba(key, "fill")
                stroke_rgba = color_scheme.get_key_rgba(key, "stroke")
                label_rgba = color_scheme.get_key_rgba(key, "label")
            else:
                fill_rgba = fill_rgbas[i]

            roundrect_arc(cr, rect, 5)
            cr.set_source_rgba(*fill_rgba)
            cr.fill_preserve()

            cr.set_source_rgba(*stroke_rgba)
            cr.stroke()

            if i == 0 or i == 3:
                if i == 0:
                    letter = "O"
                else:
                    letter = "B"

                cr.set_font_size(25)
                (x_bearing, y_bearing, _width, _height, x_advance,
                 y_advance) = cr.text_extents(letter)
                r = rect.align_rect(Rect(0, 0, _width, _height), 0.3, 0.33)
                cr.move_to(r.x - x_bearing, r.y - y_bearing)
                cr.set_source_rgba(*label_rgba)
                cr.show_text(letter)
                cr.new_path()

        cr.restore()

    def show(self):
        """
        Override Gtk.Widget.hide() to save the window geometry.
        """
        Gtk.Window.show_all(self)
        self.move_resize(*self.get_rect())  # sync with WindowRectTracker
        self._visible = True

    def hide(self):
        """
        Override Gtk.Widget.hide() to save the window geometry.
        """
        Gtk.Window.hide(self)
        self._visible = False

    def _on_config_rect_changed(self):
        """ Gsettings position or size changed """
        orientation = self.get_screen_orientation()
        rect = self.read_window_rect(orientation)
        if self.get_rect() != rect:
            self.restore_window_rect()

    def read_window_rect(self, orientation):
        """
        Read orientation dependent rect.
        Overload for WindowRectPersist.
        """
        if orientation == Orientation.LANDSCAPE:
            co = config.icp.landscape
        else:
            co = config.icp.portrait
        rect = Rect(co.x, co.y, max(co.width, 10), max(co.height, 10))
        return rect

    def write_window_rect(self, orientation, rect):
        """
        Write orientation dependent rect.
        Overload for WindowRectPersist.
        """
        # There are separate rects for normal and rotated screen (tablets).
        if orientation == Orientation.LANDSCAPE:
            co = config.icp.landscape
        else:
            co = config.icp.portrait

        co.delay()
        co.x, co.y, co.width, co.height = rect
        co.apply()

    def _is_dwelling(self):
        return (bool(self._dwell_begin_timer)
                and (self._dwell_begin_timer.is_running()
                     or self._dwell_progress.is_dwelling()))

    def _start_dwelling(self):
        self._stop_dwelling()
        self._dwell_begin_timer = Timer(1.5, self._on_dwell_begin_timer)
        self._no_more_dwelling = True

    def _stop_dwelling(self):
        if self._dwell_begin_timer:
            self._dwell_begin_timer.stop()
            if self._dwell_timer:
                self._dwell_timer.stop()
                self._dwell_progress.stop_dwelling()
                self.queue_draw()

    def _on_dwell_begin_timer(self):
        self._dwell_progress.start_dwelling()
        self._dwell_timer = Timer(0.025, self._on_dwell_timer)
        return False

    def _on_dwell_timer(self):
        self._dwell_progress.opacity, done = \
            Fade.sin_fade(self._dwell_progress.dwell_start_time, 0.3, 0, 1.0)
        self.queue_draw()
        if self._dwell_progress.is_done():
            if not self.is_drag_active():
                self.emit("activated")
                self.stop_drag()
            return False
        return True
Beispiel #13
0
 def _on_dwell_begin_timer(self):
     self._dwell_progress.start_dwelling()
     self._dwell_timer = Timer(0.025, self._on_dwell_timer)
     return False
Beispiel #14
0
 def _start_dwelling(self):
     self._stop_dwelling()
     self._dwell_begin_timer = Timer(1.5, self._on_dwell_begin_timer)
     self._no_more_dwelling = True
Beispiel #15
0
class OnboardGtk(object):
    """
    Main controller class for Onboard using GTK+
    """

    keyboard = None

    def __init__(self):

        # Make sure windows get "onboard", "Onboard" as name and class
        # For some reason they aren't correctly set when onboard is started
        # from outside the source directory (Precise).
        GLib.set_prgname(str(app))
        Gdk.set_program_class(app[0].upper() + app[1:])

        # no python3-dbus on Fedora17
        bus = None
        err_msg = ""
        if "dbus" not in globals():
            err_msg = "D-Bus bindings unavailable"
        else:
            # Use D-bus main loop by default
            dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

            # Don't fail to start in Xenial's lightdm when
            # D-Bus isn't available.
            try:
                bus = dbus.SessionBus()
            except dbus.exceptions.DBusException:
                err_msg = "D-Bus session bus unavailable"
                bus = None

        if not bus:
            _logger.warning(err_msg + "  " +
                            "Onboard will start with reduced functionality. "
                            "Single-instance check, "
                            "D-Bus service and "
                            "hover click are disabled.")

        # Yield to GNOME Shell's keyboard before any other D-Bus activity
        # to reduce the chance for D-Bus timeouts when enabling a11y keboard.
        if not self._can_show_in_current_desktop(bus):
            sys.exit(0)

        if bus:
            # Check if there is already an Onboard instance running
            has_remote_instance = \
                bus.name_has_owner(ServiceOnboardKeyboard.NAME)

            # Onboard in Ubuntu on first start silently embeds itself into
            # gnome-screensaver and stays like this until embedding is manually
            # turned off.
            # If gnome's "Typing Assistent" is disabled, only show onboard in
            # gss when there is already a non-embedded instance running in
            # the user session (LP: 938302).
            if config.xid_mode and \
               config.launched_by == config.LAUNCHER_GSS and \
               not (config.gnome_a11y and
                    config.gnome_a11y.screen_keyboard_enabled) and \
               not has_remote_instance:
                sys.exit(0)

            # Embedded instances can't become primary instances
            if not config.xid_mode:
                if has_remote_instance and \
                   not config.options.allow_multiple_instances:
                    # Present remote instance
                    remote = bus.get_object(ServiceOnboardKeyboard.NAME,
                                            ServiceOnboardKeyboard.PATH)
                    remote.Show(dbus_interface=ServiceOnboardKeyboard.IFACE)
                    _logger.info("Exiting: Not the primary instance.")
                    sys.exit(0)

                # Register our dbus name
                self._bus_name = \
                    dbus.service.BusName(ServiceOnboardKeyboard.NAME, bus)

        self.init()

        _logger.info("Entering mainloop of onboard")
        Gtk.main()

        # Shut up error messages on SIGTERM in lightdm:
        # "sys.excepthook is missing, lost sys.stderr"
        # See http://bugs.python.org/issue11380 for more.
        # Python 2.7, Precise
        try:
            sys.stdout.close()
        except:
            pass
        try:
            sys.stderr.close()
        except:
            pass

    def init(self):
        self.keyboard_state = None
        self.vk_timer = None
        self.reset_vk()
        self._connections = []
        self._window = None
        self.status_icon = None
        self.service_keyboard = None
        self._reload_layout_timer = Timer()

        # finish config initialization
        config.init()

        # Release pressed keys when onboard is killed.
        # Don't keep enter key stuck when being killed by lightdm.
        self._osk_util = osk.Util()
        self._osk_util.set_unix_signal_handler(signal.SIGTERM, self.on_sigterm)
        self._osk_util.set_unix_signal_handler(signal.SIGINT, self.on_sigint)

        sys.path.append(os.path.join(config.install_dir, 'scripts'))

        # Create the central keyboard model
        self.keyboard = Keyboard(self)

        # Create the initial keyboard widget
        # Care for toolkit independence only once there is another
        # supported one besides GTK.
        self.keyboard_widget = KeyboardWidget(self.keyboard)

        # create the main window
        if config.xid_mode:    # XEmbed mode for gnome-screensaver?
            # no icp, don't flash the icon palette in lightdm

            self._window = KbdPlugWindow(self.keyboard_widget)

            # write xid to stdout
            sys.stdout.write('%d\n' % self._window.get_id())
            sys.stdout.flush()
        else:
            icp = IconPalette(self.keyboard)
            icp.set_layout_view(self.keyboard_widget)
            icp.connect("activated", self._on_icon_palette_acticated)
            self.do_connect(icp.get_menu(), "quit-onboard",
                        lambda x: self.do_quit_onboard())

            self._window = KbdWindow(self.keyboard_widget, icp)
            self.do_connect(self._window, "quit-onboard",
                            lambda x: self.do_quit_onboard())

        # config.xid_mode = True
        self._window.application = self
        # need this to access screen properties
        config.main_window = self._window

        # load the initial layout
        _logger.info("Loading initial layout")
        self.reload_layout()

        # Handle command line options x, y, size after window creation
        # because the rotation code needs the window's screen.
        if not config.xid_mode:
            rect = self._window.get_rect().copy()
            options = config.options
            if options.size:
                size = options.size.split("x")
                rect.w = int(size[0])
                rect.h = int(size[1])
            if options.x is not None:
                rect.x = options.x
            if options.y is not None:
                rect.y = options.y

            # Make sure the keyboard fits on screen
            rect = self._window.limit_size(rect)

            if rect != self._window.get_rect():
                orientation = self._window.get_screen_orientation()
                self._window.write_window_rect(orientation, rect)
                self._window.restore_window_rect()  # move/resize early

        # export dbus service
        if not config.xid_mode and \
           "dbus" in globals():
            self.service_keyboard = ServiceOnboardKeyboard(self)

        # show/hide the window
        self.keyboard_widget.set_startup_visibility()

        # keep keyboard window and icon palette on top of dash
        self._keep_windows_on_top()

        # connect notifications for keyboard map and group changes
        self.keymap = Gdk.Keymap.get_default()
        # map changes
        self.do_connect(self.keymap, "keys-changed", self.cb_keys_changed)
        self.do_connect(self.keymap, "state-changed", self.cb_state_changed)
        # group changes
        Gdk.event_handler_set(cb_any_event, self)

        # connect config notifications here to keep config from holding
        # references to keyboard objects.
        once = CallOnce(50).enqueue  # delay callbacks by 50ms
        reload_layout       = lambda x: once(self.reload_layout_and_present)
        update_ui           = lambda x: once(self._update_ui)
        update_ui_no_resize = lambda x: once(self._update_ui_no_resize)
        update_transparency = \
            lambda x: once(self.keyboard_widget.update_transparency)
        update_inactive_transparency = \
            lambda x: once(self.keyboard_widget.update_inactive_transparency)

        # general
        config.auto_show.enabled_notify_add(lambda x:
                                    self.keyboard.update_auto_show())
        config.auto_show.hide_on_key_press_notify_add(lambda x:
                                    self.keyboard.update_auto_hide())

        # keyboard
        config.keyboard.key_synth_notify_add(reload_layout)
        config.keyboard.input_event_source_notify_add(lambda x:
                                    self.keyboard.update_input_event_source())
        config.keyboard.touch_input_notify_add(lambda x:
                                    self.keyboard.update_touch_input_mode())
        config.keyboard.show_secondary_labels_notify_add(update_ui)

        # window
        config.window.window_state_sticky_notify_add(
            lambda x: self._window.update_sticky_state())
        config.window.window_decoration_notify_add(
            self._on_window_options_changed)
        config.window.force_to_top_notify_add(self._on_window_options_changed)
        config.window.keep_aspect_ratio_notify_add(update_ui)

        config.window.transparency_notify_add(update_transparency)
        config.window.background_transparency_notify_add(update_transparency)
        config.window.transparent_background_notify_add(update_ui)
        config.window.enable_inactive_transparency_notify_add(update_transparency)
        config.window.inactive_transparency_notify_add(update_inactive_transparency)
        config.window.docking_notify_add(self._update_docking)

        # layout
        config.layout_filename_notify_add(reload_layout)

        # theme
        # config.gdi.gtk_theme_notify_add(self.on_gtk_theme_changed)
        config.theme_notify_add(self.on_theme_changed)
        config.key_label_font_notify_add(reload_layout)
        config.key_label_overrides_notify_add(reload_layout)
        config.theme_settings.color_scheme_filename_notify_add(reload_layout)
        config.theme_settings.key_label_font_notify_add(reload_layout)
        config.theme_settings.key_label_overrides_notify_add(reload_layout)
        config.theme_settings.theme_attributes_notify_add(update_ui)

        # snippets
        config.snippets_notify_add(reload_layout)

        # word suggestions
        config.word_suggestions.show_context_line_notify_add(update_ui)
        config.word_suggestions.enabled_notify_add(lambda x:
                                 self.keyboard.on_word_suggestions_enabled(x))
        config.word_suggestions.auto_learn_notify_add(
                                 update_ui_no_resize)
        config.typing_assistance.active_language_notify_add(lambda x: \
                                 self.keyboard.on_active_lang_id_changed())
        config.typing_assistance.spell_check_backend_notify_add(lambda x: \
                                 self.keyboard.on_spell_checker_changed())
        config.typing_assistance.auto_capitalization_notify_add(lambda x: \
                                 self.keyboard.on_word_suggestions_enabled(x))
        config.word_suggestions.spelling_suggestions_enabled_notify_add(lambda x: \
                                 self.keyboard.on_spell_checker_changed())
        config.word_suggestions.delayed_word_separators_enabled_notify_add(lambda x: \
                                 self.keyboard.on_punctuator_changed())
        config.word_suggestions.wordlist_buttons_notify_add(
                                 update_ui_no_resize)

        # universal access
        config.scanner.enabled_notify_add(self.keyboard._on_scanner_enabled)
        config.window.window_handles_notify_add(self._on_window_handles_changed)

        # misc
        config.keyboard.show_click_buttons_notify_add(update_ui)
        config.lockdown.lockdown_notify_add(update_ui)
        if config.mousetweaks:
            config.mousetweaks.state_notify_add(update_ui_no_resize)

        # create status icon
        self.status_icon = Indicator()
        self.status_icon.set_keyboard(self.keyboard)
        self.do_connect(self.status_icon.get_menu(), "quit-onboard",
                        lambda x: self.do_quit_onboard())

        # Callbacks to use when icp or status icon is toggled
        config.show_status_icon_notify_add(self.show_hide_status_icon)
        config.icp.in_use_notify_add(self.cb_icp_in_use_toggled)

        self.show_hide_status_icon(config.show_status_icon)


        # Minimize to IconPalette if running under GDM
        if 'RUNNING_UNDER_GDM' in os.environ:
            _logger.info("RUNNING_UNDER_GDM set, turning on icon palette")
            config.icp.in_use = True
            _logger.info("RUNNING_UNDER_GDM set, turning off indicator")
            config.show_status_icon = False

            # For some reason the new values don't arrive in gsettings when
            # running the unit test "test_running_in_live_cd_environment".
            # -> Force gsettings to apply them, that seems to do the trick.
            config.icp.apply()
            config.apply()

        # unity-2d needs the skip-task-bar hint set before the first mapping.
        self.show_hide_taskbar()


        # Check gnome-screen-saver integration
        # onboard_xembed_enabled                False True     True      True
        # config.gss.embedded_keyboard_enabled  any   False    any       False
        # config.gss.embedded_keyboard_command  any   empty    !=onboard ==onboard
        # Action:                               nop   enable   Question1 Question2
        #                                             silently
        if not config.xid_mode and \
           config.onboard_xembed_enabled:

            # If it appears, that nothing has touched the gss keys before,
            # silently enable gss integration with onboard.
            if not config.gss.embedded_keyboard_enabled and \
               not config.gss.embedded_keyboard_command:
                config.enable_gss_embedding(True)

            # If onboard is configured to be embedded into the unlock screen
            # dialog, and the embedding command is different from onboard, ask
            # the user what to do
            elif not config.is_onboard_in_xembed_command_string():
                question = _("Onboard is configured to appear with the dialog to "
                             "unlock the screen; for example to dismiss the "
                             "password-protected screensaver.\n\n"
                             "However the system is not configured anymore to use "
                             "Onboard to unlock the screen. A possible reason can "
                             "be that another application configured the system to "
                             "use something else.\n\n"
                             "Would you like to reconfigure the system to show "
                             "Onboard when unlocking the screen?")
                _logger.warning("showing dialog: '{}'".format(question))
                reply = show_confirmation_dialog(question,
                                                 self._window,
                                                 config.is_force_to_top())
                if reply == True:
                    config.enable_gss_embedding(True)
                else:
                    config.onboard_xembed_enabled = False
            else:
                if not config.gss.embedded_keyboard_enabled:
                    question = _("Onboard is configured to appear with the dialog "
                                 "to unlock the screen; for example to dismiss "
                                 "the password-protected screensaver.\n\n"
                                 "However this function is disabled in the system.\n\n"
                                 "Would you like to activate it?")
                    _logger.warning("showing dialog: '{}'".format(question))
                    reply = show_confirmation_dialog(question,
                                                     self._window,
                                                     config.is_force_to_top())
                    if reply == True:
                        config.enable_gss_embedding(True)
                    else:
                        config.onboard_xembed_enabled = False

        # check if gnome accessibility is enabled for auto-show
        if (config.is_auto_show_enabled() or \
            config.are_word_suggestions_enabled()) and \
            not config.check_gnome_accessibility(self._window):
            config.auto_show.enabled = False

    def on_sigterm(self):
        """
        Exit onboard on kill.
        """
        _logger.debug("SIGTERM received")
        self.do_quit_onboard()

    def on_sigint(self):
        """
        Exit onboard on Ctrl+C press.
        """
        _logger.debug("SIGINT received")
        self.do_quit_onboard()

    def do_connect(self, instance, signal, handler):
        handler_id = instance.connect(signal, handler)
        self._connections.append((instance, handler_id))

    # Method concerning the taskbar
    def show_hide_taskbar(self):
        """
        This method shows or hides the taskbard depending on whether there
        is an alternative way to unminimize the Onboard window.
        This method should be called every time such an alternative way
        is activated or deactivated.
        """
        if self._window:
            self._window.update_taskbar_hint()

    # Method concerning the icon palette
    def _on_icon_palette_acticated(self, widget):
        self.keyboard.toggle_visible()

    def cb_icp_in_use_toggled(self, icp_in_use):
        """
        This is the callback that gets executed when the user toggles
        the gsettings key named in_use of the icon_palette. It also
        handles the showing/hiding of the taskar.
        """
        _logger.debug("Entered in on_icp_in_use_toggled")
        self.show_hide_icp()
        _logger.debug("Leaving on_icp_in_use_toggled")

    def show_hide_icp(self):
        if self._window.icp:
            show = config.is_icon_palette_in_use()
            if show:
                # Show icon palette if appropriate and handle visibility of taskbar.
                if not self._window.is_visible():
                    self._window.icp.show()
                self.show_hide_taskbar()
            else:
                # Show icon palette if appropriate and handle visibility of taskbar.
                if not self._window.is_visible():
                    self._window.icp.hide()
                self.show_hide_taskbar()

    # Methods concerning the status icon
    def show_hide_status_icon(self, show_status_icon):
        """
        Callback called when gsettings detects that the gsettings key specifying
        whether the status icon should be shown or not is changed. It also
        handles the showing/hiding of the taskar.
        """
        if show_status_icon:
            self.status_icon.set_visible(True)
        else:
            self.status_icon.set_visible(False)
        self.show_hide_icp()
        self.show_hide_taskbar()

    def cb_status_icon_clicked(self,widget):
        """
        Callback called when status icon clicked.
        Toggles whether Onboard window visibile or not.

        TODO would be nice if appeared to iconify to taskbar
        """
        self.keyboard.toggle_visible()

    def cb_group_changed(self):
        """ keyboard group change """
        self.reload_layout_delayed()

    def cb_keys_changed(self, keymap):
        """ keyboard map change """
        self.reload_layout_delayed()

    def cb_state_changed(self, keymap):
        """ keyboard modifier state change """
        mod_mask = keymap.get_modifier_state()
        _logger.debug("keyboard state changed to 0x{:x}" \
                      .format(mod_mask))
        self.keyboard.set_modifiers(mod_mask)

    def cb_vk_timer(self):
        """
        Timer callback for polling until virtkey becomes valid.
        """
        if self.get_vk():
            self.reload_layout(force_update=True)
            GLib.source_remove(self.vk_timer)
            self.vk_timer = None
            return False
        return True

    def _update_ui(self):
        if self.keyboard:
            self.keyboard.invalidate_ui()
            self.keyboard.commit_ui_updates()

    def _update_ui_no_resize(self):
        if self.keyboard:
            self.keyboard.invalidate_ui_no_resize()
            self.keyboard.commit_ui_updates()

    def _on_window_handles_changed(self, value = None):
        self.keyboard_widget.update_window_handles()
        self._update_ui()

    def _on_window_options_changed(self, value = None):
        self._update_window_options()
        self.keyboard.commit_ui_updates()

    def _update_window_options(self, value = None):
        window = self._window
        if window:
            window.update_window_options()
            if window.icp:
                window.icp.update_window_options()
            self.keyboard.invalidate_ui()

    def _update_docking(self, value = None):
        self._update_window_options()

        # give WM time to settle, else move to the strut position may fail
        GLib.idle_add(self._update_docking_delayed)

    def _update_docking_delayed(self):
        self._window.on_docking_notify()
        self.keyboard.invalidate_ui()  # show/hide the move button
#        self.keyboard.commit_ui_updates() # redundant

    def on_gdk_setting_changed(self, name):
        if name == "gtk-theme-name":
            self.on_gtk_theme_changed()

        elif name in ["gtk-xft-dpi",
                      "gtk-xft-antialias"
                      "gtk-xft-hinting",
                      "gtk-xft-hintstyle"]:
            # For some reason the font sizes are still off when running
            # this immediately. Delay it a little.
            GLib.idle_add(self.on_gtk_font_dpi_changed)

    def on_gtk_theme_changed(self, gtk_theme = None):
        """
        Switch onboard themes in sync with gtk-theme changes.
        """
        config.load_theme()

    def on_gtk_font_dpi_changed(self):
        """
        Refresh the key's pango layout objects so that they can adapt
        to the new system dpi setting.
        """
        self.keyboard_widget.refresh_pango_layouts()
        self._update_ui()

        return False

    def on_theme_changed(self, theme):
        config.apply_theme()
        self.reload_layout()

    def _keep_windows_on_top(self, enable=True):
        if not config.xid_mode: # be defensive, not necessary when embedding
            if enable:
                windows = [self._window, self._window.icp]
            else:
                windows = []
            _logger.debug("keep_windows_on_top {}".format(windows))
            self._osk_util.keep_windows_on_top(windows)

    def on_focusable_gui_opening(self):
        self._keep_windows_on_top(False)

    def on_focusable_gui_closed(self):
        self._keep_windows_on_top(True)

    def reload_layout_and_present(self):
        """
        Reload the layout and briefly show the window
        with active transparency
        """
        self.reload_layout(force_update = True)
        self.keyboard_widget.update_transparency()

    def reload_layout_delayed(self):
        """
        Delay reloading the layout on keyboard map or group changes
        This is mainly for LP #1313176 when Caps-lock is set up as
        an accelerator to switch between keyboard "input sources" (layouts)
        in unity-control-center->Text Entry (aka region panel).
        Without waiting until after the shortcut turns off numlock,
        the next "input source" (layout) is skipped and a second one
        is selected.
        """
        self._reload_layout_timer.start(.5, self.reload_layout)

    def reload_layout(self, force_update=False):
        """
        Checks if the X keyboard layout has changed and
        (re)loads Onboards layout accordingly.
        """
        keyboard_state = (None, None)

        vk = self.get_vk()
        if vk:
            try:
                vk.reload() # reload keyboard names
                keyboard_state = (vk.get_layout_symbols(),
                                  vk.get_current_group_name())
            except osk.error:
                self.reset_vk()
                force_update = True
                _logger.warning("Keyboard layout changed, but retrieving "
                                "keyboard information failed")

        if self.keyboard_state != keyboard_state or force_update:
            self.keyboard_state = keyboard_state

            layout_filename = config.layout_filename
            color_scheme_filename = config.theme_settings.color_scheme_filename

            try:
                self.load_layout(layout_filename, color_scheme_filename)
            except LayoutFileError as ex:
                _logger.error("Layout error: " + unicode_str(ex) + ". Falling back to default layout.")

                # last ditch effort to load the default layout
                self.load_layout(config.get_fallback_layout_filename(),
                                 color_scheme_filename)

        # if there is no X keyboard, poll until it appears (if ever)
        if not vk and not self.vk_timer:
            self.vk_timer = GLib.timeout_add_seconds(1, self.cb_vk_timer)

    def load_layout(self, layout_filename, color_scheme_filename):
        _logger.info("Loading keyboard layout " + layout_filename)
        if (color_scheme_filename):
            _logger.info("Loading color scheme " + color_scheme_filename)

        vk = self.get_vk()

        color_scheme = ColorScheme.load(color_scheme_filename) \
                       if color_scheme_filename else None
        layout = LayoutLoaderSVG().load(vk, layout_filename, color_scheme)

        self.keyboard.set_layout(layout, color_scheme, vk)

        if self._window and self._window.icp:
            self._window.icp.queue_draw()

    def get_vk(self):
        if not self._vk:
            try:
                # may fail if there is no X keyboard (LP: 526791)
                self._vk = osk.Virtkey()

            except osk.error as e:
                t = time.time()
                if t > self._vk_error_time + .2: # rate limit to once per 200ms
                    _logger.warning("vk: " + unicode_str(e))
                    self._vk_error_time = t

        return self._vk

    def reset_vk(self):
        self._vk = None
        self._vk_error_time = 0


    # Methods concerning the application
    def emit_quit_onboard(self):
        self._window.emit_quit_onboard()

    def do_quit_onboard(self):
        _logger.debug("Entered do_quit_onboard")
        self.final_cleanup()
        self.cleanup()

    def cleanup(self):
        self._reload_layout_timer.stop()

        config.cleanup()

        # Make an effort to disconnect all handlers.
        # Used to be used for safe restarting.
        for instance, handler_id in self._connections:
            instance.disconnect(handler_id)

        if self.keyboard:
            if self.keyboard.scanner:
                self.keyboard.scanner.finalize()
                self.keyboard.scanner = None
            self.keyboard.cleanup()

        self.status_icon.set_keyboard(None)
        self._window.cleanup()
        self._window.destroy()
        self._window = None
        Gtk.main_quit()

    def final_cleanup(self):
        config.final_cleanup()

    @staticmethod
    def _can_show_in_current_desktop(bus):
        """
        When GNOME's "Typing Assistent" is enabled in GNOME Shell, Onboard
        starts simultaneously with the Shell's built-in screen keyboard.
        With GNOME Shell 3.5.4-0ubuntu2 there is no known way to choose
        one over the other (LP: 879942).

        Adding NotShowIn=GNOME; to onboard-autostart.desktop prevents it
        from running not only in GNOME Shell, but also in the GMOME Fallback
        session, which is undesirable. Both share the same xdg-desktop name.

        -> Do it ourselves: optionally check for GNOME Shell and yield to the
        built-in keyboard.
        """
        result = True

        # Content of XDG_CURRENT_DESKTOP:
        # Before Vivid: GNOME Shell:   GNOME
        #               GNOME Classic: GNOME
        # Since Vivid:  GNOME Shell:   GNOME
        #               GNOME Classic: GNOME-Classic:GNOME

        if config.options.not_show_in:
            current = os.environ.get("XDG_CURRENT_DESKTOP", "")
            names = config.options.not_show_in.split(",")
            for name in names:
                if name == current:
                    # Before Vivid GNOME Shell and GNOME Classic had the same
                    # desktop name "GNOME". Use the D-BUS name to decide if we
                    # are in the Shell.
                    if name == "GNOME":
                        if bus and bus.name_has_owner("org.gnome.Shell"):
                            result = False
                    else:
                        result  = False

            if not result:
                _logger.info("Command line option not-show-in={} forbids running in "
                             "current desktop environment '{}'; exiting." \
                             .format(names, current))
        return result
Beispiel #16
0
    def init(self):
        self.keyboard_state = None
        self.vk_timer = None
        self.reset_vk()
        self._connections = []
        self._window = None
        self.status_icon = None
        self.service_keyboard = None
        self._reload_layout_timer = Timer()

        # finish config initialization
        config.init()

        # Release pressed keys when onboard is killed.
        # Don't keep enter key stuck when being killed by lightdm.
        self._osk_util = osk.Util()
        self._osk_util.set_unix_signal_handler(signal.SIGTERM, self.on_sigterm)
        self._osk_util.set_unix_signal_handler(signal.SIGINT, self.on_sigint)

        sys.path.append(os.path.join(config.install_dir, 'scripts'))

        # Create the central keyboard model
        self.keyboard = Keyboard(self)

        # Create the initial keyboard widget
        # Care for toolkit independence only once there is another
        # supported one besides GTK.
        self.keyboard_widget = KeyboardWidget(self.keyboard)

        # create the main window
        if config.xid_mode:    # XEmbed mode for gnome-screensaver?
            # no icp, don't flash the icon palette in lightdm

            self._window = KbdPlugWindow(self.keyboard_widget)

            # write xid to stdout
            sys.stdout.write('%d\n' % self._window.get_id())
            sys.stdout.flush()
        else:
            icp = IconPalette(self.keyboard)
            icp.set_layout_view(self.keyboard_widget)
            icp.connect("activated", self._on_icon_palette_acticated)
            self.do_connect(icp.get_menu(), "quit-onboard",
                        lambda x: self.do_quit_onboard())

            self._window = KbdWindow(self.keyboard_widget, icp)
            self.do_connect(self._window, "quit-onboard",
                            lambda x: self.do_quit_onboard())

        # config.xid_mode = True
        self._window.application = self
        # need this to access screen properties
        config.main_window = self._window

        # load the initial layout
        _logger.info("Loading initial layout")
        self.reload_layout()

        # Handle command line options x, y, size after window creation
        # because the rotation code needs the window's screen.
        if not config.xid_mode:
            rect = self._window.get_rect().copy()
            options = config.options
            if options.size:
                size = options.size.split("x")
                rect.w = int(size[0])
                rect.h = int(size[1])
            if options.x is not None:
                rect.x = options.x
            if options.y is not None:
                rect.y = options.y

            # Make sure the keyboard fits on screen
            rect = self._window.limit_size(rect)

            if rect != self._window.get_rect():
                orientation = self._window.get_screen_orientation()
                self._window.write_window_rect(orientation, rect)
                self._window.restore_window_rect()  # move/resize early

        # export dbus service
        if not config.xid_mode and \
           "dbus" in globals():
            self.service_keyboard = ServiceOnboardKeyboard(self)

        # show/hide the window
        self.keyboard_widget.set_startup_visibility()

        # keep keyboard window and icon palette on top of dash
        self._keep_windows_on_top()

        # connect notifications for keyboard map and group changes
        self.keymap = Gdk.Keymap.get_default()
        # map changes
        self.do_connect(self.keymap, "keys-changed", self.cb_keys_changed)
        self.do_connect(self.keymap, "state-changed", self.cb_state_changed)
        # group changes
        Gdk.event_handler_set(cb_any_event, self)

        # connect config notifications here to keep config from holding
        # references to keyboard objects.
        once = CallOnce(50).enqueue  # delay callbacks by 50ms
        reload_layout       = lambda x: once(self.reload_layout_and_present)
        update_ui           = lambda x: once(self._update_ui)
        update_ui_no_resize = lambda x: once(self._update_ui_no_resize)
        update_transparency = \
            lambda x: once(self.keyboard_widget.update_transparency)
        update_inactive_transparency = \
            lambda x: once(self.keyboard_widget.update_inactive_transparency)

        # general
        config.auto_show.enabled_notify_add(lambda x:
                                    self.keyboard.update_auto_show())
        config.auto_show.hide_on_key_press_notify_add(lambda x:
                                    self.keyboard.update_auto_hide())

        # keyboard
        config.keyboard.key_synth_notify_add(reload_layout)
        config.keyboard.input_event_source_notify_add(lambda x:
                                    self.keyboard.update_input_event_source())
        config.keyboard.touch_input_notify_add(lambda x:
                                    self.keyboard.update_touch_input_mode())
        config.keyboard.show_secondary_labels_notify_add(update_ui)

        # window
        config.window.window_state_sticky_notify_add(
            lambda x: self._window.update_sticky_state())
        config.window.window_decoration_notify_add(
            self._on_window_options_changed)
        config.window.force_to_top_notify_add(self._on_window_options_changed)
        config.window.keep_aspect_ratio_notify_add(update_ui)

        config.window.transparency_notify_add(update_transparency)
        config.window.background_transparency_notify_add(update_transparency)
        config.window.transparent_background_notify_add(update_ui)
        config.window.enable_inactive_transparency_notify_add(update_transparency)
        config.window.inactive_transparency_notify_add(update_inactive_transparency)
        config.window.docking_notify_add(self._update_docking)

        # layout
        config.layout_filename_notify_add(reload_layout)

        # theme
        # config.gdi.gtk_theme_notify_add(self.on_gtk_theme_changed)
        config.theme_notify_add(self.on_theme_changed)
        config.key_label_font_notify_add(reload_layout)
        config.key_label_overrides_notify_add(reload_layout)
        config.theme_settings.color_scheme_filename_notify_add(reload_layout)
        config.theme_settings.key_label_font_notify_add(reload_layout)
        config.theme_settings.key_label_overrides_notify_add(reload_layout)
        config.theme_settings.theme_attributes_notify_add(update_ui)

        # snippets
        config.snippets_notify_add(reload_layout)

        # word suggestions
        config.word_suggestions.show_context_line_notify_add(update_ui)
        config.word_suggestions.enabled_notify_add(lambda x:
                                 self.keyboard.on_word_suggestions_enabled(x))
        config.word_suggestions.auto_learn_notify_add(
                                 update_ui_no_resize)
        config.typing_assistance.active_language_notify_add(lambda x: \
                                 self.keyboard.on_active_lang_id_changed())
        config.typing_assistance.spell_check_backend_notify_add(lambda x: \
                                 self.keyboard.on_spell_checker_changed())
        config.typing_assistance.auto_capitalization_notify_add(lambda x: \
                                 self.keyboard.on_word_suggestions_enabled(x))
        config.word_suggestions.spelling_suggestions_enabled_notify_add(lambda x: \
                                 self.keyboard.on_spell_checker_changed())
        config.word_suggestions.delayed_word_separators_enabled_notify_add(lambda x: \
                                 self.keyboard.on_punctuator_changed())
        config.word_suggestions.wordlist_buttons_notify_add(
                                 update_ui_no_resize)

        # universal access
        config.scanner.enabled_notify_add(self.keyboard._on_scanner_enabled)
        config.window.window_handles_notify_add(self._on_window_handles_changed)

        # misc
        config.keyboard.show_click_buttons_notify_add(update_ui)
        config.lockdown.lockdown_notify_add(update_ui)
        if config.mousetweaks:
            config.mousetweaks.state_notify_add(update_ui_no_resize)

        # create status icon
        self.status_icon = Indicator()
        self.status_icon.set_keyboard(self.keyboard)
        self.do_connect(self.status_icon.get_menu(), "quit-onboard",
                        lambda x: self.do_quit_onboard())

        # Callbacks to use when icp or status icon is toggled
        config.show_status_icon_notify_add(self.show_hide_status_icon)
        config.icp.in_use_notify_add(self.cb_icp_in_use_toggled)

        self.show_hide_status_icon(config.show_status_icon)


        # Minimize to IconPalette if running under GDM
        if 'RUNNING_UNDER_GDM' in os.environ:
            _logger.info("RUNNING_UNDER_GDM set, turning on icon palette")
            config.icp.in_use = True
            _logger.info("RUNNING_UNDER_GDM set, turning off indicator")
            config.show_status_icon = False

            # For some reason the new values don't arrive in gsettings when
            # running the unit test "test_running_in_live_cd_environment".
            # -> Force gsettings to apply them, that seems to do the trick.
            config.icp.apply()
            config.apply()

        # unity-2d needs the skip-task-bar hint set before the first mapping.
        self.show_hide_taskbar()


        # Check gnome-screen-saver integration
        # onboard_xembed_enabled                False True     True      True
        # config.gss.embedded_keyboard_enabled  any   False    any       False
        # config.gss.embedded_keyboard_command  any   empty    !=onboard ==onboard
        # Action:                               nop   enable   Question1 Question2
        #                                             silently
        if not config.xid_mode and \
           config.onboard_xembed_enabled:

            # If it appears, that nothing has touched the gss keys before,
            # silently enable gss integration with onboard.
            if not config.gss.embedded_keyboard_enabled and \
               not config.gss.embedded_keyboard_command:
                config.enable_gss_embedding(True)

            # If onboard is configured to be embedded into the unlock screen
            # dialog, and the embedding command is different from onboard, ask
            # the user what to do
            elif not config.is_onboard_in_xembed_command_string():
                question = _("Onboard is configured to appear with the dialog to "
                             "unlock the screen; for example to dismiss the "
                             "password-protected screensaver.\n\n"
                             "However the system is not configured anymore to use "
                             "Onboard to unlock the screen. A possible reason can "
                             "be that another application configured the system to "
                             "use something else.\n\n"
                             "Would you like to reconfigure the system to show "
                             "Onboard when unlocking the screen?")
                _logger.warning("showing dialog: '{}'".format(question))
                reply = show_confirmation_dialog(question,
                                                 self._window,
                                                 config.is_force_to_top())
                if reply == True:
                    config.enable_gss_embedding(True)
                else:
                    config.onboard_xembed_enabled = False
            else:
                if not config.gss.embedded_keyboard_enabled:
                    question = _("Onboard is configured to appear with the dialog "
                                 "to unlock the screen; for example to dismiss "
                                 "the password-protected screensaver.\n\n"
                                 "However this function is disabled in the system.\n\n"
                                 "Would you like to activate it?")
                    _logger.warning("showing dialog: '{}'".format(question))
                    reply = show_confirmation_dialog(question,
                                                     self._window,
                                                     config.is_force_to_top())
                    if reply == True:
                        config.enable_gss_embedding(True)
                    else:
                        config.onboard_xembed_enabled = False

        # check if gnome accessibility is enabled for auto-show
        if (config.is_auto_show_enabled() or \
            config.are_word_suggestions_enabled()) and \
            not config.check_gnome_accessibility(self._window):
            config.auto_show.enabled = False
class AtspiStateTracker(EventSource):
    """
    Keeps track of the currently active accessible by listening
    to AT-SPI focus events.
    """

    _focus_event_names = ("text-entry-activated", )
    _text_event_names = ("text-changed", "text-caret-moved")
    _key_stroke_event_names = ("key-pressed", )
    _async_event_names = ("async-focus-changed", "async-text-changed",
                          "async-text-caret-moved")
    _event_names = (_async_event_names + _focus_event_names +
                    _text_event_names + _key_stroke_event_names)

    _focus_listeners_registered = False
    _keystroke_listeners_registered = False
    _text_listeners_registered = False

    _keystroke_listener = None

    # asynchronously accessible members
    _focused_accessible = None  # last focused editable accessible
    _focused_pid = None  # pid of last focused editable accessible
    _active_accessible = None  # currently active editable accessible
    _active_accessible_activation_time = 0.0  # time since focus received
    _last_active_accessible = None

    _poll_unity_timer = Timer()

    def __new__(cls, *args, **kwargs):
        """
        Singleton magic.
        """
        if not hasattr(cls, "self"):
            cls.self = object.__new__(cls, *args, **kwargs)
            cls.self.construct()
        return cls.self

    def __init__(self):
        """
        Called multiple times, don't use this.
        """
        pass

    def construct(self):
        """
        Singleton constructor, runs only once.
        """
        EventSource.__init__(self, self._event_names)

        self._frozen = False

    def cleanup(self):
        EventSource.cleanup(self)
        self._register_atspi_listeners(False)

    def connect(self, event_name, callback):
        EventSource.connect(self, event_name, callback)
        self._update_listeners()

    def disconnect(self, event_name, callback):
        had_listeners = self.has_listeners(self._event_names)

        EventSource.disconnect(self, event_name, callback)
        self._update_listeners()

        # help debugging disconnecting events on exit
        if had_listeners and not self.has_listeners(self._event_names):
            _logger.info("all listeners disconnected")

    def _update_listeners(self):
        register = self.has_listeners(self._focus_event_names)
        self._register_atspi_focus_listeners(register)

        register = self.has_listeners(self._text_event_names)
        self._register_atspi_text_listeners(register)

        register = self.has_listeners(self._key_stroke_event_names)
        self._register_atspi_keystroke_listeners(register)

    def _register_atspi_listeners(self, register):
        self._register_atspi_focus_listeners(register)
        self._register_atspi_text_listeners(register)
        self._register_atspi_keystroke_listeners(register)

    def _register_atspi_focus_listeners(self, register):
        if "Atspi" not in globals():
            return

        if self._focus_listeners_registered != register:

            if register:
                self.atspi_connect("_listener_focus", "focus",
                                   self._on_atspi_global_focus)
                self.atspi_connect("_listener_object_focus",
                                   "object:state-changed:focused",
                                   self._on_atspi_object_focus)

                # private asynchronous events
                for name in self._async_event_names:
                    handler = "_on_" + name.replace("-", "_")
                    EventSource.connect(self, name, getattr(self, handler))
            else:
                self._poll_unity_timer.stop()

                self.atspi_disconnect("_listener_focus", "focus")
                self.atspi_disconnect("_listener_object_focus",
                                      "object:state-changed:focused")

                for name in self._async_event_names:
                    handler = "_on_" + name.replace("-", "_")
                    EventSource.disconnect(self, name, getattr(self, handler))

            self._focus_listeners_registered = register

    def _register_atspi_text_listeners(self, register):
        if "Atspi" not in globals():
            return

        if self._text_listeners_registered != register:
            if register:
                self.atspi_connect("_listener_text_changed",
                                   "object:text-changed:insert",
                                   self._on_atspi_text_changed)
                self.atspi_connect("_listener_text_changed",
                                   "object:text-changed:delete",
                                   self._on_atspi_text_changed)
                self.atspi_connect("_listener_text_caret_moved",
                                   "object:text-caret-moved",
                                   self._on_atspi_text_caret_moved)
            else:
                self.atspi_disconnect("_listener_text_changed",
                                      "object:text-changed:insert")
                self.atspi_disconnect("_listener_text_changed",
                                      "object:text-changed:delete")
                self.atspi_disconnect("_listener_text_caret_moved",
                                      "object:text-caret-moved")

        self._text_listeners_registered = register

    def _register_atspi_keystroke_listeners(self, register):
        if "Atspi" not in globals():
            return

        if self._keystroke_listeners_registered != register:
            modifier_masks = range(16)

            if register:
                if not self._keystroke_listener:
                    self._keystroke_listener = \
                        Atspi.DeviceListener.new(self._on_atspi_keystroke,
                                                 None)

                for modifier_mask in modifier_masks:
                    Atspi.register_keystroke_listener(
                        self._keystroke_listener,
                        None,  # key set, None=all
                        modifier_mask,
                        Atspi.KeyEventType.PRESSED,
                        Atspi.KeyListenerSyncType.SYNCHRONOUS)
            else:
                # Apparently any single deregister call will turn off
                # all the other registered modifier_masks too. Since
                # deregistering takes extremely long (~2.5s for 16 calls)
                # seize the opportunity and just pick a single arbitrary
                # mask (Quantal).
                modifier_masks = [2]

                for modifier_mask in modifier_masks:
                    Atspi.deregister_keystroke_listener(
                        self._keystroke_listener,
                        None,  # key set, None=all
                        modifier_mask,
                        Atspi.KeyEventType.PRESSED)

        self._keystroke_listeners_registered = register

    def atspi_connect(self, attribute, event, callback):
        """
        Start listening to an AT-SPI event.
        Creates a new event listener for each event, since this seems
        to be the only way to allow reliable deregistering of events.
        """
        if hasattr(self, attribute):
            listener = getattr(self, attribute)
        else:
            listener = None

        if listener is None:
            listener = Atspi.EventListener.new(callback, None)
            setattr(self, attribute, listener)
        listener.register(event)

    def atspi_disconnect(self, attribute, event):
        """
        Stop listening to AT-SPI event.
        """
        listener = getattr(self, attribute)
        listener.deregister(event)

    def freeze(self):
        """
        Freeze AT-SPI message processing, e.g. while displaying
        a dialog or popoup menu.
        """
        self._register_atspi_listeners(False)
        self._frozen = True

    def thaw(self):
        """
        Resume AT-SPI message processing.
        """
        self._update_listeners()
        self._frozen = False

    def emit_async(self, event_name, *args, **kwargs):
        if not self._frozen:
            EventSource.emit_async(self, event_name, *args, **kwargs)

    def _get_cached_accessible(self, accessible):
        return CachedAccessible(accessible) \
            if accessible else None

    # ######### synchronous handlers ######### #

    def _on_atspi_global_focus(self, event, user_data):
        self._on_atspi_focus(event, True)

    def _on_atspi_object_focus(self, event, user_data):
        self._on_atspi_focus(event)

    def _on_atspi_focus(self, event, focus_received=False):
        focused = (bool(focus_received)
                   or bool(event.detail1))  # received focus?
        ae = AsyncEvent(accessible=self._get_cached_accessible(event.source),
                        focused=focused)
        self.emit_async("async-focus-changed", ae)

    def _on_atspi_text_changed(self, event, user_data):
        # print("_on_atspi_text_changed", event.detail1, event.detail2,
        #       event.source, event.type, event.type.endswith("delete"))
        ae = AsyncEvent(accessible=self._get_cached_accessible(event.source),
                        type=event.type,
                        pos=event.detail1,
                        length=event.detail2)
        self.emit_async("async-text-changed", ae)
        return False

    def _on_atspi_text_caret_moved(self, event, user_data):
        # print("_on_atspi_text_caret_moved", event.detail1, event.detail2,
        #       event.source, event.type, event.source.get_name(),
        #       event.source.get_role())
        ae = AsyncEvent(accessible=self._get_cached_accessible(event.source),
                        caret=event.detail1)
        self.emit_async("async-text-caret-moved", ae)
        return False

    def _on_atspi_keystroke(self, event, user_data):
        if event.type == Atspi.EventType.KEY_PRESSED_EVENT:
            _logger.atspi("key-stroke {} {} {} {}".format(
                event.modifiers, event.hw_code, event.id, event.is_text))
            # keysym = event.id # What is this? Not an XK_ keysym apparently.
            ae = AsyncEvent(hw_code=event.hw_code, modifiers=event.modifiers)
            self.emit_async("key-pressed", ae)

        return False  # don't consume event

    # ######### asynchronous handlers ######### #
    def _on_async_focus_changed(self, event):
        accessible = event.accessible
        focused = event.focused

        # Don't access the accessible while frozen. This leads to deadlocks
        # while displaying Onboard's own dialogs/popup menu's.
        if self._frozen:
            return

        self._log_accessible(accessible, focused)

        if not accessible:
            return

        app_name = accessible.get_app_name().lower()
        if app_name == "unity":
            self._handle_focus_changed_unity(event)
        else:
            self._handle_focus_changed_apps(event)

    def _handle_focus_changed_apps(self, event):
        """ Focus change in regular applications """
        accessible = event.accessible
        focused = event.focused

        # Since Trusty, focus events no longer come reliably in a
        # predictable order. -> Store the last editable accessible
        # so we can pick it over later focused non-editable ones.
        # Helps to keep the keyboard open in presence of popup selections
        # e.g. in GNOME's file dialog and in Unity Dash.
        if self._focused_accessible == accessible:
            if not focused:
                self._focused_accessible = None
        else:
            pid = accessible.get_pid()

            if focused:
                self._poll_unity_timer.stop()

                if accessible.is_editable():
                    self._focused_accessible = accessible
                    self._focused_pid = pid

                # Static accessible, i.e. something that cannot
                # accidentally steal the focus from an editable
                # accessible. e.g. firefox ATSPI_ROLE_DOCUMENT_FRAME?
                elif accessible.is_not_focus_stealing():
                    self._focused_accessible = None
                    self._focused_pid = None

                else:
                    # Wily: attempt to hide when unity dash closes
                    # (there's no focus lost event).
                    # Also check duration since last activation to
                    # skip out of order focus events (firefox
                    # ATSPI_ROLE_DOCUMENT_FRAME) for a short while
                    # after opening dash.
                    now = time.time()
                    if focused and \
                       now - self._active_accessible_activation_time > .5:
                        if self._focused_pid != pid:
                            self._focused_accessible = None
                            _logger.atspi("Dropping accessible due to "
                                          "pid change: {} != {} ".format(
                                              self._focused_pid, pid))

        # Has the previously focused accessible lost the focus?
        active_accessible = self._focused_accessible
        if active_accessible and \
           not active_accessible.is_focused(True):

            # Zesty: Firefox 50+ loses focus of the URL entry after
            # typing just a few letters and focuses a completion
            # menu item instead. Let's pretend the accessible is
            # still focused in that case.
            is_firefox_completion = \
                self._focused_accessible.is_urlbar() and \
                accessible.get_role() == Atspi.Role.MENU_ITEM

            if not is_firefox_completion:
                active_accessible = None

        self._set_active_accessible(active_accessible)

    def _handle_focus_changed_unity(self, event):
        """ Focus change in Unity Dash """
        accessible = event.accessible
        focused = event.focused

        # Wily: prevent random icons, buttons and toolbars
        # in unity dash from hiding Onboard. Somehow hovering
        # over those buttons silently drops the focus from the
        # text entry. Let's pretend the buttons don't exist
        # and keep the previously saved text entry active.

        # Zesty: Don't fight lost focus events anymore, only
        # react to focus events when the text entry gains focus.
        if focused and \
           accessible.is_editable():
            self._focused_accessible = accessible
            self._set_active_accessible(accessible)

            # For hiding we poll Dash's toplevel accessible
            def _poll_unity_dash():
                frame = accessible.get_frame()
                state_set = frame.get_state_set()

                _logger.debug("polling unity dash state_set: {}".format(
                    AtspiStateType.to_strings(state_set)))

                if not state_set or \
                   not state_set.contains(Atspi.StateType.ACTIVE):
                    self._focused_accessible = None
                    self._set_active_accessible(None)
                    return False

                return True

            # Only ever start polling if Dash is "ACTIVE".
            # The state_set might change in the future and the
            # keyboard better fail to auto-hide than to never show.
            frame = accessible.get_frame()
            state_set = frame.get_state_set()

            _logger.debug("dash focused, state_set: {}".format(
                AtspiStateType.to_strings(state_set)))

            if state_set and \
               state_set.contains(Atspi.StateType.ACTIVE):
                self._poll_unity_timer.start(0.5, _poll_unity_dash)

    def _set_active_accessible(self, accessible):
        if self._active_accessible != accessible:
            self._active_accessible = accessible

            if self._active_accessible or \
               self._last_active_accessible:

                # notify listeners
                self.emit("text-entry-activated", self._active_accessible)

                self._last_active_accessible = self._active_accessible
                self._active_accessible_activation_time = time.time()

    def _on_async_text_changed(self, event):
        if event.accessible == self._active_accessible:
            type = event.type
            insert = type.endswith(("insert", "insert:system"))
            delete = type.endswith(("delete", "delete:system"))
            # print(event.accessible.get_id(), type, insert)
            if insert or delete:
                event.insert = insert
                self.emit("text-changed", event)
            else:
                _logger.warning("_on_async_text_changed: "
                                "unknown event type '{}'".format(event.type))

    def _on_async_text_caret_moved(self, event):
        if event.accessible == self._active_accessible:
            self.emit("text-caret-moved", event)

    def _log_accessible(self, accessible, focused):
        if _logger.isEnabledFor(_logger.LEVEL_ATSPI):
            msg = "AT-SPI focus event: focused={}, ".format(focused)
            msg += "accessible={}, ".format(accessible)

            if accessible:
                name = accessible.get_name()
                role = accessible.get_role()
                role_name = accessible.get_role_name()
                state_set = accessible.get_state_set()
                states = state_set.states
                editable = state_set.contains(Atspi.StateType.EDITABLE) \
                    if state_set else None
                extents = accessible.get_extents()

                msg += "name={name}, role={role}({role_name}), " \
                       "editable={editable}, states={states}, " \
                       "extents={extents}]" \
                       .format(accessible=accessible, name=repr(name),
                               role=role.value_name if role else role,
                               role_name=repr(role_name),
                               editable=editable,
                               states=states,
                               extents=extents
                               )
            _logger.atspi(msg)
Beispiel #18
0
class ScanMode(Timer):
    """
    Abstract base class for all scanning modes.

    Specifies how the scanner moves between chunks of keys
    and when to activate them. Scan mode subclasses define
    a set of actions they support and the base class translates
    input device events into scan actions.

    Hierarchy:
        ScanMode --> AutoScan --> UserScan
                              --> OverScan
                 --> StepScan
                 --> DirectScan
    """
    """ Scan actions """
    ACTION_STEP = 0
    ACTION_LEFT = 1
    ACTION_RIGHT = 2
    ACTION_UP = 3
    ACTION_DOWN = 4
    ACTION_ACTIVATE = 5
    ACTION_STEP_START = 6
    ACTION_STEP_STOP = 7
    ACTION_UNHANDLED = 8
    """ Time between key activation flashes (in sec) """
    ACTIVATION_FLASH_INTERVAL = 0.1
    """ Number of key activation flashes """
    ACTIVATION_FLASH_COUNT = 4

    def __init__(self, redraw_callback, activate_callback):
        super(ScanMode, self).__init__()

        logger.debug("ScanMode.__init__()")
        """ Activation timer instance """
        self._activation_timer = Timer()
        """ Counter for key flash animation """
        self._flash = 0
        """ Callback for key redraws """
        self._redraw_callback = redraw_callback
        """ Callback for key activation """
        self._activate_callback = activate_callback
        """ A Chunker instance """
        self.chunker = None

    def __del__(self):
        logger.debug("ScanMode.__del__()")

    def map_actions(self, detail, pressed):
        """
        Abstract: Convert input events into scan actions.
        """
        raise NotImplementedError()

    def do_action(self, action):
        """
        Abstract: Handle scan actions.
        """
        raise NotImplementedError()

    def scan(self):
        """
        Abstract: Move between chunks.
        """
        raise NotImplementedError()

    def create_chunker(self):
        """
        Abstract: Create a chunker instance.
        """
        raise NotImplementedError()

    def init_position(self):
        """
        Virtual: Called if a new layer was set or a key activated.
        """
        pass

    def handle_event(self, event):
        """
        Translate device events into scan actions.
        """
        # Ignore events during key activation
        if self._activation_timer.is_running():
            return

        event_type = event.xi_type
        if event_type == XIEventType.ButtonPress:
            button_map = config.scanner.device_button_map
            action = self.map_actions(button_map, event.button, True)

        elif event_type == XIEventType.ButtonRelease:
            button_map = config.scanner.device_button_map
            action = self.map_actions(button_map, event.button, False)

        elif event_type == XIEventType.KeyPress:
            key_map = config.scanner.device_key_map
            action = self.map_actions(key_map, event.keyval, True)

        elif event_type == XIEventType.KeyRelease:
            key_map = config.scanner.device_key_map
            action = self.map_actions(key_map, event.keyval, False)

        else:
            action = self.ACTION_UNHANDLED

        if action != self.ACTION_UNHANDLED:
            self.do_action(action)

    def on_timer(self):
        """
        Override: Timer() callback.
        """
        return self.scan()

    def max_cycles_reached(self):
        """
        Check if the maximum number of scan cycles is reached.
        """
        return self.chunker.cycles >= config.scanner.cycles

    def set_layer(self, layout, layer):
        """
        Set the layer that should be scanned.
        """
        self.reset()
        self.chunker = self.create_chunker()
        self.chunker.chunk(layout, layer)
        self.init_position()

    def _on_activation_timer(self, key):
        """
        Timer callback: Flashes the key and finally activates it.
        """
        if self._flash > 0:
            key.scanned = not key.scanned
            self._flash -= 1
            self.redraw([key])
            return True
        else:
            self._activate_callback(key)
            self.init_position()
            return False

    def activate(self):
        """
        Activates a key and triggers feedback.
        """
        key = self.chunker.get_key()
        if not key:
            return

        if config.scanner.feedback_flash:
            self._flash = self.ACTIVATION_FLASH_COUNT
            self._activation_timer.start(self.ACTIVATION_FLASH_INTERVAL,
                                         self._on_activation_timer, key)
        else:
            self._activate_callback(key)
            self.init_position()

    def reset(self):
        """
        Stop scanning and clear all highlights.
        """
        if self.is_running():
            self.stop()

        if self.chunker:
            self.redraw(self.chunker.highlight_all(False))

    def redraw(self, keys=None):
        """
        Update individual keys or the entire keyboard.
        """
        self._redraw_callback(keys)

    def finalize(self):
        """
        Clean up the ScanMode instance.
        """
        self.reset()
        self._activation_timer = None
Beispiel #19
0
class IconPalette(WindowRectPersist, WindowManipulator, Gtk.Window):
    """
    Class that creates a movable and resizable floating window without
    decorations. The window shows the icon of onboard scaled to fit to the
    window and a resize grip that honors the desktop theme in use.

    Onboard offers an option to the user to make the window appear
    whenever the user hides the onscreen keyboard. The user can then
    click on the window to hide it and make the onscreen keyboard
    reappear.
    """

    __gsignals__ = {
        str('activated') : (GObject.SignalFlags.RUN_LAST,
                            GObject.TYPE_NONE, ())
    }

    """ Minimum size of the IconPalette """
    MINIMUM_SIZE = 20

    OPACITY = 0.75

    _layout_view = None

    def __init__(self, keyboard):
        self._visible = False
        self._force_to_top = False
        self._last_pos = None

        self._dwell_progress = DwellProgress()
        self._dwell_begin_timer = None
        self._dwell_timer = None
        self._no_more_dwelling = False

        self._menu = ContextMenu(keyboard)

        args = {
            "type_hint" : self._get_window_type_hint(),
            "skip_taskbar_hint" : True,
            "skip_pager_hint" : True,
            "urgency_hint" : False,
            "decorated" : False,
            "accept_focus" : False,
            "width_request" : self.MINIMUM_SIZE,
            "height_request" : self.MINIMUM_SIZE,
            "app_paintable" : True,
        }
        if gtk_has_resize_grip_support():
            args["has_resize_grip"] = False

        Gtk.Window.__init__(self, **args)

        WindowRectPersist.__init__(self)
        WindowManipulator.__init__(self)

        self.set_keep_above(True)

        self.drawing_area = Gtk.DrawingArea()
        self.add(self.drawing_area)
        self.drawing_area.connect("draw", self._on_draw)
        self.drawing_area.show()

        # use transparency if available
        visual = Gdk.Screen.get_default().get_rgba_visual()
        if visual:
            self.set_visual(visual)
            self.drawing_area.set_visual(visual)

        # set up event handling
        self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK |
                        Gdk.EventMask.BUTTON_RELEASE_MASK |
                        Gdk.EventMask.POINTER_MOTION_MASK)

        # self.connect("draw", self._on_draw)
        self.connect("button-press-event", self._on_button_press_event)
        self.connect("motion-notify-event", self._on_motion_notify_event)
        self.connect("button-release-event", self._on_button_release_event)
        self.connect("configure-event", self._on_configure_event)
        self.connect("realize", self._on_realize_event)
        self.connect("unrealize", self._on_unrealize_event)
        self.connect("enter-notify-event", self._on_mouse_enter)
        self.connect("leave-notify-event", self._on_mouse_leave)

        # default coordinates of the iconpalette on the screen
        self.set_min_window_size(self.MINIMUM_SIZE, self.MINIMUM_SIZE)
        # no flashing on left screen edge in unity
        # self.set_default_size(1, 1)
        self.restore_window_rect()

        # Realize the window. Test changes to this in all supported
        # environments. It's all too easy to have the icp not show up reliably.
        self.update_window_options()
        self.hide()

        once = CallOnce(100).enqueue  # call at most once per 100ms
        config.icp.position_notify_add(
            lambda x: once(self._on_config_rect_changed))
        config.icp.size_notify_add(
            lambda x: once(self._on_config_rect_changed))
        config.icp.window_handles_notify_add(
            lambda x: self.update_window_handles())

        self.update_sticky_state()
        self.update_window_handles()

    def cleanup(self):
        WindowRectPersist.cleanup(self)

    def set_layout_view(self, view):
        self._layout_view = view
        self.queue_draw()

    def get_color_scheme(self):
        if self._layout_view:
            return self._layout_view.get_color_scheme()
        return None

    def get_menu(self):
        return self._menu

    def _on_configure_event(self, widget, event):
        self.update_window_rect()

    def on_drag_initiated(self):
        self.stop_save_position_timer()
        self._stop_dwelling()

    def on_drag_done(self):
        self.update_window_rect()
        self.start_save_position_timer()
        self._no_more_dwelling = True

    def _on_realize_event(self, user_data):
        """ Gdk window created """
        set_unity_property(self)
        if config.is_force_to_top():
            self.set_override_redirect(True)
        self.restore_window_rect(True)

    def _on_unrealize_event(self, user_data):
        """ Gdk window destroyed """
        self.set_type_hint(self._get_window_type_hint())

    def _get_window_type_hint(self):
        if config.is_force_to_top():
            return Gdk.WindowTypeHint.NORMAL
        else:
            return Gdk.WindowTypeHint.UTILITY

    def update_window_options(self, startup=False):
        if not config.xid_mode:   # not when embedding

            # (re-)create the gdk window?
            force_to_top = config.is_force_to_top()

            if self._force_to_top != force_to_top:
                self._force_to_top = force_to_top

                visible = self._visible  # visible before?

                if self.get_realized():  # not starting up?
                    self.hide()
                    self.unrealize()

                self.realize()

                if visible:
                    self.show()

    def update_sticky_state(self):
        if not config.xid_mode:
            if config.get_sticky_state():
                self.stick()
            else:
                self.unstick()

    def update_window_handles(self):
        """ Tell WindowManipulator about the active resize/move handles """
        self.set_drag_handles(config.icp.window_handles)

    def get_drag_threshold(self):
        """ Overload for WindowManipulator """
        return config.get_drag_threshold()

    def _on_button_press_event(self, widget, event):
        if event.window == self.get_window():
            if Gdk.Event.triggers_context_menu(event):
                self._menu.popup(event.button, event.get_time())

            elif event.button == Gdk.BUTTON_PRIMARY:
                self.enable_drag_protection(True)
                sequence = InputSequence()
                sequence.init_from_button_event(event)
                self.handle_press(sequence, move_on_background=True)
                if self.is_moving():
                    self.reset_drag_protection()  # force threshold

        return True

    def _on_motion_notify_event(self, widget, event):
        """
        Move the window if the pointer has moved more than the DND threshold.
        """
        sequence = InputSequence()
        sequence.init_from_motion_event(event)
        self.handle_motion(sequence, fallback=True)
        self.set_drag_cursor_at((event.x, event.y))

        # start dwelling if nothing else is going on
        point = (event.x, event.y)
        hit = self.hit_test_move_resize(point)
        if hit is None:
            if not self.is_drag_initiated() and \
               not self._is_dwelling() and \
               not self._no_more_dwelling and \
               not config.is_hover_click_active() and \
               not config.lockdown.disable_dwell_activation:
                self._start_dwelling()
        else:
            self._stop_dwelling()  # allow resizing in peace

        return True

    def _on_button_release_event(self, widget, event):
        """
        Save the window geometry, hide the IconPalette and
        emit the "activated" signal.
        """
        if event.window == self.get_window():
            if event.button == 1 and \
               event.window == self.get_window() and \
               not self.is_drag_active():
                self.emit("activated")

            self.stop_drag()
            self.set_drag_cursor_at((event.x, event.y))

        return True

    def _on_mouse_enter(self, widget, event):
        pass

    def _on_mouse_leave(self, widget, event):
        self._stop_dwelling()
        self._no_more_dwelling = False

    def _on_draw(self, widget, cr):
        """
        Draw the onboard icon.
        """
        if not Gtk.cairo_should_draw_window(cr, widget.get_window()):
            return False

        rect = Rect(0.0, 0.0,
                    float(self.get_allocated_width()),
                    float(self.get_allocated_height()))
        color_scheme = self.get_color_scheme()

        # clear background
        cr.save()
        cr.set_operator(cairo.OPERATOR_CLEAR)
        cr.paint()
        cr.restore()

        composited = Gdk.Screen.get_default().is_composited()
        if composited:
            cr.push_group()

        # draw background color
        background_rgba = list(color_scheme.get_icon_rgba("background"))

        if Gdk.Screen.get_default().is_composited():
            background_rgba[3] *= 0.75
            cr.set_source_rgba(*background_rgba)

            corner_radius = min(rect.w, rect.h) * 0.1

            roundrect_arc(cr, rect, corner_radius)
            cr.fill()

            # decoration frame
            line_rect = rect.deflate(2)
            cr.set_line_width(2)
            roundrect_arc(cr, line_rect, corner_radius)
            cr.stroke()
        else:
            cr.set_source_rgba(*background_rgba)
            cr.paint()

        # draw themed icon
        self._draw_themed_icon(cr, rect, color_scheme)

        # draw dwell progress
        rgba = [0.8, 0.0, 0.0, 0.5]
        bg_rgba = [0.1, 0.1, 0.1, 0.5]
        if color_scheme:
            # take dwell color from the first icon "key"
            key  = RectKey("icon0")
            rgba = color_scheme.get_key_rgba(key, "dwell-progress")
            rgba[3] = min(0.75, rgba[3])  # more transparency

            key  = RectKey("icon1")
            bg_rgba = color_scheme.get_key_rgba(key, "fill")
            bg_rgba[3] = min(0.75, rgba[3])  # more transparency

        dwell_rect = rect.grow(0.5)
        self._dwell_progress.draw(cr, dwell_rect, rgba, bg_rgba)

        if composited:
            cr.pop_group_to_source()
            cr.paint_with_alpha(self.OPACITY)

        return True

    def _draw_themed_icon(self, cr, icon_rect, color_scheme):
        """ draw themed icon """
        keys = [RectKey("icon" + str(i)) for i in range(4)]

        # Default colors for the case when none of the icon keys
        # are defined in the color scheme.
        # background_rgba =  [1.0, 1.0, 1.0, 1.0]
        fill_rgbas      = [[0.9, 0.7, 0.0, 0.75],
                           [1.0, 1.0, 1.0, 1.0],
                           [1.0, 1.0, 1.0, 1.0],
                           [0.0, 0.54, 1.0, 1.0]]
        stroke_rgba     = [0.0, 0.0, 0.0, 1.0]
        label_rgba      = [0.0, 0.0, 0.0, 1.0]

        themed = False
        if color_scheme:
            if any(color_scheme.is_key_in_scheme(key) for key in keys):
                themed = True

        # four rounded rectangles
        rects = Rect(0.0, 0.0, 100.0, 100.0).deflate(5) \
                                            .subdivide(2, 2, 6)
        cr.save()
        cr.scale(icon_rect.w / 100., icon_rect.h / 100.0)
        cr.translate(icon_rect.x, icon_rect.y)
        cr.select_font_face("sans-serif")
        cr.set_line_width(2)

        for i, key in enumerate(keys):
            rect = rects[i]

            if themed:
                fill_rgba   = color_scheme.get_key_rgba(key, "fill")
                stroke_rgba = color_scheme.get_key_rgba(key, "stroke")
                label_rgba  = color_scheme.get_key_rgba(key, "label")
            else:
                fill_rgba   = fill_rgbas[i]

            roundrect_arc(cr, rect, 5)
            cr.set_source_rgba(*fill_rgba)
            cr.fill_preserve()

            cr.set_source_rgba(*stroke_rgba)
            cr.stroke()

            if i == 0 or i == 3:
                if i == 0:
                    letter = "O"
                else:
                    letter = "B"

                cr.set_font_size(25)
                (x_bearing, y_bearing, _width, _height,
                 x_advance, y_advance) = cr.text_extents(letter)
                r = rect.align_rect(Rect(0, 0, _width, _height),
                                    0.3, 0.33)
                cr.move_to(r.x - x_bearing, r.y - y_bearing)
                cr.set_source_rgba(*label_rgba)
                cr.show_text(letter)
                cr.new_path()

        cr.restore()

    def show(self):
        """
        Override Gtk.Widget.hide() to save the window geometry.
        """
        Gtk.Window.show_all(self)
        self.move_resize(*self.get_rect())  # sync with WindowRectTracker
        self._visible = True

    def hide(self):
        """
        Override Gtk.Widget.hide() to save the window geometry.
        """
        Gtk.Window.hide(self)
        self._visible = False

    def _on_config_rect_changed(self):
        """ Gsettings position or size changed """
        orientation = self.get_screen_orientation()
        rect = self.read_window_rect(orientation)
        if self.get_rect() != rect:
            self.restore_window_rect()

    def read_window_rect(self, orientation):
        """
        Read orientation dependent rect.
        Overload for WindowRectPersist.
        """
        if orientation == Orientation.LANDSCAPE:
            co = config.icp.landscape
        else:
            co = config.icp.portrait
        rect = Rect(co.x, co.y, co.width, co.height)
        return rect

    def write_window_rect(self, orientation, rect):
        """
        Write orientation dependent rect.
        Overload for WindowRectPersist.
        """
        # There are separate rects for normal and rotated screen (tablets).
        if orientation == Orientation.LANDSCAPE:
            co = config.icp.landscape
        else:
            co = config.icp.portrait

        co.delay()
        co.x, co.y, co.width, co.height = rect
        co.apply()

    def _is_dwelling(self):
        return (bool(self._dwell_begin_timer) and
                (self._dwell_begin_timer.is_running() or
                self._dwell_progress.is_dwelling()))

    def _start_dwelling(self):
        self._stop_dwelling()
        self._dwell_begin_timer = Timer(1.5, self._on_dwell_begin_timer)
        self._no_more_dwelling = True

    def _stop_dwelling(self):
        if self._dwell_begin_timer:
            self._dwell_begin_timer.stop()
            if self._dwell_timer:
                self._dwell_timer.stop()
                self._dwell_progress.stop_dwelling()
                self.queue_draw()

    def _on_dwell_begin_timer(self):
        self._dwell_progress.start_dwelling()
        self._dwell_timer = Timer(0.025, self._on_dwell_timer)
        return False

    def _on_dwell_timer(self):
        self._dwell_progress.opacity, done = \
            Fade.sin_fade(self._dwell_progress.dwell_start_time, 0.3, 0, 1.0)
        self.queue_draw()
        if self._dwell_progress.is_done():
            if not self.is_drag_active():
                self.emit("activated")
                self.stop_drag()
            return False
        return True
Beispiel #20
0
class LayoutPopup(KeyboardPopup, LayoutView, TouchInput):
    """ Popup showing a (sub-)layout tree. """

    IDLE_CLOSE_DELAY = 0  # seconds of inactivity until window closes

    def __init__(self, keyboard, notify_done_callback):
        self._layout = None
        self._notify_done_callback = notify_done_callback
        self._drag_selected = False  # grazed by the pointer?

        KeyboardPopup.__init__(self)
        LayoutView.__init__(self, keyboard)
        TouchInput.__init__(self)

        self.connect("destroy", self._on_destroy_event)

        self._close_timer = Timer()
        self.start_close_timer()

    def cleanup(self):
        self.stop_close_timer()

        # fix label popup staying visible on double click
        self.keyboard.hide_touch_feedback()

        LayoutView.cleanup(self)  # deregister from keyboard

    def get_toplevel(self):
        return self

    def set_layout(self, layout, frame_width):
        self._layout = layout
        self._frame_width = frame_width

        self.update_labels()

        # set window size
        layout_canvas_rect = layout.get_canvas_border_rect()
        canvas_rect = layout_canvas_rect.inflate(frame_width)
        w, h = canvas_rect.get_size()
        self.set_default_size(w + 1, h + 1)

    def get_layout(self):
        return self._layout

    def get_frame_width(self):
        return self._frame_width

    def got_motion(self):
        """ Has the pointer ever entered the popup? """
        return self._drag_selected

    def handle_realize_event(self):
        self.set_override_redirect(True)
        super(LayoutPopup, self).handle_realize_event()

    def _on_destroy_event(self, user_data):
        self.cleanup()

    def on_enter_notify(self, widget, event):
        self.stop_close_timer()

    def on_leave_notify(self, widget, event):
        self.start_close_timer()

    def on_input_sequence_begin(self, sequence):
        self.stop_close_timer()
        key = self.get_key_at_location(sequence.point)
        if key:
            sequence.active_key = key
            self.keyboard.key_down(key, self, sequence)

    def on_input_sequence_update(self, sequence):
        if sequence.state & BUTTON123_MASK:
            key = self.get_key_at_location(sequence.point)

            # drag-select new active key
            active_key = sequence.active_key
            if not active_key is key and \
               (active_key is None or not active_key.activated):
                sequence.active_key = key
                self.keyboard.key_up(active_key, self, sequence, False)
                self.keyboard.key_down(key, self, sequence, False)
                self._drag_selected = True

    def on_input_sequence_end(self, sequence):
        key = sequence.active_key
        if key:
            keyboard = self.keyboard
            keyboard.key_up(key, self, sequence)

        if key and \
           not self._drag_selected:
            Timer(config.UNPRESS_DELAY, self.close_window)
        else:
            self.close_window()

    def on_draw(self, widget, context):
        context.push_group()

        LayoutView.draw(self, widget, context)

        context.pop_group_to_source()
        context.paint_with_alpha(self._opacity)

    def draw_window_frame(self, context, lod):
        corner_radius = config.CORNER_RADIUS
        border_rgba = self.get_popup_window_rgba("border")
        alpha = border_rgba[3]

        colors = [
            [[0.5, 0.5, 0.5, alpha], 0, 1],
            [border_rgba, 1.5, 2.0],
        ]

        rect = Rect(0, 0, self.get_allocated_width(),
                    self.get_allocated_height())

        for rgba, pos, width in colors:
            r = rect.deflate(width)
            roundrect_arc(context, r, corner_radius)
            context.set_line_width(width)
            context.set_source_rgba(*rgba)
            context.stroke()

    def close_window(self):
        self._notify_done_callback()

    def start_close_timer(self):
        if self.IDLE_CLOSE_DELAY:
            self._close_timer.start(self.IDLE_CLOSE_DELAY, self.close_window)

    def stop_close_timer(self):
        self._close_timer.stop()
Beispiel #21
0
    def init(self):
        self.keyboard_state = None
        self.vk_timer = None
        self.reset_vk()
        self._connections = []
        self._window = None
        self.status_icon = None
        self.service_keyboard = None
        self._reload_layout_timer = Timer()

        # finish config initialization
        config.init()

        # Release pressed keys when onboard is killed.
        # Don't keep enter key stuck when being killed by lightdm.
        self._osk_util = osk.Util()
        self._osk_util.set_unix_signal_handler(signal.SIGTERM, self.on_sigterm)
        self._osk_util.set_unix_signal_handler(signal.SIGINT, self.on_sigint)

        sys.path.append(os.path.join(config.install_dir, 'scripts'))

        # Create the central keyboard model
        self.keyboard = Keyboard(self)

        # Create the initial keyboard widget
        # Care for toolkit independence only once there is another
        # supported one besides GTK.
        self.keyboard_widget = KeyboardWidget(self.keyboard)

        # create the main window
        if config.xid_mode:  # XEmbed mode for gnome-screensaver?
            # no icp, don't flash the icon palette in lightdm

            self._window = KbdPlugWindow(self.keyboard_widget)

            # write xid to stdout
            sys.stdout.write('%d\n' % self._window.get_id())
            sys.stdout.flush()
        else:
            icp = IconPalette(self.keyboard)
            icp.set_layout_view(self.keyboard_widget)
            icp.connect("activated", self._on_icon_palette_acticated)
            self.do_connect(icp.get_menu(), "quit-onboard",
                            lambda x: self.do_quit_onboard())

            self._window = KbdWindow(self.keyboard_widget, icp)
            self.do_connect(self._window, "quit-onboard",
                            lambda x: self.do_quit_onboard())

        # config.xid_mode = True
        self._window.application = self
        # need this to access screen properties
        config.main_window = self._window

        # load the initial layout
        _logger.info("Loading initial layout")
        self.reload_layout()

        # Handle command line options x, y, size after window creation
        # because the rotation code needs the window's screen.
        if not config.xid_mode:
            rect = self._window.get_rect().copy()
            options = config.options
            if options.size:
                size = options.size.split("x")
                rect.w = int(size[0])
                rect.h = int(size[1])
            if options.x is not None:
                rect.x = options.x
            if options.y is not None:
                rect.y = options.y

            # Make sure the keyboard fits on screen
            rect = self._window.limit_size(rect)

            if rect != self._window.get_rect():
                orientation = self._window.get_screen_orientation()
                self._window.write_window_rect(orientation, rect)
                self._window.restore_window_rect()  # move/resize early

        # export dbus service
        if not config.xid_mode and \
           "dbus" in globals():
            self.service_keyboard = ServiceOnboardKeyboard(self)

        # show/hide the window
        self.keyboard_widget.set_startup_visibility()

        # keep keyboard window and icon palette on top of dash
        self._keep_windows_on_top()

        # connect notifications for keyboard map and group changes
        self.keymap = Gdk.Keymap.get_default()
        # map changes
        self.do_connect(self.keymap, "keys-changed", self.cb_keys_changed)
        self.do_connect(self.keymap, "state-changed", self.cb_state_changed)
        # group changes
        Gdk.event_handler_set(cb_any_event, self)

        # connect config notifications here to keep config from holding
        # references to keyboard objects.
        once = CallOnce(50).enqueue  # delay callbacks by 50ms
        reload_layout = lambda x: once(self.reload_layout_and_present)
        update_ui = lambda x: once(self._update_ui)
        update_ui_no_resize = lambda x: once(self._update_ui_no_resize)
        update_transparency = \
            lambda x: once(self.keyboard_widget.update_transparency)
        update_inactive_transparency = \
            lambda x: once(self.keyboard_widget.update_inactive_transparency)

        # general
        config.auto_show.enabled_notify_add(
            lambda x: self.keyboard.update_auto_show())
        config.auto_show.hide_on_key_press_notify_add(
            lambda x: self.keyboard.update_auto_hide())

        # keyboard
        config.keyboard.key_synth_notify_add(reload_layout)
        config.keyboard.input_event_source_notify_add(
            lambda x: self.keyboard.update_input_event_source())
        config.keyboard.touch_input_notify_add(
            lambda x: self.keyboard.update_touch_input_mode())
        config.keyboard.show_secondary_labels_notify_add(update_ui)

        # window
        config.window.window_state_sticky_notify_add(
            lambda x: self._window.update_sticky_state())
        config.window.window_decoration_notify_add(
            self._on_window_options_changed)
        config.window.force_to_top_notify_add(self._on_window_options_changed)
        config.window.keep_aspect_ratio_notify_add(update_ui)

        config.window.transparency_notify_add(update_transparency)
        config.window.background_transparency_notify_add(update_transparency)
        config.window.transparent_background_notify_add(update_ui)
        config.window.enable_inactive_transparency_notify_add(
            update_transparency)
        config.window.inactive_transparency_notify_add(
            update_inactive_transparency)
        config.window.docking_notify_add(self._update_docking)

        # layout
        config.layout_filename_notify_add(reload_layout)

        # theme
        # config.gdi.gtk_theme_notify_add(self.on_gtk_theme_changed)
        config.theme_notify_add(self.on_theme_changed)
        config.key_label_font_notify_add(reload_layout)
        config.key_label_overrides_notify_add(reload_layout)
        config.theme_settings.color_scheme_filename_notify_add(reload_layout)
        config.theme_settings.key_label_font_notify_add(reload_layout)
        config.theme_settings.key_label_overrides_notify_add(reload_layout)
        config.theme_settings.theme_attributes_notify_add(update_ui)

        # snippets
        config.snippets_notify_add(reload_layout)

        # word suggestions
        config.word_suggestions.show_context_line_notify_add(update_ui)
        config.word_suggestions.enabled_notify_add(
            lambda x: self.keyboard.on_word_suggestions_enabled(x))
        config.word_suggestions.auto_learn_notify_add(update_ui_no_resize)
        config.typing_assistance.active_language_notify_add(lambda x: \
                                 self.keyboard.on_active_lang_id_changed())
        config.typing_assistance.spell_check_backend_notify_add(lambda x: \
                                 self.keyboard.on_spell_checker_changed())
        config.typing_assistance.auto_capitalization_notify_add(lambda x: \
                                 self.keyboard.on_word_suggestions_enabled(x))
        config.word_suggestions.spelling_suggestions_enabled_notify_add(lambda x: \
                                 self.keyboard.on_spell_checker_changed())
        config.word_suggestions.delayed_word_separators_enabled_notify_add(lambda x: \
                                 self.keyboard.on_punctuator_changed())
        config.word_suggestions.wordlist_buttons_notify_add(
            update_ui_no_resize)

        # universal access
        config.scanner.enabled_notify_add(self.keyboard._on_scanner_enabled)
        config.window.window_handles_notify_add(
            self._on_window_handles_changed)

        # misc
        config.keyboard.show_click_buttons_notify_add(update_ui)
        config.lockdown.lockdown_notify_add(update_ui)
        if config.mousetweaks:
            config.mousetweaks.state_notify_add(update_ui_no_resize)

        # create status icon
        self.status_icon = Indicator()
        self.status_icon.set_keyboard(self.keyboard)
        self.do_connect(self.status_icon.get_menu(), "quit-onboard",
                        lambda x: self.do_quit_onboard())

        # Callbacks to use when icp or status icon is toggled
        config.show_status_icon_notify_add(self.show_hide_status_icon)
        config.icp.in_use_notify_add(self.cb_icp_in_use_toggled)

        self.show_hide_status_icon(config.show_status_icon)

        # Minimize to IconPalette if running under GDM
        if 'RUNNING_UNDER_GDM' in os.environ:
            _logger.info("RUNNING_UNDER_GDM set, turning on icon palette")
            config.icp.in_use = True
            _logger.info("RUNNING_UNDER_GDM set, turning off indicator")
            config.show_status_icon = False

            # For some reason the new values don't arrive in gsettings when
            # running the unit test "test_running_in_live_cd_environment".
            # -> Force gsettings to apply them, that seems to do the trick.
            config.icp.apply()
            config.apply()

        # unity-2d needs the skip-task-bar hint set before the first mapping.
        self.show_hide_taskbar()

        # Check gnome-screen-saver integration
        # onboard_xembed_enabled                False True     True      True
        # config.gss.embedded_keyboard_enabled  any   False    any       False
        # config.gss.embedded_keyboard_command  any   empty    !=onboard ==onboard
        # Action:                               nop   enable   Question1 Question2
        #                                             silently
        if not config.xid_mode and \
           config.onboard_xembed_enabled:

            # If it appears, that nothing has touched the gss keys before,
            # silently enable gss integration with onboard.
            if not config.gss.embedded_keyboard_enabled and \
               not config.gss.embedded_keyboard_command:
                config.enable_gss_embedding(True)

            # If onboard is configured to be embedded into the unlock screen
            # dialog, and the embedding command is different from onboard, ask
            # the user what to do
            elif not config.is_onboard_in_xembed_command_string():
                question = _(
                    "Onboard is configured to appear with the dialog to "
                    "unlock the screen; for example to dismiss the "
                    "password-protected screensaver.\n\n"
                    "However the system is not configured anymore to use "
                    "Onboard to unlock the screen. A possible reason can "
                    "be that another application configured the system to "
                    "use something else.\n\n"
                    "Would you like to reconfigure the system to show "
                    "Onboard when unlocking the screen?")
                _logger.warning("showing dialog: '{}'".format(question))
                reply = show_confirmation_dialog(question, self._window,
                                                 config.is_force_to_top())
                if reply == True:
                    config.enable_gss_embedding(True)
                else:
                    config.onboard_xembed_enabled = False
            else:
                if not config.gss.embedded_keyboard_enabled:
                    question = _(
                        "Onboard is configured to appear with the dialog "
                        "to unlock the screen; for example to dismiss "
                        "the password-protected screensaver.\n\n"
                        "However this function is disabled in the system.\n\n"
                        "Would you like to activate it?")
                    _logger.warning("showing dialog: '{}'".format(question))
                    reply = show_confirmation_dialog(question, self._window,
                                                     config.is_force_to_top())
                    if reply == True:
                        config.enable_gss_embedding(True)
                    else:
                        config.onboard_xembed_enabled = False

        # check if gnome accessibility is enabled for auto-show
        if (config.is_auto_show_enabled() or \
            config.are_word_suggestions_enabled()) and \
            not config.check_gnome_accessibility(self._window):
            config.auto_show.enabled = False
Beispiel #22
0
class TouchInput(InputEventSource):
    """
    Unified handling of multi-touch sequences and conventional pointer input.
    """
    GESTURE_DETECTION_SPAN = 100  # [ms] until two finger tap&drag is detected
    GESTURE_DELAY_PAUSE = 3000  # [ms] Suspend delayed sequence begin for this
    # amount of time after the last key press.
    DELAY_SEQUENCE_BEGIN = True  # No delivery, i.e. no key-presses after

    # gesture detection, but delays press-down.

    def __init__(self):
        InputEventSource.__init__(self)

        self._input_sequences = {}

        self._touch_events_enabled = False
        self._multi_touch_enabled = False
        self._gestures_enabled = False

        self._last_event_was_touch = False
        self._last_sequence_time = 0

        self._gesture = NO_GESTURE
        self._gesture_begin_point = (0, 0)
        self._gesture_begin_time = 0
        self._gesture_detected = False
        self._gesture_cancelled = False
        self._num_tap_sequences = 0
        self._gesture_timer = Timer()

    def set_touch_input_mode(self, touch_input):
        """ Call this to enable single/multi-touch """
        self._touch_events_enabled = touch_input != TouchInputEnum.NONE
        self._multi_touch_enabled = touch_input == TouchInputEnum.MULTI
        self._gestures_enabled = self._touch_events_enabled
        if self._device_manager:
            self._device_manager.update_devices()  # reset touch_active

        _logger.debug("setting touch input mode {}: "
                      "touch_events_enabled={}, "
                      "multi_touch_enabled={}, "
                      "gestures_enabled={}" \
                      .format(touch_input,
                              self._touch_events_enabled,
                              self._multi_touch_enabled,
                              self._gestures_enabled))

    def has_input_sequences(self):
        """ Are any clicks/touches still ongoing? """
        return bool(self._input_sequences)

    def last_event_was_touch(self):
        """ Was there just a touch event? """
        return self._last_event_was_touch

    @staticmethod
    def _get_event_source(event):
        device = event.get_source_device()
        return device.get_source()

    def _can_handle_pointer_event(self, event):
        """
        Rely on pointer events? True for non-touch devices
        and wacom touch-screens with gestures enabled.
        """
        device = event.get_source_device()
        source = device.get_source()

        return not self._touch_events_enabled or \
               source != Gdk.InputSource.TOUCHSCREEN or \
               not self.is_device_touch_active(device)

    def _can_handle_touch_event(self, event):
        """
        Rely on touch events? True for touch devices
        and wacom touch-screens with gestures disabled.
        """
        return not self._can_handle_pointer_event(event)

    def _on_button_press_event(self, widget, event):
        self.log_event("_on_button_press_event1 {} {} {} ",
                       self._touch_events_enabled,
                       self._can_handle_pointer_event(event),
                       self._get_event_source(event))

        if not self._can_handle_pointer_event(event):
            self.log_event("_on_button_press_event2 abort")
            return

        # - Ignore double clicks (GDK_2BUTTON_PRESS),
        #   we're handling those ourselves.
        # - Ignore mouse wheel button events
        self.log_event("_on_button_press_event3 {} {}", event.type,
                       event.button)
        if event.type == Gdk.EventType.BUTTON_PRESS and \
           1 <= event.button <= 3:
            sequence = InputSequence()
            sequence.init_from_button_event(event)
            sequence.primary = True
            self._last_event_was_touch = False

            self.log_event("_on_button_press_event4")
            self._input_sequence_begin(sequence)

        return True

    def _on_button_release_event(self, widget, event):
        sequence = self._input_sequences.get(POINTER_SEQUENCE)
        self.log_event("_on_button_release_event", sequence)
        if not sequence is None:
            sequence.point = (event.x, event.y)
            sequence.root_point = (event.x_root, event.y_root)
            sequence.time = event.get_time()

            self._input_sequence_end(sequence)

        return True

    def _on_motion_event(self, widget, event):
        if not self._can_handle_pointer_event(event):
            return

        sequence = self._input_sequences.get(POINTER_SEQUENCE)
        if sequence is None and \
           not event.state & BUTTON123_MASK:
            sequence = InputSequence()
            sequence.primary = True

        if sequence:
            sequence.init_from_motion_event(event)

            self._last_event_was_touch = False
            self._input_sequence_update(sequence)

        return True

    def _on_enter_notify(self, widget, event):
        self.on_enter_notify(widget, event)
        return True

    def _on_leave_notify(self, widget, event):
        self.on_leave_notify(widget, event)
        return True

    def _on_touch_event(self, widget, event):
        self.log_event("_on_touch_event1 {}", self._get_event_source(event))

        event_type = event.type

        # Set source_device touch-active to block processing of pointer events.
        # "touch-screens" that don't send touch events will keep having pointer
        # events handled (Wacom devices with gestures enabled).
        # This assumes that for devices that emit both touch and pointer
        # events, the touch event comes first. Else there will be a dangling
        # touch sequence. _discard_stuck_input_sequences would clean that up,
        # but a key might get still get stuck in pressed state.
        device = event.get_source_device()
        self.set_device_touch_active(device)

        if not self._can_handle_touch_event(event):
            self.log_event("_on_touch_event2 abort")
            return

        touch = event.touch if hasattr(event, "touch") else event
        id = str(touch.sequence)
        self._last_event_was_touch = True

        if event_type == Gdk.EventType.TOUCH_BEGIN:
            sequence = InputSequence()
            sequence.init_from_touch_event(touch, id)
            if len(self._input_sequences) == 0:
                sequence.primary = True

            self._input_sequence_begin(sequence)

        elif event_type == Gdk.EventType.TOUCH_UPDATE:
            sequence = self._input_sequences.get(id)
            if not sequence is None:
                sequence.point = (touch.x, touch.y)
                sequence.root_point = (touch.x_root, touch.y_root)
                sequence.time = event.get_time()
                sequence.update_time = time.time()

                self._input_sequence_update(sequence)

        else:
            if event_type == Gdk.EventType.TOUCH_END:
                pass

            elif event_type == Gdk.EventType.TOUCH_CANCEL:
                pass

            sequence = self._input_sequences.get(id)
            if not sequence is None:
                sequence.time = event.get_time()
                self._input_sequence_end(sequence)

        return True

    def _input_sequence_begin(self, sequence):
        """ Button press/touch begin """
        self.log_event("_input_sequence_begin1 {}", sequence)
        self._gesture_sequence_begin(sequence)
        first_sequence = len(self._input_sequences) == 0

        if first_sequence or \
           self._multi_touch_enabled:
            self._input_sequences[sequence.id] = sequence

            if not self._gesture_detected:
                if first_sequence and \
                   self._multi_touch_enabled and \
                   self.DELAY_SEQUENCE_BEGIN and \
                   sequence.time - self._last_sequence_time > \
                                   self.GESTURE_DELAY_PAUSE and \
                   self.can_delay_sequence_begin(sequence): # ask Keyboard
                    # Delay the first tap; we may have to stop it
                    # from reaching the keyboard.
                    self._gesture_timer.start(
                        self.GESTURE_DETECTION_SPAN / 1000.0,
                        self.on_delayed_sequence_begin, sequence,
                        sequence.point)

                else:
                    # Tell the keyboard right away.
                    self.deliver_input_sequence_begin(sequence)

        self._last_sequence_time = sequence.time

    def can_delay_sequence_begin(self, sequence):
        """ Overloaded in LayoutView to veto delay for move buttons. """
        return True

    def on_delayed_sequence_begin(self, sequence, point):
        if not self._gesture_detected:  # work around race condition
            sequence.point = point  # return to the original begin point
            self.deliver_input_sequence_begin(sequence)
            self._gesture_cancelled = True
        return False

    def deliver_input_sequence_begin(self, sequence):
        self.log_event("deliver_input_sequence_begin {}", sequence)
        self.on_input_sequence_begin(sequence)
        sequence.delivered = True

    def _input_sequence_update(self, sequence):
        """ Pointer motion/touch update """
        self._gesture_sequence_update(sequence)
        if not sequence.state & BUTTON123_MASK or \
           not self.in_gesture_detection_delay(sequence):
            self._gesture_timer.finish()  # run delayed begin before update
            self.on_input_sequence_update(sequence)

    def _input_sequence_end(self, sequence):
        """ Button release/touch end """
        self.log_event("_input_sequence_end1 {}", sequence)
        self._gesture_sequence_end(sequence)
        self._gesture_timer.finish()  # run delayed begin before end
        if sequence.id in self._input_sequences:
            del self._input_sequences[sequence.id]

            if sequence.delivered:
                self.log_event("_input_sequence_end2 {}", sequence)
                self.on_input_sequence_end(sequence)

        if self._input_sequences:
            self._discard_stuck_input_sequences()

        self._last_sequence_time = sequence.time

    def _discard_stuck_input_sequences(self):
        """
        Input sequence handling requires guaranteed balancing of
        begin, update and end events. There is no indication yet this
        isn't always the case, but still, at this time it seems like a
        good idea to prepare for the worst.
        -> Clear out aged input sequences, so Onboard can start from a
        fresh slate and not become terminally unresponsive.
        """
        expired_time = time.time() - 30
        for id, sequence in list(self._input_sequences.items()):
            if sequence.update_time < expired_time:
                _logger.warning("discarding expired input sequence " + str(id))
                del self._input_sequences[id]

    def in_gesture_detection_delay(self, sequence):
        """
        Are we still in the time span where sequence begins aren't delayed
        and can't be undone after gesture detection?
        """
        span = sequence.time - self._gesture_begin_time
        return span < self.GESTURE_DETECTION_SPAN

    def _gesture_sequence_begin(self, sequence):
        # first tap?
        if self._num_tap_sequences == 0:
            self._gesture = NO_GESTURE
            self._gesture_detected = False
            self._gesture_cancelled = False
            self._gesture_begin_point = sequence.point
            self._gesture_begin_time = sequence.time  # event time
        else:  # subsequent taps
            if self.in_gesture_detection_delay(sequence) and \
               not self._gesture_cancelled:
                self._gesture_timer.stop()  # cancel delayed sequence begin
                self._gesture_detected = True
        self._num_tap_sequences += 1

    def _gesture_sequence_update(self, sequence):
        if self._gesture_detected and \
           sequence.state & BUTTON123_MASK and \
           self._gesture == NO_GESTURE:
            point = sequence.point
            dx = self._gesture_begin_point[0] - point[0]
            dy = self._gesture_begin_point[1] - point[1]
            d2 = dx * dx + dy * dy

            # drag gesture?
            if d2 >= DRAG_GESTURE_THRESHOLD2:
                num_touches = len(self._input_sequences)
                self._gesture = DRAG_GESTURE
                self.on_drag_gesture_begin(num_touches)
        return True

    def _gesture_sequence_end(self, sequence):
        if len(self._input_sequences) == 1:  # last sequence of the gesture?
            if self._gesture_detected:
                gesture = self._gesture

                if gesture == NO_GESTURE:
                    # tap gesture?
                    elapsed = sequence.time - self._gesture_begin_time
                    if elapsed <= 300:
                        self.on_tap_gesture(self._num_tap_sequences)

                elif gesture == DRAG_GESTURE:
                    self.on_drag_gesture_end(0)

            self._num_tap_sequences = 0

    def on_tap_gesture(self, num_touches):
        return False

    def on_drag_gesture_begin(self, num_touches):
        return False

    def on_drag_gesture_end(self, num_touches):
        return False

    def redirect_sequence_update(self, sequence, func):
        """ redirect input sequence update to self. """
        sequence = self._get_redir_sequence(sequence)
        func(sequence)

    def redirect_sequence_end(self, sequence, func):
        """ Redirect input sequence end to self. """
        sequence = self._get_redir_sequence(sequence)

        # Make sure has_input_sequences() returns False inside of func().
        # Class Keyboard needs this to detect the end of input.
        if sequence.id in self._input_sequences:
            del self._input_sequences[sequence.id]

        func(sequence)

    def _get_redir_sequence(self, sequence):
        """ Return a copy of <sequence>, managed in the target window. """
        redir_sequence = self._input_sequences.get(sequence.id)
        if redir_sequence is None:
            redir_sequence = sequence.copy()
            redir_sequence.initial_active_key = None
            redir_sequence.active_key = None
            redir_sequence.cancel_key_action = False  # was canceled by long press

            self._input_sequences[redir_sequence.id] = redir_sequence

        # convert to the new window client coordinates
        pos = self.get_position()
        rp = sequence.root_point
        redir_sequence.point = (rp[0] - pos[0], rp[1] - pos[1])

        return redir_sequence
Beispiel #23
0
class AtspiTextContext(TextContext):
    """
    Keep track of the current text context with AT-SPI
    """

    _state_tracker = AtspiStateTracker()

    def __init__(self, wp):
        self._wp = wp
        self._accessible = None
        self._can_insert_text = False

        self._text_domains = TextDomains()
        self._text_domain = self._text_domains.get_nop_domain()

        self._changes = TextChanges()
        self._entering_text = False
        self._text_changed = False

        self._context = ""
        self._line = ""
        self._line_caret = 0
        self._selection_span = TextSpan()
        self._begin_of_text = False        # context starts at begin of text?
        self._begin_of_text_offset = None  # offset of text begin

        self._pending_separator_span = None
        self._last_text_change_time = 0
        self._last_caret_move_time = 0
        self._last_caret_move_position = 0

        self._last_context = None
        self._last_line = None

        self._update_context_timer = Timer()
        self._update_context_delay_normal = 0.01
        self._update_context_delay = self._update_context_delay_normal

    def cleanup(self):
        self._register_atspi_listeners(False)

    def enable(self, enable):
        self._register_atspi_listeners(enable)

    def get_text_domain(self):
        return self._text_domain

    def set_pending_separator(self, separator_span=None):
        """ Remember this separator span for later insertion. """
        if self._pending_separator_span is not separator_span:
            self._pending_separator_span = separator_span

    def get_pending_separator(self):
        """ Return current pending separator span or None """
        return self._pending_separator_span

    def get_context(self):
        """
        Returns the predictions context, i.e. some range of
        text before the caret position.
        """
        if self._accessible is None:
            return ""

        # Don't update suggestions in scrolling terminals
        if self._entering_text or \
           not self._text_changed or \
           self.can_suggest_before_typing():
            return self._context

        return ""

    def get_bot_context(self):
        """
        Returns the predictions context with
        begin of text marker (at text begin).
        """
        context = ""
        if self._accessible:
            context = self.get_context()

            # prepend domain specific begin-of-text marker
            if self._begin_of_text:
                marker = self.get_text_begin_marker()
                if marker:
                    context = marker + " " + context

        return context

    def get_pending_bot_context(self):
        """
        Context including bot marker and pending separator.
        """
        context = self.get_bot_context()
        if self._pending_separator_span is not None:
            context += self._pending_separator_span.get_span_text()
        return context

    def get_line(self):
        return self._line \
               if self._accessible else ""

    def get_line_caret_pos(self):
        return self._line_caret \
               if self._accessible else 0

    def get_line_past_caret(self):
        return self._line[self._line_caret:] \
               if self._accessible else ""

    def get_selection_span(self):
        return self._selection_span \
               if self._accessible else None

    def get_span_at_caret(self):
        if not self._accessible:
            return None

        span = self._selection_span.copy()
        span.length = 0
        return span

    def get_caret(self):
        return self._selection_span.begin() \
            if self._accessible else 0

    def get_character_extents(self, offset):
        accessible = self._accessible
        if accessible:
            return self._state_tracker.get_accessible_character_extents(
                accessible, offset)
        else:
            return None

    def get_text_begin_marker(self):
        domain = self.get_text_domain()
        if domain:
            return domain.get_text_begin_marker()
        return ""

    def can_record_insertion(self, accessible, pos, length):
        domain = self.get_text_domain()
        if domain:
            return domain.can_record_insertion(accessible, pos, length)
        return True

    def can_suggest_before_typing(self):
        domain = self.get_text_domain()
        if domain:
            return domain.can_suggest_before_typing()
        return True

    def can_auto_punctuate(self):
        domain = self.get_text_domain()
        if domain:
            return domain.can_auto_punctuate(self._begin_of_text)
        return False

    def get_begin_of_text_offset(self):
        return self._begin_of_text_offset \
               if self._accessible else None

    def get_changes(self):
        return self._changes

    def has_changes(self):
        """ Are there any changes to learn? """
        return not self._changes.is_empty()

    def clear_changes(self):
        self._changes.clear()

    def can_insert_text(self):
        """
        Can delete or insert text into the accessible?
        """
        #return False # support for inserting is spotty: not in firefox, terminal
        return bool(self._accessible) and self._can_insert_text

    def delete_text(self, offset, length=1):
        """ Delete directly, without going through faking key presses. """
        self._accessible.delete_text(offset, offset + length)

    def delete_text_before_caret(self, length=1):
        """ Delete directly, without going through faking key presses. """
        offset = self._accessible.get_caret_offset()
        self.delete_text(offset - length, length)

    def insert_text(self, offset, text):
        """
        Insert directly, without going through faking key presses.
        """
        self._accessible.insert_text(offset, text, -1)

        # Move the caret after insertion if the accessible itself
        # hasn't done so already. This assumes the insertion begins at
        # the current caret position, which always happens to be the case
        # currently.
        # Only the nautilus rename text entry appears to need this.
        offset_before = offset
        offset_after = self._accessible.get_caret_offset()
        if text and offset_before == offset_after:
            self._accessible.set_caret_offset(offset_before + len(text))

    def insert_text_at_caret(self, text):
        """
        Insert directly, without going through faking key presses.
        Fails for terminal and firefox, unfortunately.
        """
        offset = self._accessible.get_caret_offset()
        self.insert_text(offset, text)

    def _register_atspi_listeners(self, register=True):
        st = self._state_tracker
        if register:
            st.connect("text-entry-activated", self._on_text_entry_activated)
            st.connect("text-changed", self._on_text_changed)
            st.connect("text-caret-moved", self._on_text_caret_moved)
            # st.connect("key-pressed", self._on_atspi_key_pressed)
        else:
            st.disconnect("text-entry-activated", self._on_text_entry_activated)
            st.disconnect("text-changed", self._on_text_changed)
            st.disconnect("text-caret-moved", self._on_text_caret_moved)
            # st.disconnect("key-pressed", self._on_atspi_key_pressed)

    def get_accessible_capabilities(self, accessible, **kwargs):
        can_insert_text = False
        attributes = kwargs.get("attributes", {})
        interfaces = kwargs.get("interfaces", [])

        if accessible:

            # Can insert text via Atspi?
            # Advantages:
            # - faster, no individual key presses
            # - trouble-free insertion of all unicode characters
            if "EditableText" in interfaces:
                # Support for atspi text insertion is spotty.
                # Firefox, LibreOffice Writer, gnome-terminal don't support it,
                # even if they claim to implement the EditableText interface.

                # Allow direct text insertion for gtk widgets
                if self._state_tracker.is_toolkit_gtk3(attributes):
                    can_insert_text = True

        return can_insert_text

    def _on_text_entry_activated(self, accessible):
        # old text_domain still valid here
        self._wp.on_text_entry_deactivated()

        # keep track of the active accessible asynchronously
        self._accessible = accessible
        self._entering_text = False
        self._text_changed = False

        # select text domain matching this accessible
        state = self._state_tracker.get_state() \
                if self._accessible else {}
        self._text_domain = self._text_domains.find_match(**state)
        self._text_domain.init_domain()

        # determine capabilities of this accessible
        self._can_insert_text = \
            self.get_accessible_capabilities(accessible, **state)

        # log accessible info
        if _logger.isEnabledFor(_logger.LEVEL_ATSPI):
            log = _logger.atspi
            log("-" * 70)
            log("Accessible focused: ")
            indent = " " * 4
            if self._accessible:
                state = self._state_tracker.get_state()
                for key, value in sorted(state.items()):
                    msg = str(key) + "="
                    if key == "state-set":
                        msg += repr(AtspiStateType.to_strings(value))
                    elif hasattr(value, "value_name"):  # e.g. role
                        msg += value.value_name
                    else:
                        msg += repr(value)
                    log(indent + msg)
                log(indent + "text_domain: {}"
                    .format(self._text_domain and
                            type(self._text_domain).__name__))
                log(indent + "can_insert_text: {}"
                    .format(self._can_insert_text))
            else:
                log(indent + "None")

        self._update_context()

        self._wp.on_text_entry_activated()

    def _on_text_changed(self, event):
        insertion_span = self._record_text_change(event.pos,
                                                  event.length,
                                                  event.insert)
        # synchronously notify of text insertion
        if insertion_span:
            try:
                caret_offset = self._accessible.get_caret_offset()
            except:  # gi._glib.GError
                pass
            else:
                self._wp.on_text_inserted(insertion_span, caret_offset)

        self._last_text_change_time = time.time()
        self._update_context()

    def _on_text_caret_moved(self, event):
        self._last_caret_move_time = time.time()
        self._last_caret_move_position = event.caret
        self._update_context()
        self._wp.on_text_caret_moved()

    def _on_atspi_key_pressed(self, event):
        """ disabled, Francesco didn't receive any AT-SPI key-strokes. """
        # keycode = event.hw_code # uh oh, only keycodes...
        #                         # hopefully "c" doesn't move around a lot.
        # modifiers = event.modifiers
        # self._handle_key_press(keycode, modifiers)

    def on_onboard_typing(self, key, mod_mask):
        if key.is_text_changing():
            keycode = 0
            if key.is_return():
                keycode = KeyCode.KP_Enter
            else:
                label = key.get_label()
                if label == "C" or label == "c":
                    keycode = KeyCode.C

            self._handle_key_press(keycode, mod_mask)

    def _handle_key_press(self, keycode, modifiers):
        if self._accessible:
            domain = self.get_text_domain()
            if domain:
                self._entering_text, end_of_editing = \
                    domain.handle_key_press(keycode, modifiers)

                if end_of_editing is True:
                    self._wp.commit_changes()
                elif end_of_editing is False:
                    self._wp.discard_changes()

    def _record_text_change(self, pos, length, insert):
        accessible = self._accessible

        insertion_span = None
        char_count = None
        if accessible:
            try:
                char_count = accessible.get_character_count()
            except:     # gi._glib.GError: The application no longer exists
                        # when closing a tab in gnome-terminal.
                char_count = None

        if char_count is not None:
            # record the change
            spans_to_update = []

            if insert:
                if self._entering_text and \
                   self.can_record_insertion(accessible, pos, length):
                    if self._wp.is_typing() or length < 30:
                        # Remember all of the insertion, might have been
                        # a pressed snippet or wordlist button.
                        include_length = -1
                    else:
                        # Remember only the first few characters.
                        # Large inserts can be paste, reload or scroll
                        # operations. Only learn the first word of these.
                        include_length = 2

                    # simple span for current insertion
                    begin = max(pos - 100, 0)
                    end = min(pos + length + 100, char_count)
                    text = self._state_tracker.get_accessible_text(accessible,
                                                                   begin, end)
                    if text is not None:
                        insertion_span = TextSpan(pos, length, text, begin)
                else:
                    # Remember nothing, just update existing spans.
                    include_length = None

                spans_to_update = self._changes.insert(pos, length,
                                                       include_length)

            else:
                spans_to_update = self._changes.delete(pos, length,
                                                       self._entering_text)

            # update text of all modified spans
            for span in spans_to_update:
                # Get some more text around the span to hopefully
                # include whole words at beginning and end.
                begin = max(span.begin() - 100, 0)
                end = min(span.end() + 100, char_count)
                span.text = Atspi.Text.get_text(accessible, begin, end)
                span.text_pos = begin

        self._text_changed = True

        return insertion_span

    def set_update_context_delay(self, delay):
        self._update_context_delay = delay

    def reset_update_context_delay(self):
        self._update_context_delay = self._update_context_delay_normal

    def _update_context(self):
        self._update_context_timer.start(self._update_context_delay,
                                         self.on_text_context_changed)

    def on_text_context_changed(self):
        # Clear pending separator when the user clicked to move
        # the cursor away from the separator position.
        if self._pending_separator_span:
            # Lone caret movement, no recent text change?
            if self._last_caret_move_time - self._last_text_change_time > 1.0:
                # Away from the separator?
                if self._last_caret_move_position != \
                   self._pending_separator_span.begin():
                    self.set_pending_separator(None)

        result = self._text_domain.read_context(self._accessible)
        if result is not None:
            (self._context,
             self._line,
             self._line_caret,
             self._selection_span,
             self._begin_of_text,
             self._begin_of_text_offset) = result

            # make sure to include bot-markers and pending separator
            context = self.get_pending_bot_context()
            change_detected = (self._last_context != context or
                               self._last_line != self._line)
            if change_detected:
                self._last_context = context
                self._last_line    = self._line

            self._wp.on_text_context_changed(change_detected)

        return False
Beispiel #24
0
 def _start_dwelling(self):
     self._stop_dwelling()
     self._dwell_begin_timer = Timer(1.5, self._on_dwell_begin_timer)
     self._no_more_dwelling = True
Beispiel #25
0
class WindowRectPersist(WindowRectTracker):
    """
    Save and restore window position and size.
    """
    def __init__(self):
        WindowRectTracker.__init__(self)
        self._screen_orientation = None
        self._save_position_timer = Timer()

        # init detection of screen "rotation"
        screen = self.get_screen()
        screen.connect('size-changed', self.on_screen_size_changed)

    def cleanup(self):
        self._save_position_timer.finish()

    def is_visible(self):
        """ This is overloaded in KbdWindow """
        return Gtk.Window.get_visible(self)

    def on_screen_size_changed(self, screen):
        """ detect screen rotation (tablets)"""

        # Give the screen time to settle, the window manager
        # may block the move to previously invalid positions and
        # when docked, the slide animation may be drowned out by all
        # the action in other processes.
        Timer(1.5, self.on_screen_size_changed_delayed, screen)

    def on_screen_size_changed_delayed(self, screen):
        self.restore_window_rect()

    def get_screen_orientation(self):
        """
        Current orientation of the screen (tablet rotation).
        Only the aspect ratio is taken into account at this time.
        This appears to cover more cases than looking at monitor rotation,
        in particular with multi-monitor screens.
        """
        screen = self.get_screen()
        if screen.get_width() >= screen.get_height():
            return Orientation.LANDSCAPE
        else:
            return Orientation.PORTRAIT

    def restore_window_rect(self, startup = False):
        """
        Restore window size and position.
        """
        # Run pending save operations now, so they don't
        # interfere with the window rect after it was restored.
        self._save_position_timer.finish()

        orientation = self.get_screen_orientation()
        rect = self.read_window_rect(orientation)

        self._screen_orientation = orientation
        self._window_rect = rect
        _logger.debug("restore_window_rect {rect}, {orientation}" \
                      .format(rect = rect, orientation = orientation))

        # Give the derived class a chance to modify the rect,
        # for example to correct the position for auto-show.
        rect = self.on_restore_window_rect(rect)
        self._window_rect = rect

        # move/resize the window
        if startup:
            # gnome-shell doesn't take kindly to an initial move_resize().
            # The window ends up at (0, 0) on and goes back there
            # repeatedly when hiding and unhiding.
            self.set_default_size(rect.w, rect.h)
            self.move(rect.x, rect.y)
        else:
            self.move_resize(rect.x, rect.y, rect.w, rect.h)

        # Initialize shadow variables with valid values so they
        # don't get taken from the unreliable window.
        # Fixes bad positioning of the very first auto-show.
        if startup:
            self._window_rect = rect.copy()
            # Ignore frame dimensions; still better than asking the window.
            self._origin      = rect.left_top()
            self._screen_orientation = self.get_screen_orientation()

    def on_restore_window_rect(self, rect):
        return rect

    def save_window_rect(self, orientation=None, rect=None):
        """
        Save window size and position.
        """
        if orientation is None:
            orientation = self._screen_orientation
        if rect is None:
            rect = self._window_rect

        # Give the derived class a chance to modify the rect,
        # for example to override it for auto-show.
        rect = self.on_save_window_rect(rect)

        self.write_window_rect(orientation, rect)

        _logger.debug("save_window_rect {rect}, {orientation}" \
                      .format(rect=rect, orientation=orientation))

    def on_save_window_rect(self, rect):
        return rect

    def read_window_rect(self, orientation, rect):
        """
        Read orientation dependent rect.
        Overload this in derived classes.
        """
        raise NotImplementedError()

    def write_window_rect(self, orientation, rect):
        """
        Write orientation dependent rect.
        Overload this in derived classes.
        """
        raise NotImplementedError()

    def start_save_position_timer(self):
        """
        Trigger saving position and size to gsettings
        Delay this a few seconds to avoid excessive disk writes.

        Remember the current rect and rotation as the screen may have been
        rotated when the saving happens.
        """
        self._save_position_timer.start(5,
                                        self.save_window_rect,
                                        self.get_screen_orientation(),
                                        self.get_rect())

    def stop_save_position_timer(self):
        self._save_position_timer.stop()
Beispiel #26
0
class OnboardGtk(object):
    """
    Main controller class for Onboard using GTK+
    """

    keyboard = None

    def __init__(self):

        # Make sure windows get "onboard", "Onboard" as name and class
        # For some reason they aren't correctly set when onboard is started
        # from outside the source directory (Precise).
        GLib.set_prgname(str(app))
        Gdk.set_program_class(app[0].upper() + app[1:])

        # no python3-dbus on Fedora17
        bus = None
        err_msg = ""
        if not has_dbus:
            err_msg = "D-Bus bindings unavailable"
        else:
            # Use D-bus main loop by default
            dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

            # Don't fail to start in Xenial's lightdm when
            # D-Bus isn't available.
            try:
                bus = dbus.SessionBus()
            except dbus.exceptions.DBusException:
                err_msg = "D-Bus session bus unavailable"
                bus = None

        if not bus:
            _logger.warning(err_msg + "  " +
                            "Onboard will start with reduced functionality. "
                            "Single-instance check, "
                            "D-Bus service and "
                            "hover click are disabled.")

        # Yield to GNOME Shell's keyboard before any other D-Bus activity
        # to reduce the chance for D-Bus timeouts when enabling a11y keboard.
        if not self._can_show_in_current_desktop(bus):
            sys.exit(0)

        if bus:
            # Check if there is already an Onboard instance running
            has_remote_instance = \
                bus.name_has_owner(ServiceOnboardKeyboard.NAME)

            # Onboard in Ubuntu on first start silently embeds itself into
            # gnome-screensaver and stays like this until embedding is manually
            # turned off.
            # If gnome's "Typing Assistent" is disabled, only show onboard in
            # gss when there is already a non-embedded instance running in
            # the user session (LP: 938302).
            if config.xid_mode and \
               config.launched_by == config.LAUNCHER_GSS and \
               not (config.gnome_a11y and
                    config.gnome_a11y.screen_keyboard_enabled) and \
               not has_remote_instance:
                sys.exit(0)

            # Embedded instances can't become primary instances
            if not config.xid_mode:
                if has_remote_instance and \
                   not config.options.allow_multiple_instances:
                    # Present remote instance
                    remote = bus.get_object(ServiceOnboardKeyboard.NAME,
                                            ServiceOnboardKeyboard.PATH)
                    remote.Show(dbus_interface=ServiceOnboardKeyboard.IFACE)
                    _logger.info("Exiting: Not the primary instance.")
                    sys.exit(0)

                # Register our dbus name
                self._bus_name = \
                    dbus.service.BusName(ServiceOnboardKeyboard.NAME, bus)

        self.init()

        _logger.info("Entering mainloop of onboard")
        Gtk.main()

        # Shut up error messages on SIGTERM in lightdm:
        # "sys.excepthook is missing, lost sys.stderr"
        # See http://bugs.python.org/issue11380 for more.
        # Python 2.7, Precise
        try:
            sys.stdout.close()
        except:
            pass
        try:
            sys.stderr.close()
        except:
            pass

    def init(self):
        self.keyboard_state = None
        self._last_mod_mask = 0
        self.vk_timer = None
        self.reset_vk()
        self._connections = []
        self._window = None
        self.status_icon = None
        self.service_keyboard = None
        self._reload_layout_timer = Timer()

        # finish config initialization
        config.init()

        # Optionally wait a little before proceeding.
        # When the system starts up, the docking strut can be unreliable
        # on Compiz (Yakkety) and the keyboard may end up at unexpected initial
        # positions. Delaying the startup when launched by
        # onboard-autostart.desktop helps to prevent this.
        delay = config.startup_delay
        if delay:
            Timer(delay, self._init_delayed)
        else:
            self._init_delayed()

    def _init_delayed(self):

        # Release pressed keys when onboard is killed.
        # Don't keep enter key stuck when being killed by lightdm.
        self._osk_util = osk.Util()
        self._osk_util.set_unix_signal_handler(signal.SIGTERM, self.on_sigterm)
        self._osk_util.set_unix_signal_handler(signal.SIGINT, self.on_sigint)

        sys.path.append(os.path.join(config.install_dir, 'scripts'))

        # Create the central keyboard model
        self.keyboard = Keyboard(self)

        # Create the initial keyboard widget
        # Care for toolkit independence only once there is another
        # supported one besides GTK.
        self.keyboard_widget = KeyboardWidget(self.keyboard)

        # create the main window
        if config.xid_mode:    # XEmbed mode for gnome-screensaver?
            # no icp, don't flash the icon palette in lightdm

            self._window = KbdPlugWindow(self.keyboard_widget)

            # write xid to stdout
            sys.stdout.write('%d\n' % self._window.get_id())
            sys.stdout.flush()
        else:
            icp = IconPalette(self.keyboard)
            icp.set_layout_view(self.keyboard_widget)
            icp.connect("activated", self._on_icon_palette_acticated)
            self.do_connect(icp.get_menu(), "quit-onboard",
                        lambda x: self.do_quit_onboard())

            self._window = KbdWindow(self.keyboard_widget, icp)
            self.do_connect(self._window, "quit-onboard",
                            lambda x: self.do_quit_onboard())

        # config.xid_mode = True
        self._window.application = self
        # need this to access screen properties
        config.main_window = self._window

        # load the initial layout
        _logger.info("Loading initial layout")
        self.reload_layout()

        # Handle command line options x, y, size after window creation
        # because the rotation code needs the window's screen.
        if not config.xid_mode:
            rect = self._window.get_rect().copy()
            options = config.options
            if options.size:
                size = options.size.split("x")
                rect.w = int(size[0])
                rect.h = int(size[1])
            if options.x is not None:
                rect.x = options.x
            if options.y is not None:
                rect.y = options.y

            # Make sure the keyboard fits on screen
            rect = self._window.limit_size(rect)

            if not rect.is_empty() and \
               rect != self._window.get_rect():
                _logger.debug("limiting window size: {} to {}"
                            .format(self._window.get_rect(), rect))
                orientation = self._window.get_screen_orientation()
                self._window.write_window_rect(orientation, rect)
                self._window.restore_window_rect()  # move/resize early
            else:
                _logger.debug("not limiting window size: {} to {}"
                            .format(self._window.get_rect(), rect))


        # export dbus service
        if not config.xid_mode and \
           has_dbus:
            self.service_keyboard = ServiceOnboardKeyboard(self)

        # show/hide the window
        self.keyboard_widget.set_startup_visibility()

        # keep keyboard window and icon palette on top of dash
        self._keep_windows_on_top()

        # connect notifications for keyboard map and group changes
        self.keymap = Gdk.Keymap.get_default()
        # map changes
        self.do_connect(self.keymap, "keys-changed", self.cb_keys_changed)
        self.do_connect(self.keymap, "state-changed", self.cb_state_changed)
        # group changes
        Gdk.event_handler_set(cb_any_event, self)

        # connect config notifications here to keep config from holding
        # references to keyboard objects.
        once = CallOnce(50).enqueue  # delay callbacks by 50ms
        reload_layout       = lambda x: once(self.reload_layout_and_present)
        update_ui           = lambda x: once(self._update_ui)
        update_ui_no_resize = lambda x: once(self._update_ui_no_resize)
        update_transparency = \
            lambda x: once(self.keyboard_widget.update_transparency)
        update_inactive_transparency = \
            lambda x: once(self.keyboard_widget.update_inactive_transparency)

        # general

        # keyboard
        config.keyboard.key_synth_notify_add(reload_layout)
        config.keyboard.input_event_source_notify_add(lambda x:
                                    self.keyboard.update_input_event_source())
        config.keyboard.touch_input_notify_add(lambda x:
                                    self.keyboard.update_touch_input_mode())
        config.keyboard.show_secondary_labels_notify_add(update_ui)

        # window
        config.window.window_state_sticky_notify_add(
            lambda x: self._window.update_sticky_state())
        config.window.window_decoration_notify_add(
            self._on_window_options_changed)
        config.window.force_to_top_notify_add(self._on_window_options_changed)
        config.window.keep_aspect_ratio_notify_add(update_ui)

        config.window.transparency_notify_add(update_transparency)
        config.window.background_transparency_notify_add(update_transparency)
        config.window.transparent_background_notify_add(update_ui)
        config.window.enable_inactive_transparency_notify_add(update_transparency)
        config.window.inactive_transparency_notify_add(update_inactive_transparency)
        config.window.docking_notify_add(self._update_docking)
        config.window.docking_aspect_change_range_notify_add(
            lambda x: self.keyboard_widget
            .update_docking_aspect_change_range())

        # layout
        config.layout_filename_notify_add(reload_layout)

        # theme
        # config.gdi.gtk_theme_notify_add(self.on_gtk_theme_changed)
        config.theme_notify_add(self.on_theme_changed)
        config.key_label_font_notify_add(reload_layout)
        config.key_label_overrides_notify_add(reload_layout)
        config.theme_settings.color_scheme_filename_notify_add(reload_layout)
        config.theme_settings.key_label_font_notify_add(reload_layout)
        config.theme_settings.key_label_overrides_notify_add(reload_layout)
        config.theme_settings.theme_attributes_notify_add(update_ui)

        # snippets
        config.snippets_notify_add(reload_layout)

        # auto-show
        config.auto_show.enabled_notify_add(
            lambda x: self.keyboard.update_auto_show())
        config.auto_show.hide_on_key_press_notify_add(
            lambda x: self.keyboard.update_auto_hide())
        config.auto_show.tablet_mode_detection_notify_add(
            lambda x: self.keyboard.update_tablet_mode_detection())
        config.auto_show.keyboard_device_detection_enabled_notify_add(
            lambda x: self.keyboard.update_keyboard_device_detection())

        # word suggestions
        config.word_suggestions.show_context_line_notify_add(update_ui)
        config.word_suggestions.enabled_notify_add(lambda x:
                                 self.keyboard.on_word_suggestions_enabled(x))
        config.word_suggestions.auto_learn_notify_add(
                                 update_ui_no_resize)
        config.typing_assistance.active_language_notify_add(lambda x: \
                                 self.keyboard.on_active_lang_id_changed())
        config.typing_assistance.spell_check_backend_notify_add(lambda x: \
                                 self.keyboard.on_spell_checker_changed())
        config.typing_assistance.auto_capitalization_notify_add(lambda x: \
                                 self.keyboard.on_word_suggestions_enabled(x))
        config.word_suggestions.spelling_suggestions_enabled_notify_add(lambda x: \
                                 self.keyboard.on_spell_checker_changed())
        config.word_suggestions.delayed_word_separators_enabled_notify_add(lambda x: \
                                 self.keyboard.on_punctuator_changed())
        config.word_suggestions.wordlist_buttons_notify_add(
                                 update_ui_no_resize)

        # universal access
        config.scanner.enabled_notify_add(self.keyboard._on_scanner_enabled)
        config.window.window_handles_notify_add(self._on_window_handles_changed)

        # misc
        config.keyboard.show_click_buttons_notify_add(update_ui)
        config.lockdown.lockdown_notify_add(update_ui)
        if config.mousetweaks:
            config.mousetweaks.state_notify_add(update_ui_no_resize)

        # create status icon
        self.status_icon = Indicator()
        self.status_icon.set_keyboard(self.keyboard)
        self.do_connect(self.status_icon.get_menu(), "quit-onboard",
                        lambda x: self.do_quit_onboard())

        # Callbacks to use when icp or status icon is toggled
        config.show_status_icon_notify_add(self.show_hide_status_icon)
        config.icp.in_use_notify_add(self.cb_icp_in_use_toggled)

        self.show_hide_status_icon(config.show_status_icon)


        # Minimize to IconPalette if running under GDM
        if 'RUNNING_UNDER_GDM' in os.environ:
            _logger.info("RUNNING_UNDER_GDM set, turning on icon palette")
            config.icp.in_use = True
            _logger.info("RUNNING_UNDER_GDM set, turning off indicator")
            config.show_status_icon = False

            # For some reason the new values don't arrive in gsettings when
            # running the unit test "test_running_in_live_cd_environment".
            # -> Force gsettings to apply them, that seems to do the trick.
            config.icp.apply()
            config.apply()

        # unity-2d needs the skip-task-bar hint set before the first mapping.
        self.show_hide_taskbar()


        # Check gnome-screen-saver integration
        # onboard_xembed_enabled                False True     True      True
        # config.gss.embedded_keyboard_enabled  any   False    any       False
        # config.gss.embedded_keyboard_command  any   empty    !=onboard ==onboard
        # Action:                               nop   enable   Question1 Question2
        #                                             silently
        if not config.xid_mode and \
           config.onboard_xembed_enabled:

            # If it appears, that nothing has touched the gss keys before,
            # silently enable gss integration with onboard.
            if not config.gss.embedded_keyboard_enabled and \
               not config.gss.embedded_keyboard_command:
                config.enable_gss_embedding(True)

            # If onboard is configured to be embedded into the unlock screen
            # dialog, and the embedding command is different from onboard, ask
            # the user what to do
            elif not config.is_onboard_in_xembed_command_string():
                question = _("Onboard is configured to appear with the dialog to "
                             "unlock the screen; for example to dismiss the "
                             "password-protected screensaver.\n\n"
                             "However the system is not configured anymore to use "
                             "Onboard to unlock the screen. A possible reason can "
                             "be that another application configured the system to "
                             "use something else.\n\n"
                             "Would you like to reconfigure the system to show "
                             "Onboard when unlocking the screen?")
                _logger.warning("showing dialog: '{}'".format(question))
                reply = show_confirmation_dialog(question,
                                                 self._window,
                                                 config.is_force_to_top())
                if reply == True:
                    config.enable_gss_embedding(True)
                else:
                    config.onboard_xembed_enabled = False
            else:
                if not config.gss.embedded_keyboard_enabled:
                    question = _("Onboard is configured to appear with the dialog "
                                 "to unlock the screen; for example to dismiss "
                                 "the password-protected screensaver.\n\n"
                                 "However this function is disabled in the system.\n\n"
                                 "Would you like to activate it?")
                    _logger.warning("showing dialog: '{}'".format(question))
                    reply = show_confirmation_dialog(question,
                                                     self._window,
                                                     config.is_force_to_top())
                    if reply == True:
                        config.enable_gss_embedding(True)
                    else:
                        config.onboard_xembed_enabled = False

        # check if gnome accessibility is enabled for auto-show
        if (config.is_auto_show_enabled() or \
            config.are_word_suggestions_enabled()) and \
            not config.check_gnome_accessibility(self._window):
            config.auto_show.enabled = False

    def on_sigterm(self):
        """
        Exit onboard on kill.
        """
        _logger.debug("SIGTERM received")
        self.do_quit_onboard()

    def on_sigint(self):
        """
        Exit onboard on Ctrl+C press.
        """
        _logger.debug("SIGINT received")
        self.do_quit_onboard()

    def do_connect(self, instance, signal, handler):
        handler_id = instance.connect(signal, handler)
        self._connections.append((instance, handler_id))

    # Method concerning the taskbar
    def show_hide_taskbar(self):
        """
        This method shows or hides the taskbard depending on whether there
        is an alternative way to unminimize the Onboard window.
        This method should be called every time such an alternative way
        is activated or deactivated.
        """
        if self._window:
            self._window.update_taskbar_hint()

    # Method concerning the icon palette
    def _on_icon_palette_acticated(self, widget):
        self.keyboard.toggle_visible()

    def cb_icp_in_use_toggled(self, icp_in_use):
        """
        This is the callback that gets executed when the user toggles
        the gsettings key named in_use of the icon_palette. It also
        handles the showing/hiding of the taskar.
        """
        _logger.debug("Entered in on_icp_in_use_toggled")
        self.show_hide_icp()
        _logger.debug("Leaving on_icp_in_use_toggled")

    def show_hide_icp(self):
        if self._window.icp:
            show = config.is_icon_palette_in_use()
            if show:
                # Show icon palette if appropriate and handle visibility of taskbar.
                if not self._window.is_visible():
                    self._window.icp.show()
                self.show_hide_taskbar()
            else:
                # Show icon palette if appropriate and handle visibility of taskbar.
                if not self._window.is_visible():
                    self._window.icp.hide()
                self.show_hide_taskbar()

    # Methods concerning the status icon
    def show_hide_status_icon(self, show_status_icon):
        """
        Callback called when gsettings detects that the gsettings key specifying
        whether the status icon should be shown or not is changed. It also
        handles the showing/hiding of the taskar.
        """
        if show_status_icon:
            self.status_icon.set_visible(True)
        else:
            self.status_icon.set_visible(False)
        self.show_hide_icp()
        self.show_hide_taskbar()

    def cb_status_icon_clicked(self,widget):
        """
        Callback called when status icon clicked.
        Toggles whether Onboard window visibile or not.

        TODO would be nice if appeared to iconify to taskbar
        """
        self.keyboard.toggle_visible()

    def cb_group_changed(self):
        """ keyboard group change """
        self.reload_layout_delayed()

    def cb_keys_changed(self, keymap):
        """ keyboard map change """
        self.reload_layout_delayed()

    def cb_state_changed(self, keymap):
        """ keyboard modifier state change """
        mod_mask = keymap.get_modifier_state()
        _logger.debug("keyboard state changed to 0x{:x}" \
                      .format(mod_mask))
        if self._last_mod_mask == mod_mask:
            # Must be something other than changed modifiers,
            # group change on wayland, for example.
            self.reload_layout_delayed()
        else:
            self.keyboard.set_modifiers(mod_mask)

        self._last_mod_mask = mod_mask


    def cb_vk_timer(self):
        """
        Timer callback for polling until virtkey becomes valid.
        """
        if self.get_vk():
            self.reload_layout(force_update=True)
            GLib.source_remove(self.vk_timer)
            self.vk_timer = None
            return False
        return True

    def _update_ui(self):
        if self.keyboard:
            self.keyboard.invalidate_ui()
            self.keyboard.commit_ui_updates()

    def _update_ui_no_resize(self):
        if self.keyboard:
            self.keyboard.invalidate_ui_no_resize()
            self.keyboard.commit_ui_updates()

    def _on_window_handles_changed(self, value = None):
        self.keyboard_widget.update_window_handles()
        self._update_ui()

    def _on_window_options_changed(self, value = None):
        self._update_window_options()
        self.keyboard.commit_ui_updates()

    def _update_window_options(self, value = None):
        window = self._window
        if window:
            window.update_window_options()
            if window.icp:
                window.icp.update_window_options()
            self.keyboard.invalidate_ui()

    def _update_docking(self, value = None):
        self._update_window_options()

        # give WM time to settle, else move to the strut position may fail
        GLib.idle_add(self._update_docking_delayed)

    def _update_docking_delayed(self):
        self._window.on_docking_notify()
        self.keyboard.invalidate_ui()  # show/hide the move button
#        self.keyboard.commit_ui_updates() # redundant

    def on_gdk_setting_changed(self, name):
        if name == "gtk-theme-name":
            # In Zesty this has to be delayed too.
            GLib.timeout_add_seconds(1, self.on_gtk_theme_changed)

        elif name in ["gtk-xft-dpi",
                      "gtk-xft-antialias"
                      "gtk-xft-hinting",
                      "gtk-xft-hintstyle"]:
            # For some reason the font sizes are still off when running
            # this immediately. Delay it a little.
            GLib.idle_add(self.on_gtk_font_dpi_changed)

    def on_gtk_theme_changed(self, gtk_theme = None):
        """
        Switch onboard themes in sync with gtk-theme changes.
        """
        config.load_theme()

    def on_gtk_font_dpi_changed(self):
        """
        Refresh the key's pango layout objects so that they can adapt
        to the new system dpi setting.
        """
        self.keyboard_widget.refresh_pango_layouts()
        self._update_ui()

        return False

    def on_theme_changed(self, theme):
        config.apply_theme()
        self.reload_layout()

    def _keep_windows_on_top(self, enable=True):
        if not config.xid_mode: # be defensive, not necessary when embedding
            if enable:
                windows = [self._window, self._window.icp]
            else:
                windows = []
            _logger.debug("keep_windows_on_top {}".format(windows))
            self._osk_util.keep_windows_on_top(windows)

    def on_focusable_gui_opening(self):
        self._keep_windows_on_top(False)

    def on_focusable_gui_closed(self):
        self._keep_windows_on_top(True)

    def reload_layout_and_present(self):
        """
        Reload the layout and briefly show the window
        with active transparency
        """
        self.reload_layout(force_update = True)
        self.keyboard_widget.update_transparency()

    def reload_layout_delayed(self):
        """
        Delay reloading the layout on keyboard map or group changes
        This is mainly for LP #1313176 when Caps-lock is set up as
        an accelerator to switch between keyboard "input sources" (layouts)
        in unity-control-center->Text Entry (aka region panel).
        Without waiting until after the shortcut turns off numlock,
        the next "input source" (layout) is skipped and a second one
        is selected.
        """
        self._reload_layout_timer.start(.5, self.reload_layout)

    def reload_layout(self, force_update=False):
        """
        Checks if the X keyboard layout has changed and
        (re)loads Onboards layout accordingly.
        """
        keyboard_state = (None, None)

        vk = self.get_vk()
        if vk:
            try:
                vk.reload() # reload keyboard names
                keyboard_state = (vk.get_layout_as_string(),
                                  vk.get_current_group_name())
            except osk.error:
                self.reset_vk()
                force_update = True
                _logger.warning("Keyboard layout changed, but retrieving "
                                "keyboard information failed")

        if self.keyboard_state != keyboard_state or force_update:
            self.keyboard_state = keyboard_state

            layout_filename = config.layout_filename
            color_scheme_filename = config.theme_settings.color_scheme_filename

            try:
                self.load_layout(layout_filename, color_scheme_filename)
            except LayoutFileError as ex:
                _logger.error("Layout error: " + unicode_str(ex) + ". Falling back to default layout.")

                # last ditch effort to load the default layout
                self.load_layout(config.get_fallback_layout_filename(),
                                 color_scheme_filename)

        # if there is no X keyboard, poll until it appears (if ever)
        if not vk and not self.vk_timer:
            self.vk_timer = GLib.timeout_add_seconds(1, self.cb_vk_timer)

    def load_layout(self, layout_filename, color_scheme_filename):
        _logger.info("Loading keyboard layout " + layout_filename)
        if (color_scheme_filename):
            _logger.info("Loading color scheme " + color_scheme_filename)

        vk = self.get_vk()

        color_scheme = ColorScheme.load(color_scheme_filename) \
                       if color_scheme_filename else None
        layout = LayoutLoaderSVG().load(vk, layout_filename, color_scheme)

        self.keyboard.set_layout(layout, color_scheme, vk)

        if self._window and self._window.icp:
            self._window.icp.queue_draw()

    def get_vk(self):
        if not self._vk:
            try:
                # may fail if there is no X keyboard (LP: 526791)
                self._vk = osk.Virtkey()

            except osk.error as e:
                t = time.time()
                if t > self._vk_error_time + .2: # rate limit to once per 200ms
                    _logger.warning("vk: " + unicode_str(e))
                    self._vk_error_time = t

        return self._vk

    def reset_vk(self):
        self._vk = None
        self._vk_error_time = 0


    # Methods concerning the application
    def emit_quit_onboard(self):
        self._window.emit_quit_onboard()

    def do_quit_onboard(self):
        _logger.debug("Entered do_quit_onboard")
        self.final_cleanup()
        self.cleanup()

    def cleanup(self):
        self._reload_layout_timer.stop()

        config.cleanup()

        # Make an effort to disconnect all handlers.
        # Used to be used for safe restarting.
        for instance, handler_id in self._connections:
            instance.disconnect(handler_id)

        if self.keyboard:
            if self.keyboard.scanner:
                self.keyboard.scanner.finalize()
                self.keyboard.scanner = None
            self.keyboard.cleanup()

        self.status_icon.cleanup()
        self.status_icon = None

        self._window.cleanup()
        self._window.destroy()
        self._window = None
        Gtk.main_quit()


    def final_cleanup(self):
        config.final_cleanup()

    @staticmethod
    def _can_show_in_current_desktop(bus):
        """
        When GNOME's "Typing Assistent" is enabled in GNOME Shell, Onboard
        starts simultaneously with the Shell's built-in screen keyboard.
        With GNOME Shell 3.5.4-0ubuntu2 there is no known way to choose
        one over the other (LP: 879942).

        Adding NotShowIn=GNOME; to onboard-autostart.desktop prevents it
        from running not only in GNOME Shell, but also in the GMOME Fallback
        session, which is undesirable. Both share the same xdg-desktop name.

        -> Do it ourselves: optionally check for GNOME Shell and yield to the
        built-in keyboard.
        """
        result = True

        # Content of XDG_CURRENT_DESKTOP:
        # Before Vivid: GNOME Shell:   GNOME
        #               GNOME Classic: GNOME
        # Since Vivid:  GNOME Shell:   GNOME
        #               GNOME Classic: GNOME-Classic:GNOME

        if config.options.not_show_in:
            current = os.environ.get("XDG_CURRENT_DESKTOP", "")
            names = config.options.not_show_in.split(",")
            for name in names:
                if name == current:
                    # Before Vivid GNOME Shell and GNOME Classic had the same
                    # desktop name "GNOME". Use the D-BUS name to decide if we
                    # are in the Shell.
                    if name == "GNOME":
                        if bus and bus.name_has_owner("org.gnome.Shell"):
                            result = False
                    else:
                        result  = False

            if not result:
                _logger.info("Command line option not-show-in={} forbids running in "
                             "current desktop environment '{}'; exiting." \
                             .format(names, current))
        return result
Beispiel #27
0
class ScanMode(Timer):
    """
    Abstract base class for all scanning modes.

    Specifies how the scanner moves between chunks of keys
    and when to activate them. Scan mode subclasses define
    a set of actions they support and the base class translates
    input device events into scan actions.

    Hierarchy:
        ScanMode --> AutoScan --> UserScan
                              --> OverScan
                 --> StepScan
                 --> DirectScan
    """

    """ Scan actions """
    ACTION_STEP       = 0
    ACTION_LEFT       = 1
    ACTION_RIGHT      = 2
    ACTION_UP         = 3
    ACTION_DOWN       = 4
    ACTION_ACTIVATE   = 5
    ACTION_STEP_START = 6
    ACTION_STEP_STOP  = 7
    ACTION_UNHANDLED  = 8

    """ Time between key activation flashes (in sec) """
    ACTIVATION_FLASH_INTERVAL = 0.1

    """ Number of key activation flashes """
    ACTIVATION_FLASH_COUNT = 4

    def __init__(self, redraw_callback, activate_callback):
        super(ScanMode, self).__init__()

        logger.debug("ScanMode.__init__()")

        """ Activation timer instance """
        self._activation_timer = Timer()

        """ Counter for key flash animation """
        self._flash = 0

        """ Callback for key redraws """
        self._redraw_callback = redraw_callback

        """ Callback for key activation """
        self._activate_callback = activate_callback

        """ A Chunker instance """
        self.chunker = None

    def __del__(self):
        logger.debug("ScanMode.__del__()")

    def map_actions(self, detail, pressed):
        """
        Abstract: Convert input events into scan actions.
        """
        raise NotImplementedError()

    def do_action(self, action):
        """
        Abstract: Handle scan actions.
        """
        raise NotImplementedError()

    def scan(self):
        """
        Abstract: Move between chunks.
        """
        raise NotImplementedError()

    def create_chunker(self):
        """
        Abstract: Create a chunker instance.
        """
        raise NotImplementedError()

    def init_position(self):
        """
        Virtual: Called if a new layer was set or a key activated.
        """
        pass

    def handle_event(self, event):
        """
        Translate device events into scan actions.
        """
        # Ignore events during key activation
        if self._activation_timer.is_running():
            return

        event_type = event.xi_type
        if event_type == XIEventType.ButtonPress:
            button_map = config.scanner.device_button_map
            action = self.map_actions(button_map, event.button, True)

        elif event_type == XIEventType.ButtonRelease:
            button_map = config.scanner.device_button_map
            action = self.map_actions(button_map, event.button, False)

        elif event_type == XIEventType.KeyPress:
            key_map = config.scanner.device_key_map
            action = self.map_actions(key_map, event.keyval, True)

        elif event_type == XIEventType.KeyRelease:
            key_map = config.scanner.device_key_map
            action = self.map_actions(key_map, event.keyval, False)

        else:
            action = self.ACTION_UNHANDLED

        if action != self.ACTION_UNHANDLED:
            self.do_action(action)

    def on_timer(self):
        """
        Override: Timer() callback.
        """
        return self.scan()

    def max_cycles_reached(self):
        """
        Check if the maximum number of scan cycles is reached.
        """
        return self.chunker.cycles >= config.scanner.cycles

    def set_layer(self, layout, layer):
        """
        Set the layer that should be scanned.
        """
        self.reset()
        self.chunker = self.create_chunker()
        self.chunker.chunk(layout, layer)
        self.init_position()

    def _on_activation_timer(self, key):
        """
        Timer callback: Flashes the key and finally activates it.
        """
        if self._flash > 0:
            key.scanned = not key.scanned
            self._flash -= 1
            self.redraw([key])
            return True
        else:
            self._activate_callback(key)
            self.init_position()
            return False

    def activate(self):
        """
        Activates a key and triggers feedback.
        """
        key = self.chunker.get_key()
        if not key:
            return

        if config.scanner.feedback_flash:
            self._flash = self.ACTIVATION_FLASH_COUNT
            self._activation_timer.start(self.ACTIVATION_FLASH_INTERVAL,
                                         self._on_activation_timer,
                                         key)
        else:
            self._activate_callback(key)
            self.init_position()

    def reset(self):
        """
        Stop scanning and clear all highlights.
        """
        if self.is_running():
            self.stop()

        if self.chunker:
            self.redraw(self.chunker.highlight_all(False))

    def redraw(self, keys=None):
        """
        Update individual keys or the entire keyboard.
        """
        self._redraw_callback(keys)

    def finalize(self):
        """
        Clean up the ScanMode instance.
        """
        self.reset()
        self._activation_timer = None