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
Exemple #2
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
Exemple #3
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
Exemple #4
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
Exemple #5
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
Exemple #6
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()
Exemple #7
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()
Exemple #8
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