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
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
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
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
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
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()
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()
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