def __init__(self, wp): self._wp = wp self._accessible = None self._can_insert_text = False self._text_domains = TextDomains() self._text_domain = self._text_domains.get_nop_domain() self._changes = TextChanges() self._entering_text = False self._text_changed = False self._context = "" self._line = "" self._line_caret = 0 self._selection_span = TextSpan() self._begin_of_text = False # context starts at begin of text? self._begin_of_text_offset = None # offset of text begin self._pending_separator_span = None self._last_text_change_time = 0 self._last_caret_move_time = 0 self._last_caret_move_position = 0 self._last_context = None self._last_line = None self._update_context_timer = Timer() self._update_context_delay_normal = 0.01 self._update_context_delay = self._update_context_delay_normal
def __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 __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 __init__(self, redraw_callback, activate_callback): super(ScanMode, self).__init__() logger.debug("ScanMode.__init__()") """ Activation timer instance """ self._activation_timer = Timer() """ Counter for key flash animation """ self._flash = 0 """ Callback for key redraws """ self._redraw_callback = redraw_callback """ Callback for key activation """ self._activate_callback = activate_callback """ A Chunker instance """ self.chunker = None
def 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 __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 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 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__(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 __init__(self, redraw_callback, activate_callback): super(ScanMode, self).__init__() logger.debug("ScanMode.__init__()") """ Activation timer instance """ self._activation_timer = Timer() """ Counter for key flash animation """ self._flash = 0 """ Callback for key redraws """ self._redraw_callback = redraw_callback """ Callback for key activation """ self._activate_callback = activate_callback """ A Chunker instance """ self.chunker = None
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, 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
def _on_dwell_begin_timer(self): self._dwell_progress.start_dwelling() self._dwell_timer = Timer(0.025, self._on_dwell_timer) return False
def _start_dwelling(self): self._stop_dwelling() self._dwell_begin_timer = Timer(1.5, self._on_dwell_begin_timer) self._no_more_dwelling = 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
def init(self): self.keyboard_state = None self.vk_timer = None self.reset_vk() self._connections = [] self._window = None self.status_icon = None self.service_keyboard = None self._reload_layout_timer = Timer() # finish config initialization config.init() # Release pressed keys when onboard is killed. # Don't keep enter key stuck when being killed by lightdm. self._osk_util = osk.Util() self._osk_util.set_unix_signal_handler(signal.SIGTERM, self.on_sigterm) self._osk_util.set_unix_signal_handler(signal.SIGINT, self.on_sigint) sys.path.append(os.path.join(config.install_dir, 'scripts')) # Create the central keyboard model self.keyboard = Keyboard(self) # Create the initial keyboard widget # Care for toolkit independence only once there is another # supported one besides GTK. self.keyboard_widget = KeyboardWidget(self.keyboard) # create the main window if config.xid_mode: # XEmbed mode for gnome-screensaver? # no icp, don't flash the icon palette in lightdm self._window = KbdPlugWindow(self.keyboard_widget) # write xid to stdout sys.stdout.write('%d\n' % self._window.get_id()) sys.stdout.flush() else: icp = IconPalette(self.keyboard) icp.set_layout_view(self.keyboard_widget) icp.connect("activated", self._on_icon_palette_acticated) self.do_connect(icp.get_menu(), "quit-onboard", lambda x: self.do_quit_onboard()) self._window = KbdWindow(self.keyboard_widget, icp) self.do_connect(self._window, "quit-onboard", lambda x: self.do_quit_onboard()) # config.xid_mode = True self._window.application = self # need this to access screen properties config.main_window = self._window # load the initial layout _logger.info("Loading initial layout") self.reload_layout() # Handle command line options x, y, size after window creation # because the rotation code needs the window's screen. if not config.xid_mode: rect = self._window.get_rect().copy() options = config.options if options.size: size = options.size.split("x") rect.w = int(size[0]) rect.h = int(size[1]) if options.x is not None: rect.x = options.x if options.y is not None: rect.y = options.y # Make sure the keyboard fits on screen rect = self._window.limit_size(rect) if rect != self._window.get_rect(): orientation = self._window.get_screen_orientation() self._window.write_window_rect(orientation, rect) self._window.restore_window_rect() # move/resize early # export dbus service if not config.xid_mode and \ "dbus" in globals(): self.service_keyboard = ServiceOnboardKeyboard(self) # show/hide the window self.keyboard_widget.set_startup_visibility() # keep keyboard window and icon palette on top of dash self._keep_windows_on_top() # connect notifications for keyboard map and group changes self.keymap = Gdk.Keymap.get_default() # map changes self.do_connect(self.keymap, "keys-changed", self.cb_keys_changed) self.do_connect(self.keymap, "state-changed", self.cb_state_changed) # group changes Gdk.event_handler_set(cb_any_event, self) # connect config notifications here to keep config from holding # references to keyboard objects. once = CallOnce(50).enqueue # delay callbacks by 50ms reload_layout = lambda x: once(self.reload_layout_and_present) update_ui = lambda x: once(self._update_ui) update_ui_no_resize = lambda x: once(self._update_ui_no_resize) update_transparency = \ lambda x: once(self.keyboard_widget.update_transparency) update_inactive_transparency = \ lambda x: once(self.keyboard_widget.update_inactive_transparency) # general config.auto_show.enabled_notify_add(lambda x: self.keyboard.update_auto_show()) config.auto_show.hide_on_key_press_notify_add(lambda x: self.keyboard.update_auto_hide()) # keyboard config.keyboard.key_synth_notify_add(reload_layout) config.keyboard.input_event_source_notify_add(lambda x: self.keyboard.update_input_event_source()) config.keyboard.touch_input_notify_add(lambda x: self.keyboard.update_touch_input_mode()) config.keyboard.show_secondary_labels_notify_add(update_ui) # window config.window.window_state_sticky_notify_add( lambda x: self._window.update_sticky_state()) config.window.window_decoration_notify_add( self._on_window_options_changed) config.window.force_to_top_notify_add(self._on_window_options_changed) config.window.keep_aspect_ratio_notify_add(update_ui) config.window.transparency_notify_add(update_transparency) config.window.background_transparency_notify_add(update_transparency) config.window.transparent_background_notify_add(update_ui) config.window.enable_inactive_transparency_notify_add(update_transparency) config.window.inactive_transparency_notify_add(update_inactive_transparency) config.window.docking_notify_add(self._update_docking) # layout config.layout_filename_notify_add(reload_layout) # theme # config.gdi.gtk_theme_notify_add(self.on_gtk_theme_changed) config.theme_notify_add(self.on_theme_changed) config.key_label_font_notify_add(reload_layout) config.key_label_overrides_notify_add(reload_layout) config.theme_settings.color_scheme_filename_notify_add(reload_layout) config.theme_settings.key_label_font_notify_add(reload_layout) config.theme_settings.key_label_overrides_notify_add(reload_layout) config.theme_settings.theme_attributes_notify_add(update_ui) # snippets config.snippets_notify_add(reload_layout) # word suggestions config.word_suggestions.show_context_line_notify_add(update_ui) config.word_suggestions.enabled_notify_add(lambda x: self.keyboard.on_word_suggestions_enabled(x)) config.word_suggestions.auto_learn_notify_add( update_ui_no_resize) config.typing_assistance.active_language_notify_add(lambda x: \ self.keyboard.on_active_lang_id_changed()) config.typing_assistance.spell_check_backend_notify_add(lambda x: \ self.keyboard.on_spell_checker_changed()) config.typing_assistance.auto_capitalization_notify_add(lambda x: \ self.keyboard.on_word_suggestions_enabled(x)) config.word_suggestions.spelling_suggestions_enabled_notify_add(lambda x: \ self.keyboard.on_spell_checker_changed()) config.word_suggestions.delayed_word_separators_enabled_notify_add(lambda x: \ self.keyboard.on_punctuator_changed()) config.word_suggestions.wordlist_buttons_notify_add( update_ui_no_resize) # universal access config.scanner.enabled_notify_add(self.keyboard._on_scanner_enabled) config.window.window_handles_notify_add(self._on_window_handles_changed) # misc config.keyboard.show_click_buttons_notify_add(update_ui) config.lockdown.lockdown_notify_add(update_ui) if config.mousetweaks: config.mousetweaks.state_notify_add(update_ui_no_resize) # create status icon self.status_icon = Indicator() self.status_icon.set_keyboard(self.keyboard) self.do_connect(self.status_icon.get_menu(), "quit-onboard", lambda x: self.do_quit_onboard()) # Callbacks to use when icp or status icon is toggled config.show_status_icon_notify_add(self.show_hide_status_icon) config.icp.in_use_notify_add(self.cb_icp_in_use_toggled) self.show_hide_status_icon(config.show_status_icon) # Minimize to IconPalette if running under GDM if 'RUNNING_UNDER_GDM' in os.environ: _logger.info("RUNNING_UNDER_GDM set, turning on icon palette") config.icp.in_use = True _logger.info("RUNNING_UNDER_GDM set, turning off indicator") config.show_status_icon = False # For some reason the new values don't arrive in gsettings when # running the unit test "test_running_in_live_cd_environment". # -> Force gsettings to apply them, that seems to do the trick. config.icp.apply() config.apply() # unity-2d needs the skip-task-bar hint set before the first mapping. self.show_hide_taskbar() # Check gnome-screen-saver integration # onboard_xembed_enabled False True True True # config.gss.embedded_keyboard_enabled any False any False # config.gss.embedded_keyboard_command any empty !=onboard ==onboard # Action: nop enable Question1 Question2 # silently if not config.xid_mode and \ config.onboard_xembed_enabled: # If it appears, that nothing has touched the gss keys before, # silently enable gss integration with onboard. if not config.gss.embedded_keyboard_enabled and \ not config.gss.embedded_keyboard_command: config.enable_gss_embedding(True) # If onboard is configured to be embedded into the unlock screen # dialog, and the embedding command is different from onboard, ask # the user what to do elif not config.is_onboard_in_xembed_command_string(): question = _("Onboard is configured to appear with the dialog to " "unlock the screen; for example to dismiss the " "password-protected screensaver.\n\n" "However the system is not configured anymore to use " "Onboard to unlock the screen. A possible reason can " "be that another application configured the system to " "use something else.\n\n" "Would you like to reconfigure the system to show " "Onboard when unlocking the screen?") _logger.warning("showing dialog: '{}'".format(question)) reply = show_confirmation_dialog(question, self._window, config.is_force_to_top()) if reply == True: config.enable_gss_embedding(True) else: config.onboard_xembed_enabled = False else: if not config.gss.embedded_keyboard_enabled: question = _("Onboard is configured to appear with the dialog " "to unlock the screen; for example to dismiss " "the password-protected screensaver.\n\n" "However this function is disabled in the system.\n\n" "Would you like to activate it?") _logger.warning("showing dialog: '{}'".format(question)) reply = show_confirmation_dialog(question, self._window, config.is_force_to_top()) if reply == True: config.enable_gss_embedding(True) else: config.onboard_xembed_enabled = False # check if gnome accessibility is enabled for auto-show if (config.is_auto_show_enabled() or \ config.are_word_suggestions_enabled()) and \ not config.check_gnome_accessibility(self._window): config.auto_show.enabled = False
class AtspiStateTracker(EventSource): """ Keeps track of the currently active accessible by listening to AT-SPI focus events. """ _focus_event_names = ("text-entry-activated", ) _text_event_names = ("text-changed", "text-caret-moved") _key_stroke_event_names = ("key-pressed", ) _async_event_names = ("async-focus-changed", "async-text-changed", "async-text-caret-moved") _event_names = (_async_event_names + _focus_event_names + _text_event_names + _key_stroke_event_names) _focus_listeners_registered = False _keystroke_listeners_registered = False _text_listeners_registered = False _keystroke_listener = None # asynchronously accessible members _focused_accessible = None # last focused editable accessible _focused_pid = None # pid of last focused editable accessible _active_accessible = None # currently active editable accessible _active_accessible_activation_time = 0.0 # time since focus received _last_active_accessible = None _poll_unity_timer = Timer() def __new__(cls, *args, **kwargs): """ Singleton magic. """ if not hasattr(cls, "self"): cls.self = object.__new__(cls, *args, **kwargs) cls.self.construct() return cls.self def __init__(self): """ Called multiple times, don't use this. """ pass def construct(self): """ Singleton constructor, runs only once. """ EventSource.__init__(self, self._event_names) self._frozen = False def cleanup(self): EventSource.cleanup(self) self._register_atspi_listeners(False) def connect(self, event_name, callback): EventSource.connect(self, event_name, callback) self._update_listeners() def disconnect(self, event_name, callback): had_listeners = self.has_listeners(self._event_names) EventSource.disconnect(self, event_name, callback) self._update_listeners() # help debugging disconnecting events on exit if had_listeners and not self.has_listeners(self._event_names): _logger.info("all listeners disconnected") def _update_listeners(self): register = self.has_listeners(self._focus_event_names) self._register_atspi_focus_listeners(register) register = self.has_listeners(self._text_event_names) self._register_atspi_text_listeners(register) register = self.has_listeners(self._key_stroke_event_names) self._register_atspi_keystroke_listeners(register) def _register_atspi_listeners(self, register): self._register_atspi_focus_listeners(register) self._register_atspi_text_listeners(register) self._register_atspi_keystroke_listeners(register) def _register_atspi_focus_listeners(self, register): if "Atspi" not in globals(): return if self._focus_listeners_registered != register: if register: self.atspi_connect("_listener_focus", "focus", self._on_atspi_global_focus) self.atspi_connect("_listener_object_focus", "object:state-changed:focused", self._on_atspi_object_focus) # private asynchronous events for name in self._async_event_names: handler = "_on_" + name.replace("-", "_") EventSource.connect(self, name, getattr(self, handler)) else: self._poll_unity_timer.stop() self.atspi_disconnect("_listener_focus", "focus") self.atspi_disconnect("_listener_object_focus", "object:state-changed:focused") for name in self._async_event_names: handler = "_on_" + name.replace("-", "_") EventSource.disconnect(self, name, getattr(self, handler)) self._focus_listeners_registered = register def _register_atspi_text_listeners(self, register): if "Atspi" not in globals(): return if self._text_listeners_registered != register: if register: self.atspi_connect("_listener_text_changed", "object:text-changed:insert", self._on_atspi_text_changed) self.atspi_connect("_listener_text_changed", "object:text-changed:delete", self._on_atspi_text_changed) self.atspi_connect("_listener_text_caret_moved", "object:text-caret-moved", self._on_atspi_text_caret_moved) else: self.atspi_disconnect("_listener_text_changed", "object:text-changed:insert") self.atspi_disconnect("_listener_text_changed", "object:text-changed:delete") self.atspi_disconnect("_listener_text_caret_moved", "object:text-caret-moved") self._text_listeners_registered = register def _register_atspi_keystroke_listeners(self, register): if "Atspi" not in globals(): return if self._keystroke_listeners_registered != register: modifier_masks = range(16) if register: if not self._keystroke_listener: self._keystroke_listener = \ Atspi.DeviceListener.new(self._on_atspi_keystroke, None) for modifier_mask in modifier_masks: Atspi.register_keystroke_listener( self._keystroke_listener, None, # key set, None=all modifier_mask, Atspi.KeyEventType.PRESSED, Atspi.KeyListenerSyncType.SYNCHRONOUS) else: # Apparently any single deregister call will turn off # all the other registered modifier_masks too. Since # deregistering takes extremely long (~2.5s for 16 calls) # seize the opportunity and just pick a single arbitrary # mask (Quantal). modifier_masks = [2] for modifier_mask in modifier_masks: Atspi.deregister_keystroke_listener( self._keystroke_listener, None, # key set, None=all modifier_mask, Atspi.KeyEventType.PRESSED) self._keystroke_listeners_registered = register def atspi_connect(self, attribute, event, callback): """ Start listening to an AT-SPI event. Creates a new event listener for each event, since this seems to be the only way to allow reliable deregistering of events. """ if hasattr(self, attribute): listener = getattr(self, attribute) else: listener = None if listener is None: listener = Atspi.EventListener.new(callback, None) setattr(self, attribute, listener) listener.register(event) def atspi_disconnect(self, attribute, event): """ Stop listening to AT-SPI event. """ listener = getattr(self, attribute) listener.deregister(event) def freeze(self): """ Freeze AT-SPI message processing, e.g. while displaying a dialog or popoup menu. """ self._register_atspi_listeners(False) self._frozen = True def thaw(self): """ Resume AT-SPI message processing. """ self._update_listeners() self._frozen = False def emit_async(self, event_name, *args, **kwargs): if not self._frozen: EventSource.emit_async(self, event_name, *args, **kwargs) def _get_cached_accessible(self, accessible): return CachedAccessible(accessible) \ if accessible else None # ######### synchronous handlers ######### # def _on_atspi_global_focus(self, event, user_data): self._on_atspi_focus(event, True) def _on_atspi_object_focus(self, event, user_data): self._on_atspi_focus(event) def _on_atspi_focus(self, event, focus_received=False): focused = (bool(focus_received) or bool(event.detail1)) # received focus? ae = AsyncEvent(accessible=self._get_cached_accessible(event.source), focused=focused) self.emit_async("async-focus-changed", ae) def _on_atspi_text_changed(self, event, user_data): # print("_on_atspi_text_changed", event.detail1, event.detail2, # event.source, event.type, event.type.endswith("delete")) ae = AsyncEvent(accessible=self._get_cached_accessible(event.source), type=event.type, pos=event.detail1, length=event.detail2) self.emit_async("async-text-changed", ae) return False def _on_atspi_text_caret_moved(self, event, user_data): # print("_on_atspi_text_caret_moved", event.detail1, event.detail2, # event.source, event.type, event.source.get_name(), # event.source.get_role()) ae = AsyncEvent(accessible=self._get_cached_accessible(event.source), caret=event.detail1) self.emit_async("async-text-caret-moved", ae) return False def _on_atspi_keystroke(self, event, user_data): if event.type == Atspi.EventType.KEY_PRESSED_EVENT: _logger.atspi("key-stroke {} {} {} {}".format( event.modifiers, event.hw_code, event.id, event.is_text)) # keysym = event.id # What is this? Not an XK_ keysym apparently. ae = AsyncEvent(hw_code=event.hw_code, modifiers=event.modifiers) self.emit_async("key-pressed", ae) return False # don't consume event # ######### asynchronous handlers ######### # def _on_async_focus_changed(self, event): accessible = event.accessible focused = event.focused # Don't access the accessible while frozen. This leads to deadlocks # while displaying Onboard's own dialogs/popup menu's. if self._frozen: return self._log_accessible(accessible, focused) if not accessible: return app_name = accessible.get_app_name().lower() if app_name == "unity": self._handle_focus_changed_unity(event) else: self._handle_focus_changed_apps(event) def _handle_focus_changed_apps(self, event): """ Focus change in regular applications """ accessible = event.accessible focused = event.focused # Since Trusty, focus events no longer come reliably in a # predictable order. -> Store the last editable accessible # so we can pick it over later focused non-editable ones. # Helps to keep the keyboard open in presence of popup selections # e.g. in GNOME's file dialog and in Unity Dash. if self._focused_accessible == accessible: if not focused: self._focused_accessible = None else: pid = accessible.get_pid() if focused: self._poll_unity_timer.stop() if accessible.is_editable(): self._focused_accessible = accessible self._focused_pid = pid # Static accessible, i.e. something that cannot # accidentally steal the focus from an editable # accessible. e.g. firefox ATSPI_ROLE_DOCUMENT_FRAME? elif accessible.is_not_focus_stealing(): self._focused_accessible = None self._focused_pid = None else: # Wily: attempt to hide when unity dash closes # (there's no focus lost event). # Also check duration since last activation to # skip out of order focus events (firefox # ATSPI_ROLE_DOCUMENT_FRAME) for a short while # after opening dash. now = time.time() if focused and \ now - self._active_accessible_activation_time > .5: if self._focused_pid != pid: self._focused_accessible = None _logger.atspi("Dropping accessible due to " "pid change: {} != {} ".format( self._focused_pid, pid)) # Has the previously focused accessible lost the focus? active_accessible = self._focused_accessible if active_accessible and \ not active_accessible.is_focused(True): # Zesty: Firefox 50+ loses focus of the URL entry after # typing just a few letters and focuses a completion # menu item instead. Let's pretend the accessible is # still focused in that case. is_firefox_completion = \ self._focused_accessible.is_urlbar() and \ accessible.get_role() == Atspi.Role.MENU_ITEM if not is_firefox_completion: active_accessible = None self._set_active_accessible(active_accessible) def _handle_focus_changed_unity(self, event): """ Focus change in Unity Dash """ accessible = event.accessible focused = event.focused # Wily: prevent random icons, buttons and toolbars # in unity dash from hiding Onboard. Somehow hovering # over those buttons silently drops the focus from the # text entry. Let's pretend the buttons don't exist # and keep the previously saved text entry active. # Zesty: Don't fight lost focus events anymore, only # react to focus events when the text entry gains focus. if focused and \ accessible.is_editable(): self._focused_accessible = accessible self._set_active_accessible(accessible) # For hiding we poll Dash's toplevel accessible def _poll_unity_dash(): frame = accessible.get_frame() state_set = frame.get_state_set() _logger.debug("polling unity dash state_set: {}".format( AtspiStateType.to_strings(state_set))) if not state_set or \ not state_set.contains(Atspi.StateType.ACTIVE): self._focused_accessible = None self._set_active_accessible(None) return False return True # Only ever start polling if Dash is "ACTIVE". # The state_set might change in the future and the # keyboard better fail to auto-hide than to never show. frame = accessible.get_frame() state_set = frame.get_state_set() _logger.debug("dash focused, state_set: {}".format( AtspiStateType.to_strings(state_set))) if state_set and \ state_set.contains(Atspi.StateType.ACTIVE): self._poll_unity_timer.start(0.5, _poll_unity_dash) def _set_active_accessible(self, accessible): if self._active_accessible != accessible: self._active_accessible = accessible if self._active_accessible or \ self._last_active_accessible: # notify listeners self.emit("text-entry-activated", self._active_accessible) self._last_active_accessible = self._active_accessible self._active_accessible_activation_time = time.time() def _on_async_text_changed(self, event): if event.accessible == self._active_accessible: type = event.type insert = type.endswith(("insert", "insert:system")) delete = type.endswith(("delete", "delete:system")) # print(event.accessible.get_id(), type, insert) if insert or delete: event.insert = insert self.emit("text-changed", event) else: _logger.warning("_on_async_text_changed: " "unknown event type '{}'".format(event.type)) def _on_async_text_caret_moved(self, event): if event.accessible == self._active_accessible: self.emit("text-caret-moved", event) def _log_accessible(self, accessible, focused): if _logger.isEnabledFor(_logger.LEVEL_ATSPI): msg = "AT-SPI focus event: focused={}, ".format(focused) msg += "accessible={}, ".format(accessible) if accessible: name = accessible.get_name() role = accessible.get_role() role_name = accessible.get_role_name() state_set = accessible.get_state_set() states = state_set.states editable = state_set.contains(Atspi.StateType.EDITABLE) \ if state_set else None extents = accessible.get_extents() msg += "name={name}, role={role}({role_name}), " \ "editable={editable}, states={states}, " \ "extents={extents}]" \ .format(accessible=accessible, name=repr(name), role=role.value_name if role else role, role_name=repr(role_name), editable=editable, states=states, extents=extents ) _logger.atspi(msg)
class ScanMode(Timer): """ Abstract base class for all scanning modes. Specifies how the scanner moves between chunks of keys and when to activate them. Scan mode subclasses define a set of actions they support and the base class translates input device events into scan actions. Hierarchy: ScanMode --> AutoScan --> UserScan --> OverScan --> StepScan --> DirectScan """ """ Scan actions """ ACTION_STEP = 0 ACTION_LEFT = 1 ACTION_RIGHT = 2 ACTION_UP = 3 ACTION_DOWN = 4 ACTION_ACTIVATE = 5 ACTION_STEP_START = 6 ACTION_STEP_STOP = 7 ACTION_UNHANDLED = 8 """ Time between key activation flashes (in sec) """ ACTIVATION_FLASH_INTERVAL = 0.1 """ Number of key activation flashes """ ACTIVATION_FLASH_COUNT = 4 def __init__(self, redraw_callback, activate_callback): super(ScanMode, self).__init__() logger.debug("ScanMode.__init__()") """ Activation timer instance """ self._activation_timer = Timer() """ Counter for key flash animation """ self._flash = 0 """ Callback for key redraws """ self._redraw_callback = redraw_callback """ Callback for key activation """ self._activate_callback = activate_callback """ A Chunker instance """ self.chunker = None def __del__(self): logger.debug("ScanMode.__del__()") def map_actions(self, detail, pressed): """ Abstract: Convert input events into scan actions. """ raise NotImplementedError() def do_action(self, action): """ Abstract: Handle scan actions. """ raise NotImplementedError() def scan(self): """ Abstract: Move between chunks. """ raise NotImplementedError() def create_chunker(self): """ Abstract: Create a chunker instance. """ raise NotImplementedError() def init_position(self): """ Virtual: Called if a new layer was set or a key activated. """ pass def handle_event(self, event): """ Translate device events into scan actions. """ # Ignore events during key activation if self._activation_timer.is_running(): return event_type = event.xi_type if event_type == XIEventType.ButtonPress: button_map = config.scanner.device_button_map action = self.map_actions(button_map, event.button, True) elif event_type == XIEventType.ButtonRelease: button_map = config.scanner.device_button_map action = self.map_actions(button_map, event.button, False) elif event_type == XIEventType.KeyPress: key_map = config.scanner.device_key_map action = self.map_actions(key_map, event.keyval, True) elif event_type == XIEventType.KeyRelease: key_map = config.scanner.device_key_map action = self.map_actions(key_map, event.keyval, False) else: action = self.ACTION_UNHANDLED if action != self.ACTION_UNHANDLED: self.do_action(action) def on_timer(self): """ Override: Timer() callback. """ return self.scan() def max_cycles_reached(self): """ Check if the maximum number of scan cycles is reached. """ return self.chunker.cycles >= config.scanner.cycles def set_layer(self, layout, layer): """ Set the layer that should be scanned. """ self.reset() self.chunker = self.create_chunker() self.chunker.chunk(layout, layer) self.init_position() def _on_activation_timer(self, key): """ Timer callback: Flashes the key and finally activates it. """ if self._flash > 0: key.scanned = not key.scanned self._flash -= 1 self.redraw([key]) return True else: self._activate_callback(key) self.init_position() return False def activate(self): """ Activates a key and triggers feedback. """ key = self.chunker.get_key() if not key: return if config.scanner.feedback_flash: self._flash = self.ACTIVATION_FLASH_COUNT self._activation_timer.start(self.ACTIVATION_FLASH_INTERVAL, self._on_activation_timer, key) else: self._activate_callback(key) self.init_position() def reset(self): """ Stop scanning and clear all highlights. """ if self.is_running(): self.stop() if self.chunker: self.redraw(self.chunker.highlight_all(False)) def redraw(self, keys=None): """ Update individual keys or the entire keyboard. """ self._redraw_callback(keys) def finalize(self): """ Clean up the ScanMode instance. """ self.reset() self._activation_timer = None
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 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()
def init(self): self.keyboard_state = None self.vk_timer = None self.reset_vk() self._connections = [] self._window = None self.status_icon = None self.service_keyboard = None self._reload_layout_timer = Timer() # finish config initialization config.init() # Release pressed keys when onboard is killed. # Don't keep enter key stuck when being killed by lightdm. self._osk_util = osk.Util() self._osk_util.set_unix_signal_handler(signal.SIGTERM, self.on_sigterm) self._osk_util.set_unix_signal_handler(signal.SIGINT, self.on_sigint) sys.path.append(os.path.join(config.install_dir, 'scripts')) # Create the central keyboard model self.keyboard = Keyboard(self) # Create the initial keyboard widget # Care for toolkit independence only once there is another # supported one besides GTK. self.keyboard_widget = KeyboardWidget(self.keyboard) # create the main window if config.xid_mode: # XEmbed mode for gnome-screensaver? # no icp, don't flash the icon palette in lightdm self._window = KbdPlugWindow(self.keyboard_widget) # write xid to stdout sys.stdout.write('%d\n' % self._window.get_id()) sys.stdout.flush() else: icp = IconPalette(self.keyboard) icp.set_layout_view(self.keyboard_widget) icp.connect("activated", self._on_icon_palette_acticated) self.do_connect(icp.get_menu(), "quit-onboard", lambda x: self.do_quit_onboard()) self._window = KbdWindow(self.keyboard_widget, icp) self.do_connect(self._window, "quit-onboard", lambda x: self.do_quit_onboard()) # config.xid_mode = True self._window.application = self # need this to access screen properties config.main_window = self._window # load the initial layout _logger.info("Loading initial layout") self.reload_layout() # Handle command line options x, y, size after window creation # because the rotation code needs the window's screen. if not config.xid_mode: rect = self._window.get_rect().copy() options = config.options if options.size: size = options.size.split("x") rect.w = int(size[0]) rect.h = int(size[1]) if options.x is not None: rect.x = options.x if options.y is not None: rect.y = options.y # Make sure the keyboard fits on screen rect = self._window.limit_size(rect) if rect != self._window.get_rect(): orientation = self._window.get_screen_orientation() self._window.write_window_rect(orientation, rect) self._window.restore_window_rect() # move/resize early # export dbus service if not config.xid_mode and \ "dbus" in globals(): self.service_keyboard = ServiceOnboardKeyboard(self) # show/hide the window self.keyboard_widget.set_startup_visibility() # keep keyboard window and icon palette on top of dash self._keep_windows_on_top() # connect notifications for keyboard map and group changes self.keymap = Gdk.Keymap.get_default() # map changes self.do_connect(self.keymap, "keys-changed", self.cb_keys_changed) self.do_connect(self.keymap, "state-changed", self.cb_state_changed) # group changes Gdk.event_handler_set(cb_any_event, self) # connect config notifications here to keep config from holding # references to keyboard objects. once = CallOnce(50).enqueue # delay callbacks by 50ms reload_layout = lambda x: once(self.reload_layout_and_present) update_ui = lambda x: once(self._update_ui) update_ui_no_resize = lambda x: once(self._update_ui_no_resize) update_transparency = \ lambda x: once(self.keyboard_widget.update_transparency) update_inactive_transparency = \ lambda x: once(self.keyboard_widget.update_inactive_transparency) # general config.auto_show.enabled_notify_add( lambda x: self.keyboard.update_auto_show()) config.auto_show.hide_on_key_press_notify_add( lambda x: self.keyboard.update_auto_hide()) # keyboard config.keyboard.key_synth_notify_add(reload_layout) config.keyboard.input_event_source_notify_add( lambda x: self.keyboard.update_input_event_source()) config.keyboard.touch_input_notify_add( lambda x: self.keyboard.update_touch_input_mode()) config.keyboard.show_secondary_labels_notify_add(update_ui) # window config.window.window_state_sticky_notify_add( lambda x: self._window.update_sticky_state()) config.window.window_decoration_notify_add( self._on_window_options_changed) config.window.force_to_top_notify_add(self._on_window_options_changed) config.window.keep_aspect_ratio_notify_add(update_ui) config.window.transparency_notify_add(update_transparency) config.window.background_transparency_notify_add(update_transparency) config.window.transparent_background_notify_add(update_ui) config.window.enable_inactive_transparency_notify_add( update_transparency) config.window.inactive_transparency_notify_add( update_inactive_transparency) config.window.docking_notify_add(self._update_docking) # layout config.layout_filename_notify_add(reload_layout) # theme # config.gdi.gtk_theme_notify_add(self.on_gtk_theme_changed) config.theme_notify_add(self.on_theme_changed) config.key_label_font_notify_add(reload_layout) config.key_label_overrides_notify_add(reload_layout) config.theme_settings.color_scheme_filename_notify_add(reload_layout) config.theme_settings.key_label_font_notify_add(reload_layout) config.theme_settings.key_label_overrides_notify_add(reload_layout) config.theme_settings.theme_attributes_notify_add(update_ui) # snippets config.snippets_notify_add(reload_layout) # word suggestions config.word_suggestions.show_context_line_notify_add(update_ui) config.word_suggestions.enabled_notify_add( lambda x: self.keyboard.on_word_suggestions_enabled(x)) config.word_suggestions.auto_learn_notify_add(update_ui_no_resize) config.typing_assistance.active_language_notify_add(lambda x: \ self.keyboard.on_active_lang_id_changed()) config.typing_assistance.spell_check_backend_notify_add(lambda x: \ self.keyboard.on_spell_checker_changed()) config.typing_assistance.auto_capitalization_notify_add(lambda x: \ self.keyboard.on_word_suggestions_enabled(x)) config.word_suggestions.spelling_suggestions_enabled_notify_add(lambda x: \ self.keyboard.on_spell_checker_changed()) config.word_suggestions.delayed_word_separators_enabled_notify_add(lambda x: \ self.keyboard.on_punctuator_changed()) config.word_suggestions.wordlist_buttons_notify_add( update_ui_no_resize) # universal access config.scanner.enabled_notify_add(self.keyboard._on_scanner_enabled) config.window.window_handles_notify_add( self._on_window_handles_changed) # misc config.keyboard.show_click_buttons_notify_add(update_ui) config.lockdown.lockdown_notify_add(update_ui) if config.mousetweaks: config.mousetweaks.state_notify_add(update_ui_no_resize) # create status icon self.status_icon = Indicator() self.status_icon.set_keyboard(self.keyboard) self.do_connect(self.status_icon.get_menu(), "quit-onboard", lambda x: self.do_quit_onboard()) # Callbacks to use when icp or status icon is toggled config.show_status_icon_notify_add(self.show_hide_status_icon) config.icp.in_use_notify_add(self.cb_icp_in_use_toggled) self.show_hide_status_icon(config.show_status_icon) # Minimize to IconPalette if running under GDM if 'RUNNING_UNDER_GDM' in os.environ: _logger.info("RUNNING_UNDER_GDM set, turning on icon palette") config.icp.in_use = True _logger.info("RUNNING_UNDER_GDM set, turning off indicator") config.show_status_icon = False # For some reason the new values don't arrive in gsettings when # running the unit test "test_running_in_live_cd_environment". # -> Force gsettings to apply them, that seems to do the trick. config.icp.apply() config.apply() # unity-2d needs the skip-task-bar hint set before the first mapping. self.show_hide_taskbar() # Check gnome-screen-saver integration # onboard_xembed_enabled False True True True # config.gss.embedded_keyboard_enabled any False any False # config.gss.embedded_keyboard_command any empty !=onboard ==onboard # Action: nop enable Question1 Question2 # silently if not config.xid_mode and \ config.onboard_xembed_enabled: # If it appears, that nothing has touched the gss keys before, # silently enable gss integration with onboard. if not config.gss.embedded_keyboard_enabled and \ not config.gss.embedded_keyboard_command: config.enable_gss_embedding(True) # If onboard is configured to be embedded into the unlock screen # dialog, and the embedding command is different from onboard, ask # the user what to do elif not config.is_onboard_in_xembed_command_string(): question = _( "Onboard is configured to appear with the dialog to " "unlock the screen; for example to dismiss the " "password-protected screensaver.\n\n" "However the system is not configured anymore to use " "Onboard to unlock the screen. A possible reason can " "be that another application configured the system to " "use something else.\n\n" "Would you like to reconfigure the system to show " "Onboard when unlocking the screen?") _logger.warning("showing dialog: '{}'".format(question)) reply = show_confirmation_dialog(question, self._window, config.is_force_to_top()) if reply == True: config.enable_gss_embedding(True) else: config.onboard_xembed_enabled = False else: if not config.gss.embedded_keyboard_enabled: question = _( "Onboard is configured to appear with the dialog " "to unlock the screen; for example to dismiss " "the password-protected screensaver.\n\n" "However this function is disabled in the system.\n\n" "Would you like to activate it?") _logger.warning("showing dialog: '{}'".format(question)) reply = show_confirmation_dialog(question, self._window, config.is_force_to_top()) if reply == True: config.enable_gss_embedding(True) else: config.onboard_xembed_enabled = False # check if gnome accessibility is enabled for auto-show if (config.is_auto_show_enabled() or \ config.are_word_suggestions_enabled()) and \ not config.check_gnome_accessibility(self._window): config.auto_show.enabled = False
class 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 AtspiTextContext(TextContext): """ Keep track of the current text context with AT-SPI """ _state_tracker = AtspiStateTracker() def __init__(self, wp): self._wp = wp self._accessible = None self._can_insert_text = False self._text_domains = TextDomains() self._text_domain = self._text_domains.get_nop_domain() self._changes = TextChanges() self._entering_text = False self._text_changed = False self._context = "" self._line = "" self._line_caret = 0 self._selection_span = TextSpan() self._begin_of_text = False # context starts at begin of text? self._begin_of_text_offset = None # offset of text begin self._pending_separator_span = None self._last_text_change_time = 0 self._last_caret_move_time = 0 self._last_caret_move_position = 0 self._last_context = None self._last_line = None self._update_context_timer = Timer() self._update_context_delay_normal = 0.01 self._update_context_delay = self._update_context_delay_normal def cleanup(self): self._register_atspi_listeners(False) def enable(self, enable): self._register_atspi_listeners(enable) def get_text_domain(self): return self._text_domain def set_pending_separator(self, separator_span=None): """ Remember this separator span for later insertion. """ if self._pending_separator_span is not separator_span: self._pending_separator_span = separator_span def get_pending_separator(self): """ Return current pending separator span or None """ return self._pending_separator_span def get_context(self): """ Returns the predictions context, i.e. some range of text before the caret position. """ if self._accessible is None: return "" # Don't update suggestions in scrolling terminals if self._entering_text or \ not self._text_changed or \ self.can_suggest_before_typing(): return self._context return "" def get_bot_context(self): """ Returns the predictions context with begin of text marker (at text begin). """ context = "" if self._accessible: context = self.get_context() # prepend domain specific begin-of-text marker if self._begin_of_text: marker = self.get_text_begin_marker() if marker: context = marker + " " + context return context def get_pending_bot_context(self): """ Context including bot marker and pending separator. """ context = self.get_bot_context() if self._pending_separator_span is not None: context += self._pending_separator_span.get_span_text() return context def get_line(self): return self._line \ if self._accessible else "" def get_line_caret_pos(self): return self._line_caret \ if self._accessible else 0 def get_line_past_caret(self): return self._line[self._line_caret:] \ if self._accessible else "" def get_selection_span(self): return self._selection_span \ if self._accessible else None def get_span_at_caret(self): if not self._accessible: return None span = self._selection_span.copy() span.length = 0 return span def get_caret(self): return self._selection_span.begin() \ if self._accessible else 0 def get_character_extents(self, offset): accessible = self._accessible if accessible: return self._state_tracker.get_accessible_character_extents( accessible, offset) else: return None def get_text_begin_marker(self): domain = self.get_text_domain() if domain: return domain.get_text_begin_marker() return "" def can_record_insertion(self, accessible, pos, length): domain = self.get_text_domain() if domain: return domain.can_record_insertion(accessible, pos, length) return True def can_suggest_before_typing(self): domain = self.get_text_domain() if domain: return domain.can_suggest_before_typing() return True def can_auto_punctuate(self): domain = self.get_text_domain() if domain: return domain.can_auto_punctuate(self._begin_of_text) return False def get_begin_of_text_offset(self): return self._begin_of_text_offset \ if self._accessible else None def get_changes(self): return self._changes def has_changes(self): """ Are there any changes to learn? """ return not self._changes.is_empty() def clear_changes(self): self._changes.clear() def can_insert_text(self): """ Can delete or insert text into the accessible? """ #return False # support for inserting is spotty: not in firefox, terminal return bool(self._accessible) and self._can_insert_text def delete_text(self, offset, length=1): """ Delete directly, without going through faking key presses. """ self._accessible.delete_text(offset, offset + length) def delete_text_before_caret(self, length=1): """ Delete directly, without going through faking key presses. """ offset = self._accessible.get_caret_offset() self.delete_text(offset - length, length) def insert_text(self, offset, text): """ Insert directly, without going through faking key presses. """ self._accessible.insert_text(offset, text, -1) # Move the caret after insertion if the accessible itself # hasn't done so already. This assumes the insertion begins at # the current caret position, which always happens to be the case # currently. # Only the nautilus rename text entry appears to need this. offset_before = offset offset_after = self._accessible.get_caret_offset() if text and offset_before == offset_after: self._accessible.set_caret_offset(offset_before + len(text)) def insert_text_at_caret(self, text): """ Insert directly, without going through faking key presses. Fails for terminal and firefox, unfortunately. """ offset = self._accessible.get_caret_offset() self.insert_text(offset, text) def _register_atspi_listeners(self, register=True): st = self._state_tracker if register: st.connect("text-entry-activated", self._on_text_entry_activated) st.connect("text-changed", self._on_text_changed) st.connect("text-caret-moved", self._on_text_caret_moved) # st.connect("key-pressed", self._on_atspi_key_pressed) else: st.disconnect("text-entry-activated", self._on_text_entry_activated) st.disconnect("text-changed", self._on_text_changed) st.disconnect("text-caret-moved", self._on_text_caret_moved) # st.disconnect("key-pressed", self._on_atspi_key_pressed) def get_accessible_capabilities(self, accessible, **kwargs): can_insert_text = False attributes = kwargs.get("attributes", {}) interfaces = kwargs.get("interfaces", []) if accessible: # Can insert text via Atspi? # Advantages: # - faster, no individual key presses # - trouble-free insertion of all unicode characters if "EditableText" in interfaces: # Support for atspi text insertion is spotty. # Firefox, LibreOffice Writer, gnome-terminal don't support it, # even if they claim to implement the EditableText interface. # Allow direct text insertion for gtk widgets if self._state_tracker.is_toolkit_gtk3(attributes): can_insert_text = True return can_insert_text def _on_text_entry_activated(self, accessible): # old text_domain still valid here self._wp.on_text_entry_deactivated() # keep track of the active accessible asynchronously self._accessible = accessible self._entering_text = False self._text_changed = False # select text domain matching this accessible state = self._state_tracker.get_state() \ if self._accessible else {} self._text_domain = self._text_domains.find_match(**state) self._text_domain.init_domain() # determine capabilities of this accessible self._can_insert_text = \ self.get_accessible_capabilities(accessible, **state) # log accessible info if _logger.isEnabledFor(_logger.LEVEL_ATSPI): log = _logger.atspi log("-" * 70) log("Accessible focused: ") indent = " " * 4 if self._accessible: state = self._state_tracker.get_state() for key, value in sorted(state.items()): msg = str(key) + "=" if key == "state-set": msg += repr(AtspiStateType.to_strings(value)) elif hasattr(value, "value_name"): # e.g. role msg += value.value_name else: msg += repr(value) log(indent + msg) log(indent + "text_domain: {}" .format(self._text_domain and type(self._text_domain).__name__)) log(indent + "can_insert_text: {}" .format(self._can_insert_text)) else: log(indent + "None") self._update_context() self._wp.on_text_entry_activated() def _on_text_changed(self, event): insertion_span = self._record_text_change(event.pos, event.length, event.insert) # synchronously notify of text insertion if insertion_span: try: caret_offset = self._accessible.get_caret_offset() except: # gi._glib.GError pass else: self._wp.on_text_inserted(insertion_span, caret_offset) self._last_text_change_time = time.time() self._update_context() def _on_text_caret_moved(self, event): self._last_caret_move_time = time.time() self._last_caret_move_position = event.caret self._update_context() self._wp.on_text_caret_moved() def _on_atspi_key_pressed(self, event): """ disabled, Francesco didn't receive any AT-SPI key-strokes. """ # keycode = event.hw_code # uh oh, only keycodes... # # hopefully "c" doesn't move around a lot. # modifiers = event.modifiers # self._handle_key_press(keycode, modifiers) def on_onboard_typing(self, key, mod_mask): if key.is_text_changing(): keycode = 0 if key.is_return(): keycode = KeyCode.KP_Enter else: label = key.get_label() if label == "C" or label == "c": keycode = KeyCode.C self._handle_key_press(keycode, mod_mask) def _handle_key_press(self, keycode, modifiers): if self._accessible: domain = self.get_text_domain() if domain: self._entering_text, end_of_editing = \ domain.handle_key_press(keycode, modifiers) if end_of_editing is True: self._wp.commit_changes() elif end_of_editing is False: self._wp.discard_changes() def _record_text_change(self, pos, length, insert): accessible = self._accessible insertion_span = None char_count = None if accessible: try: char_count = accessible.get_character_count() except: # gi._glib.GError: The application no longer exists # when closing a tab in gnome-terminal. char_count = None if char_count is not None: # record the change spans_to_update = [] if insert: if self._entering_text and \ self.can_record_insertion(accessible, pos, length): if self._wp.is_typing() or length < 30: # Remember all of the insertion, might have been # a pressed snippet or wordlist button. include_length = -1 else: # Remember only the first few characters. # Large inserts can be paste, reload or scroll # operations. Only learn the first word of these. include_length = 2 # simple span for current insertion begin = max(pos - 100, 0) end = min(pos + length + 100, char_count) text = self._state_tracker.get_accessible_text(accessible, begin, end) if text is not None: insertion_span = TextSpan(pos, length, text, begin) else: # Remember nothing, just update existing spans. include_length = None spans_to_update = self._changes.insert(pos, length, include_length) else: spans_to_update = self._changes.delete(pos, length, self._entering_text) # update text of all modified spans for span in spans_to_update: # Get some more text around the span to hopefully # include whole words at beginning and end. begin = max(span.begin() - 100, 0) end = min(span.end() + 100, char_count) span.text = Atspi.Text.get_text(accessible, begin, end) span.text_pos = begin self._text_changed = True return insertion_span def set_update_context_delay(self, delay): self._update_context_delay = delay def reset_update_context_delay(self): self._update_context_delay = self._update_context_delay_normal def _update_context(self): self._update_context_timer.start(self._update_context_delay, self.on_text_context_changed) def on_text_context_changed(self): # Clear pending separator when the user clicked to move # the cursor away from the separator position. if self._pending_separator_span: # Lone caret movement, no recent text change? if self._last_caret_move_time - self._last_text_change_time > 1.0: # Away from the separator? if self._last_caret_move_position != \ self._pending_separator_span.begin(): self.set_pending_separator(None) result = self._text_domain.read_context(self._accessible) if result is not None: (self._context, self._line, self._line_caret, self._selection_span, self._begin_of_text, self._begin_of_text_offset) = result # make sure to include bot-markers and pending separator context = self.get_pending_bot_context() change_detected = (self._last_context != context or self._last_line != self._line) if change_detected: self._last_context = context self._last_line = self._line self._wp.on_text_context_changed(change_detected) return False
def _start_dwelling(self): self._stop_dwelling() self._dwell_begin_timer = Timer(1.5, self._on_dwell_begin_timer) self._no_more_dwelling = True
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 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 ScanMode(Timer): """ Abstract base class for all scanning modes. Specifies how the scanner moves between chunks of keys and when to activate them. Scan mode subclasses define a set of actions they support and the base class translates input device events into scan actions. Hierarchy: ScanMode --> AutoScan --> UserScan --> OverScan --> StepScan --> DirectScan """ """ Scan actions """ ACTION_STEP = 0 ACTION_LEFT = 1 ACTION_RIGHT = 2 ACTION_UP = 3 ACTION_DOWN = 4 ACTION_ACTIVATE = 5 ACTION_STEP_START = 6 ACTION_STEP_STOP = 7 ACTION_UNHANDLED = 8 """ Time between key activation flashes (in sec) """ ACTIVATION_FLASH_INTERVAL = 0.1 """ Number of key activation flashes """ ACTIVATION_FLASH_COUNT = 4 def __init__(self, redraw_callback, activate_callback): super(ScanMode, self).__init__() logger.debug("ScanMode.__init__()") """ Activation timer instance """ self._activation_timer = Timer() """ Counter for key flash animation """ self._flash = 0 """ Callback for key redraws """ self._redraw_callback = redraw_callback """ Callback for key activation """ self._activate_callback = activate_callback """ A Chunker instance """ self.chunker = None def __del__(self): logger.debug("ScanMode.__del__()") def map_actions(self, detail, pressed): """ Abstract: Convert input events into scan actions. """ raise NotImplementedError() def do_action(self, action): """ Abstract: Handle scan actions. """ raise NotImplementedError() def scan(self): """ Abstract: Move between chunks. """ raise NotImplementedError() def create_chunker(self): """ Abstract: Create a chunker instance. """ raise NotImplementedError() def init_position(self): """ Virtual: Called if a new layer was set or a key activated. """ pass def handle_event(self, event): """ Translate device events into scan actions. """ # Ignore events during key activation if self._activation_timer.is_running(): return event_type = event.xi_type if event_type == XIEventType.ButtonPress: button_map = config.scanner.device_button_map action = self.map_actions(button_map, event.button, True) elif event_type == XIEventType.ButtonRelease: button_map = config.scanner.device_button_map action = self.map_actions(button_map, event.button, False) elif event_type == XIEventType.KeyPress: key_map = config.scanner.device_key_map action = self.map_actions(key_map, event.keyval, True) elif event_type == XIEventType.KeyRelease: key_map = config.scanner.device_key_map action = self.map_actions(key_map, event.keyval, False) else: action = self.ACTION_UNHANDLED if action != self.ACTION_UNHANDLED: self.do_action(action) def on_timer(self): """ Override: Timer() callback. """ return self.scan() def max_cycles_reached(self): """ Check if the maximum number of scan cycles is reached. """ return self.chunker.cycles >= config.scanner.cycles def set_layer(self, layout, layer): """ Set the layer that should be scanned. """ self.reset() self.chunker = self.create_chunker() self.chunker.chunk(layout, layer) self.init_position() def _on_activation_timer(self, key): """ Timer callback: Flashes the key and finally activates it. """ if self._flash > 0: key.scanned = not key.scanned self._flash -= 1 self.redraw([key]) return True else: self._activate_callback(key) self.init_position() return False def activate(self): """ Activates a key and triggers feedback. """ key = self.chunker.get_key() if not key: return if config.scanner.feedback_flash: self._flash = self.ACTIVATION_FLASH_COUNT self._activation_timer.start(self.ACTIVATION_FLASH_INTERVAL, self._on_activation_timer, key) else: self._activate_callback(key) self.init_position() def reset(self): """ Stop scanning and clear all highlights. """ if self.is_running(): self.stop() if self.chunker: self.redraw(self.chunker.highlight_all(False)) def redraw(self, keys=None): """ Update individual keys or the entire keyboard. """ self._redraw_callback(keys) def finalize(self): """ Clean up the ScanMode instance. """ self.reset() self._activation_timer = None