# -*- coding: utf-8 -*- # Copyright © 2009 Chris Jones # Copyright © 2012 Gerd Kohlberger # Copyright © 2009, 2011-2017 marmuta # # This file is part of Onboard. # # Onboard is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # Onboard is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ GTK keyboard widget """ from __future__ import division, print_function, unicode_literals import sys import time from math import sin, pi from Onboard.Version import require_gi_versions require_gi_versions() from gi.repository import GLib, Gdk, Gtk from Onboard.TouchInput import TouchInput, InputSequence from Onboard.Keyboard import EventType from Onboard.KeyboardPopups import LayoutPopup, \ LayoutBuilderAlternatives, \ LayoutBuilder from Onboard.KeyGtk import Key from Onboard.KeyCommon import LOD from Onboard.TouchHandles import TouchHandles from Onboard.LayoutView import LayoutView from Onboard.utils import Rect, escape_markup from Onboard.Timer import Timer, FadeTimer from Onboard.definitions import Handle, HandleFunction from Onboard.WindowUtils import WindowManipulator, \ canvas_to_root_window_rect, \ canvas_to_root_window_point, \ get_monitor_dimensions ### Logging ### import logging _logger = logging.getLogger("KeyboardWidget") ############### ### Config Singleton ### from Onboard.Config import Config config = Config() ######################## # prepare mask for faster access BUTTON123_MASK = Gdk.ModifierType.BUTTON1_MASK | \ Gdk.ModifierType.BUTTON2_MASK | \ Gdk.ModifierType.BUTTON3_MASK class AutoReleaseTimer(Timer): """ Releases latched and locked modifiers after a period of inactivity. Inactivity here means no keys are pressed. """ _keyboard = None def __init__(self, keyboard): self._keyboard = keyboard def start(self, visibility_change = None): self.stop() delay = config.keyboard.sticky_key_release_delay if visibility_change == False: hide_delay = config.keyboard.sticky_key_release_on_hide_delay if hide_delay: if delay: delay = min(delay, hide_delay) else: delay = hide_delay if delay: Timer.start(self, delay) def on_timer(self): # When sticky_key_release_delay is set, release NumLock too. # We then assume Onboard is used in a kiosk setting, and # everything has to be reset for the next customer. release_all_keys = bool(config.keyboard.sticky_key_release_delay) if release_all_keys: config.word_suggestions.set_pause_learning(0) self._keyboard.release_latched_sticky_keys() self._keyboard.release_locked_sticky_keys(release_all_keys) self._keyboard.active_layer_index = 0 self._keyboard.invalidate_ui_no_resize() self._keyboard.commit_ui_updates() return False class InactivityTimer(Timer): """ Waits for the inactivity delay and transitions between active and inactive state. Inactivity here means, the pointer has left the keyboard window. """ _keyboard = None _active = False def __init__(self, keyboard): self._keyboard = keyboard def is_enabled(self): window = self._keyboard.get_kbd_window() if not window: return False screen = window.get_screen() return screen and screen.is_composited() and \ config.is_inactive_transparency_enabled() and \ config.window.enable_inactive_transparency and \ not config.xid_mode def is_active(self): return self._active def begin_transition(self, active): self._active = active if active: Timer.stop(self) if self._keyboard.transition_active_to(True): self._keyboard.commit_transition() else: if not config.xid_mode: Timer.start(self, config.window.inactive_transparency_delay) def on_timer(self): self._keyboard.transition_active_to(False) self._keyboard.commit_transition() return False class HideInputLineTimer(Timer): """ Temporarily hides the input line when the pointer touches it. """ def __init__(self, keyboard): self._keyboard = keyboard def handle_motion(self, sequence): """ Handle pointer motion. """ point = sequence.point # Hide inputline when the pointer touches it. # Show it again when leaving the area. for key in self._keyboard.get_text_displays(): rect = key.get_canvas_border_rect() if rect.is_point_within(point): if not self.is_running(): self.start(0.3) else: self.stop() self._keyboard.hide_input_line(False) def on_timer(self): """ Hide the input line after delay """ self._keyboard.hide_input_line(True) return False class TransitionVariable: """ A variable taking part in opacity transitions """ value = 0.0 start_value = 0.0 target_value = 0.0 start_time = 0.0 duration = 0.0 done = False def start_transition(self, target, duration): """ Begin transition """ self.start_value = self.value self.target_value = target self.start_time = time.time() self.duration = duration self.done = False def update(self): """ Update self.value based on the elapsed time since start_transition. """ range = self.target_value - self.start_value if range and self.duration: elapsed = time.time() - self.start_time lin_progress = min(1.0, elapsed / self.duration) else: lin_progress = 1.0 sin_progress = (sin(lin_progress * pi - pi / 2.0) + 1.0) / 2.0 self.value = self.start_value + sin_progress * range self.done = lin_progress >= 1.0 class TransitionState: """ Set of all state variables involved in opacity transitions. """ def __init__(self): self.visible = TransitionVariable() self.active = TransitionVariable() self.x = TransitionVariable() self.y = TransitionVariable() self._vars = [self.visible, self.active, self.x, self.y] self.target_visibility = False def update(self): for var in self._vars: var.update() def is_done(self): return all(var.done for var in self._vars) def get_max_duration(self): return max(x.duration for x in self._vars) class WindowManipulatorAspectRatio(WindowManipulator): """ Adds support for handles with function ASPECT_RATIO. """ def __init__(self): WindowManipulator.__init__(self) self._docking_aspect_change_range = \ config.window.docking_aspect_change_range def update_docking_aspect_change_range(self): """ GSettings key changed """ value = config.window.docking_aspect_change_range if self._docking_aspect_change_range != value: self._docking_aspect_change_range = value self.keyboard.invalidate_ui() self.keyboard.commit_ui_updates() def get_docking_aspect_change_range(self): return self._docking_aspect_change_range def on_drag_done(self): config.window.docking_aspect_change_range = \ self._docking_aspect_change_range def on_handle_aspect_ratio_pressed(self): self._drag_start_keyboard_frame_rect = self.get_keyboard_frame_rect() def on_handle_aspect_ratio_motion(self, dx, dy): keyboard_frame_rect = self._drag_start_keyboard_frame_rect base_aspect_rect = self.get_base_aspect_rect() base_aspect = base_aspect_rect.w / base_aspect_rect.h start_frame_width = self._drag_start_keyboard_frame_rect.w new_frame_width = start_frame_width + dx * 2 # snap to screen sides if new_frame_width >= self.canvas_rect.w * (1.0 - 0.05): new_aspect_change = 100.0 else: new_aspect_change = \ new_frame_width / (keyboard_frame_rect.h * base_aspect) # limit to minimum combined aspect min_aspect = 0.75 new_aspect = base_aspect * new_aspect_change if new_aspect < min_aspect: new_aspect_change = min_aspect / base_aspect self._docking_aspect_change_range = \ (self._docking_aspect_change_range[0], new_aspect_change) self.update_layout() self.update_touch_handles_positions() self.invalidate_for_resize(self._lod) self.redraw() class KeyboardWidget(Gtk.DrawingArea, WindowManipulatorAspectRatio, LayoutView, TouchInput): TRANSITION_DURATION_MOVE = 0.25 TRANSITION_DURATION_SLIDE = 0.25 TRANSITION_DURATION_OPACITY_HIDE = 0.3 def __init__(self, keyboard): Gtk.DrawingArea.__init__(self) WindowManipulatorAspectRatio.__init__(self) LayoutView.__init__(self, keyboard) TouchInput.__init__(self) self.set_app_paintable(True) self.canvas_rect = Rect() self._opacity = 1.0 self._last_click_time = 0 self._last_click_key = None self._outside_click_timer = Timer() self._outside_click_detected = False self._outside_click_num = 0 self._outside_click_button_mask = 0 self._outside_click_start_time = None self._long_press_timer = Timer() self._auto_release_timer = AutoReleaseTimer(keyboard) self._key_popup = None self.dwell_timer = None self.dwell_key = None self.last_dwelled_key = None self.inactivity_timer = InactivityTimer(self) self.touch_handles = TouchHandles() self.touch_handles_hide_timer = Timer() self.touch_handles_fade = FadeTimer() self.touch_handles_auto_hide = True self._window_aspect_ratio = None self._hide_input_line_timer = HideInputLineTimer(keyboard) self._transition_timer = Timer() self._transition_state = TransitionState() self._transition_state.visible.value = 0.0 self._transition_state.active.value = 1.0 self._transition_state.x.value = 0.0 self._transition_state.y.value = 0.0 self._configure_timer = Timer() self._language_menu = LanguageMenu(self) self._suggestion_menu = SuggestionMenu(self) #self.set_double_buffered(False) self.set_app_paintable(True) # no tooltips when embedding, gnome-screen-saver flickers (Oneiric) if not config.xid_mode: self.set_has_tooltip(True) # works only at window creation -> always on self.connect("parent-set", self._on_parent_set) self.connect("draw", self._on_draw) self.connect("query-tooltip", self._on_query_tooltip) self.connect("configure-event", self._on_configure_event) self._update_double_click_time() self.show() def cleanup(self): # Enter-notify isn't called when resizing without crossing into # the window again. Do it here on exit, at the latest, to make sure # the home_rect is updated before is is saved later. self.stop_system_drag() # stop timer callbacks for unused, but not yet destructed keyboards self.touch_handles_fade.stop() self.touch_handles_hide_timer.stop() self._transition_timer.stop() self.inactivity_timer.stop() self._long_press_timer.stop() self._auto_release_timer.stop() self.stop_click_polling() self._configure_timer.stop() self.close_key_popup() # free xserver memory self.invalidate_keys() self.invalidate_shadows() LayoutView.cleanup(self) TouchInput.cleanup(self) def on_layout_loaded(self): """ called when the layout has been loaded """ LayoutView.on_layout_loaded(self) def _on_parent_set(self, widget, old_parent): win = self.get_kbd_window() if win: self.touch_handles.set_window(win) self.update_window_handles() def set_opacity(self, opacity): """ Override deprecated Gtk function of the same name """ if self._opacity != opacity: self._opacity = opacity self.redraw() def get_opacity(self): """ Override deprecated Gtk function of the same name """ return self._opacity def set_startup_visibility(self): win = self.get_kbd_window() assert(win) # Show the keyboard when turning off auto-show. # Hide the keyboard when turning on auto-show. # (Fix this when we know how to get the active accessible) # Hide the keyboard on start when start-minimized is set. # Start with active transparency if the inactivity_timer is enabled. # # start_minimized False True False True # auto_show False False True True # -------------------------------------------------- # window visible on start True False False False visible = config.is_visible_on_start() # Start with low opacity to stop opacity flashing # when inactive transparency is enabled. screen = self.get_screen() if screen and screen.is_composited() and \ self.inactivity_timer.is_enabled(): self.set_opacity(0.05) # keep it slightly visible just in case # transition to initial opacity self.transition_visible_to(visible, 0.0, 0.4) self.transition_active_to(True, 0.0) self.commit_transition() # kick off inactivity timer, i.e. inactivate on timeout if self.inactivity_timer.is_enabled(): self.inactivity_timer.begin_transition(False) # Be sure to initially show/hide window and icon palette win.set_visible(visible) def pre_render_keys(self, window, w, h): if self.is_new_layout_size(w, h): self.update_layout(Rect(0, 0, w, h)) self.invalidate_for_resize() win = window.get_window() if win: context = win.cairo_create() self.render(context) def is_new_layout_size(self, w, h): return self.canvas_rect.w != w or \ self.canvas_rect.h != h def get_canvas_content_rect(self): """ Canvas rect excluding resize frame """ return self.canvas_rect.deflate(self.get_frame_width()) def get_base_aspect_rect(self): """ Rect with aspect ratio of the layout as defined in the SVG file """ layout = self.get_layout() if not layout: return Rect(0, 0, 1.0, 1.0) return layout.context.log_rect def update_layout(self, canvas_rect=None): layout = self.get_layout() if not layout: return # recalculate item rectangles if canvas_rect is None: self.canvas_rect = Rect(0, 0, self.get_allocated_width(), self.get_allocated_height()) else: self.canvas_rect = canvas_rect rect = self.get_canvas_content_rect() layout.update_log_rect() # update logical tree to base aspect ratio rect = self._get_aspect_corrected_layout_rect( rect, self.get_base_aspect_rect()) layout.do_fit_inside_canvas(rect) # update contexts to final aspect # update the aspect ratio of the main window self.on_layout_updated() def _get_aspect_corrected_layout_rect(self, rect, base_aspect_rect): """ Aspect correction specifically targets xembedding in unity-greeter and gnome-screen-saver. Else we would potentially disrupt embedding in existing kiosk applications. """ orientation_co = self.get_kbd_window().get_orientation_config_object() keep_aspect = config.is_keep_frame_aspect_ratio_enabled(orientation_co) xembedding = config.xid_mode unity_greeter = config.launched_by == config.LAUNCHER_UNITY_GREETER x_align = 0.5 aspect_change_range = (0, 100) if keep_aspect: if xembedding: aspect_change_range = config.get_xembed_aspect_change_range() elif (config.is_docking_enabled() and config.is_dock_expanded(orientation_co)): aspect_change_range = self.get_docking_aspect_change_range() ra = rect.resize_to_aspect_range(base_aspect_rect, aspect_change_range) if xembedding and \ unity_greeter: padding = rect.w - ra.w offset = config.get_xembed_unity_greeter_offset_x() # Attempt to left align to unity-greeters password box, # but use the whole width on small screens. if offset is not None \ and padding > 2 * offset: rect.x += offset rect.w -= offset x_align = 0.0 rect = rect.align_rect(ra, x_align) return rect def update_window_handles(self): """ Tell WindowManipulator about the active resize handles """ docking = config.is_docking_enabled() # frame handles WindowManipulator.set_drag_handles(self, self._get_active_drag_handles()) WindowManipulator.lock_x_axis(self, docking) # touch handles self.touch_handles.set_active_handles(self._get_active_drag_handles(True)) self.touch_handles.lock_x_axis(docking) def update_transparency(self): """ Updates transparencies in response to user action. Temporarily presents the window with active transparency when inactive transparency is enabled. """ self.transition_active_to(True) self.commit_transition() if self.inactivity_timer.is_enabled(): self.inactivity_timer.begin_transition(False) else: self.inactivity_timer.stop() self.redraw() # for background transparency def touch_inactivity_timer(self): """ extend active transparency, kick of inactivity_timer """ if self.inactivity_timer.is_enabled(): self.inactivity_timer.begin_transition(True) self.inactivity_timer.begin_transition(False) def update_inactive_transparency(self): if self.inactivity_timer.is_enabled(): self.transition_active_to(False) self.commit_transition() def _update_double_click_time(self): """ Scraping the bottom of the barrel to speed up key presses """ self._double_click_time = Gtk.Settings.get_default() \ .get_property("gtk-double-click-time") def transition_visible_to(self, visible, opacity_duration = None, slide_duration = None): result = False state = self._transition_state win = self.get_kbd_window() # hide popup if not visible: self.close_key_popup() # bail in xembed mode if config.xid_mode: return False # stop reposition updates when we're hiding anyway if win and not visible: win.stop_auto_positioning() if config.is_docking_enabled(): if slide_duration is None: slide_duration = self.TRANSITION_DURATION_SLIDE opacity_duration = 0.0 opacity_visible = True if win: visible_before = win.is_visible() visible_later = visible hideout_old_mon = win.get_docking_hideout_rect() mon_changed = win.update_docking_monitor() hideout_new_mon = win.get_docking_hideout_rect() \ if mon_changed else hideout_old_mon # Only position here if visibility or the active monitor # changed. Leave it to auto_position to move the keyboard # while it is visible, i.e. not being hidden or shown. if visible_before != visible_later or \ mon_changed: if visible: begin_rect = hideout_new_mon end_rect = win.get_visible_rect() else: begin_rect = win.get_rect() end_rect = hideout_old_mon state.y.value = begin_rect.y y = end_rect.y state.x.value = begin_rect.x x = end_rect.x result |= self._init_transition(state.x, x, slide_duration) result |= self._init_transition(state.y, y, slide_duration) else: opacity_visible = visible if opacity_duration is None: if opacity_visible: # No duration when showing. Don't fight with compiz in unity. opacity_duration = 0.0 else: opacity_duration = self.TRANSITION_DURATION_OPACITY_HIDE result |= self._init_opacity_transition(state.visible, opacity_visible, opacity_duration) state.target_visibility = visible return result def transition_active_to(self, active, duration = None): """ Transition active state for inactivity timer. This ramps up/down the window opacity. """ # not in xembed mode if config.xid_mode: return False if duration is None: if active: duration = 0.15 else: duration = 0.3 return self._init_opacity_transition(self._transition_state.active, active, duration) def transition_position_to(self, x, y): result = False state = self._transition_state duration = self.TRANSITION_DURATION_MOVE # not in xembed mode if config.xid_mode: return False win = self.get_kbd_window() if win: begin_rect = win.get_rect() state.y.value = begin_rect.y state.x.value = begin_rect.x result |= self._init_transition(state.x, x, duration) result |= self._init_transition(state.y, y, duration) return result def sync_transition_position(self, rect): """ Update transition variables with the actual window position. Necessary on user positioning. """ state = self._transition_state state.y.value = rect.y state.x.value = rect.x state.y.target_value = rect.y state.x.target_value = rect.x def _init_opacity_transition(self, var, target_value, duration): # No fade delay for screens that can't fade (unity-2d) screen = self.get_screen() if screen and not screen.is_composited(): duration = 0.0 target_value = 1.0 if target_value else 0.0 return self._init_transition(var, target_value, duration) def _init_transition(self, var, target_value, duration): # Transition not yet in progress? if var.target_value != target_value: var.start_transition(target_value, duration) return True return False def commit_transition(self): # not in xembed mode if config.xid_mode: return duration = self._transition_state.get_max_duration() if duration == 0.0: self._on_transition_step() else: self._transition_timer.start(0.02, self._on_transition_step) def _on_transition_step(self): state = self._transition_state state.update() done = state.is_done() active_opacity = config.window.get_active_opacity() inactive_opacity = config.window.get_inactive_opacity() invisible_opacity = 0.0 opacity = inactive_opacity + state.active.value * \ (active_opacity - inactive_opacity) opacity *= state.visible.value window = self.get_kbd_window() if window: self.set_opacity(opacity) visible_before = window.is_visible() visible_later = state.target_visibility # move x = int(state.x.value) y = int(state.y.value) wx, wy = window.get_position() if x != wx or y != wy: window.reposition(x, y) # show/hide visible = (visible_before or visible_later) and not done or \ visible_later and done if window.is_visible() != visible: window.set_visible(visible) # on_leave_notify does not start the inactivity timer # while the pointer remains inside of the window. Do it # here when hiding the window. if not visible and \ self.inactivity_timer.is_enabled(): self.inactivity_timer.begin_transition(False) # start/stop on-hide-release timer self._auto_release_timer.start(visible) if done: window.on_transition_done(visible_before, visible_later) return not done def is_visible(self): """ is the keyboard window currently visible? """ window = self.get_kbd_window() return window.is_visible() if window else False def set_visible(self, visible): """ main method to show/hide onboard manually """ self.transition_visible_to(visible, 0.0) # briefly present the window if visible and self.inactivity_timer.is_enabled(): self.transition_active_to(True, 0.0) self.inactivity_timer.begin_transition(False) self.commit_transition() def raise_to_top(self): """ Raise the toplevel parent to top of the z-order. """ window = self.get_kbd_window() if window: window.raise_to_top() def auto_position(self): """ auto-show, start repositioning """ window = self.get_kbd_window() if window: window.auto_position() def stop_auto_positioning(self): """ auto-show, stop all further repositioning attempts """ window = self.get_kbd_window() if window: window.stop_auto_positioning() def start_click_polling(self): if self.keyboard.has_latched_sticky_keys() or \ self._key_popup or \ config.are_word_suggestions_enabled(): self._outside_click_timer.start(0.01, self._on_click_timer) self._outside_click_detected = False self._outside_click_start_time = time.time() self._outside_click_num = 0 def stop_click_polling(self): self._outside_click_timer.stop() def _on_click_timer(self): """ poll for mouse click outside of onboards window """ rootwin = Gdk.get_default_root_window() dunno, x, y, mask = rootwin.get_pointer() if mask & BUTTON123_MASK: self._outside_click_detected = True self._outside_click_button_mask = mask elif self._outside_click_detected: self._outside_click_detected = False # A button was released anywhere outside of Onboard's control. _logger.debug("click polling: outside click") self.close_key_popup() button = \ self._get_button_from_mask(self._outside_click_button_mask) # When clicking left, don't stop polling right away. This allows # the user to select some text and paste it with middle click, # while the pending separator is still inserted. self._outside_click_num += 1 max_clicks = 4 if button != 1: # middle and right click stop polling immediately self.stop_click_polling() self.keyboard.on_outside_click(button) elif button == 1 and self._outside_click_num == 1: if not config.wp.delayed_word_separators_enabled: self.stop_click_polling() self.keyboard.on_outside_click(button) # allow a couple of left clicks with delayed separators elif self._outside_click_num >= max_clicks: self.stop_click_polling() self.keyboard.on_cancel_outside_click() return True # stop polling after 30 seconds if time.time() - self._outside_click_start_time > 30.0: self.stop_click_polling() self.keyboard.on_cancel_outside_click() return False return True @staticmethod def _get_button_from_mask(mask): for i, bit in enumerate((Gdk.ModifierType.BUTTON1_MASK, Gdk.ModifierType.BUTTON2_MASK, Gdk.ModifierType.BUTTON3_MASK,)): if mask & bit: return i + 1 return 0 def get_drag_window(self): """ Overload for WindowManipulator """ return self.get_kbd_window() def get_drag_threshold(self): """ Overload for WindowManipulator """ return config.get_drag_threshold() def on_drag_initiated(self): """ Overload for WindowManipulator """ window = self.get_drag_window() if window: window.on_user_positioning_begin() self.grab_xi_pointer(True) def on_drag_activated(self): if self.is_resizing(): self._lod = LOD.MINIMAL self.keyboard.hide_touch_feedback() def on_drag_done(self): """ Overload for WindowManipulator """ self.grab_xi_pointer(False) WindowManipulatorAspectRatio.on_drag_done(self) window = self.get_drag_window() if window: window.on_user_positioning_done() self.reset_lod() def get_always_visible_rect(self): """ Returns the bounding rectangle of all move buttons in canvas coordinates. Overload for WindowManipulator """ bounds = None if config.is_docking_enabled(): pass else: keys = self.keyboard.find_items_from_ids(["move"]) keys = [k for k in keys if k.is_path_visible()] if not keys: # no visible move key (Small, Phone layout)? keys = self.keyboard.find_items_from_ids(["RTRN"]) keys = [k for k in keys if k.is_path_visible()] for key in keys: r = key.get_canvas_border_rect() if not bounds: bounds = r else: bounds = bounds.union(r) if bounds is None: bounds = self.canvas_rect return bounds def hit_test_move_resize(self, point): """ Overload for WindowManipulator """ hit = self.touch_handles.hit_test(point) if hit is None: hit = WindowManipulator.hit_test_move_resize(self, point) return hit def _on_configure_event(self, widget, user_data): if self.is_new_layout_size(self.get_allocated_width(), self.get_allocated_height()): self.update_layout() self.update_touch_handles_positions() self.invalidate_for_resize(self._lod) def on_enter_notify(self, widget, event): self.keyboard.on_activity_detected() self._update_double_click_time() # ignore event if a mouse button is held down # we get the event once the button is released if event.state & BUTTON123_MASK: return # ignore unreliable touch enter event for inactivity timer # -> smooths startup, only one transition in set_startup_visibility() source_device = event.get_source_device() source = source_device.get_source() if source != Gdk.InputSource.TOUCHSCREEN: # stop inactivity timer if self.inactivity_timer.is_enabled(): self.inactivity_timer.begin_transition(True) # stop click polling self.stop_click_polling() # Force into view for WindowManipulator's system drag mode. #if not config.xid_mode and \ # not config.window.window_decoration and \ # not config.is_force_to_top(): # GLib.idle_add(self.force_into_view) def on_leave_notify(self, widget, event): # ignore event if a mouse button is held down # we get the event once the button is released if event.state & BUTTON123_MASK: return # Ignore leave events when the cursor hasn't acually left # our window. Fixes window becoming idle-transparent while # typing into firefox awesomebar. # Can't use event.mode as that appears to be broken and # never seems to become GDK_CROSSING_GRAB (Precise). if self.canvas_rect.is_point_within((event.x, event.y)): return self.stop_dwelling() self.reset_touch_handles() # start a timer to detect clicks outside of onboard self.start_click_polling() # Start inactivity timer, but ignore the unreliable # leave event for touch input. source_device = event.get_source_device() source = source_device.get_source() if source != Gdk.InputSource.TOUCHSCREEN: if self.inactivity_timer.is_enabled(): self.inactivity_timer.begin_transition(False) # Reset the cursor, so enabling the scanner doesn't get the last # selected one stuck forever. self.reset_drag_cursor() def do_set_cursor_at(self, point, hit_key = None): """ Set/reset the cursor for frame resize handles """ if not config.xid_mode: allow_drag_cursors = not hit_key and \ not config.has_window_decoration() self.set_drag_cursor_at(point, allow_drag_cursors) def on_input_sequence_begin(self, sequence): """ Button press/touch begin """ self.keyboard.on_activity_detected() self.stop_click_polling() self.stop_dwelling() self.close_key_popup() # There's no reliable enter/leave for touch input # -> turn up inactive transparency on touch begin if sequence.is_touch() and \ self.inactivity_timer.is_enabled(): self.inactivity_timer.begin_transition(True) point = sequence.point key = None # hit-test touch handles first hit_handle = None if self.touch_handles.active: hit_handle = self.touch_handles.hit_test(point) self.touch_handles.set_pressed(hit_handle) if not hit_handle is None: # handle clicked -> stop auto-hide until button release self.stop_touch_handles_auto_hide() else: # no handle clicked -> hide them now self.show_touch_handles(False) # hit-test keys if hit_handle is None: key = self.get_key_at_location(point) # enable/disable the drag threshold if not hit_handle is None: self.enable_drag_protection(False) elif key and key.id == "move": # Move key needs to support long press; # always use the drag threshold. self.enable_drag_protection(True) self.reset_drag_protection() else: self.enable_drag_protection(config.drag_protection) # handle resizing if key is None and \ not config.has_window_decoration() and \ not config.xid_mode: if WindowManipulator.handle_press(self, sequence): return True # bail if we are in scanning mode if config.scanner.enabled: return True # press the key sequence.active_key = key sequence.initial_active_key = key if key: # single click? if self._last_click_key != key or \ sequence.time - self._last_click_time > self._double_click_time: # handle key press sequence.event_type = EventType.CLICK self.key_down(sequence) # start long press detection delay = config.keyboard.long_press_delay if key.id == "move": # don't show touch handles too easily delay += 0.3 self._long_press_timer.start(delay, self._on_long_press, sequence) # double click else: sequence.event_type = EventType.DOUBLE_CLICK self.key_down(sequence) self._last_click_key = key self._last_click_time = sequence.time return True def on_input_sequence_update(self, sequence): """ Pointer motion/touch update """ if not sequence.primary: # only drag with the very first sequence return # Redirect to long press popup for drag selection. popup = self._key_popup if popup: popup.redirect_sequence_update(sequence, popup.on_input_sequence_update) return point = sequence.point hit_key = None # hit-test touch handles first hit_handle = None if self.touch_handles.active: hit_handle = self.touch_handles.hit_test(point) self.touch_handles.set_prelight(hit_handle) # hit-test keys if hit_handle is None: hit_key = self.get_key_at_location(point) if sequence.state & BUTTON123_MASK: # move/resize # fallback=False for faster system resizing (LP: #959035) fallback = True #self.is_moving() or config.is_force_to_top() # move/resize WindowManipulator.handle_motion(self, sequence, fallback = fallback) # stop long press when drag threshold has been overcome if self.is_drag_active(): self.stop_long_press() # drag-select new active key active_key = sequence.active_key if not self.is_drag_initiated() and \ active_key != hit_key: self.stop_long_press() if self._overcome_initial_key_resistance(sequence) and \ (not active_key or not active_key.activated) and \ not self._key_popup: sequence.active_key = hit_key self.key_down_update(sequence, active_key) else: if not hit_handle is None: # handle hovered over: extend the time touch handles are visible self.start_touch_handles_auto_hide() # Show/hide the input line self._hide_input_line_timer.handle_motion(sequence) # start dwelling if we have entered a dwell-enabled key if hit_key and \ hit_key.sensitive: controller = self.keyboard.button_controllers.get(hit_key) if controller and controller.can_dwell() and \ not self.is_dwelling() and \ not self.already_dwelled(hit_key) and \ not config.scanner.enabled and \ not config.lockdown.disable_dwell_activation: self.start_dwelling(hit_key) self.do_set_cursor_at(point, hit_key) # cancel dwelling when the hit key changes if self.dwell_key and self.dwell_key != hit_key or \ self.last_dwelled_key and self.last_dwelled_key != hit_key: self.cancel_dwelling() def on_input_sequence_end(self, sequence): """ Button release/touch end """ # Redirect to long press popup for end of drag-selection. popup = self._key_popup if popup and \ popup.got_motion(): # keep popup open if it wasn't entered popup.redirect_sequence_end(sequence, popup.on_input_sequence_end) # key up active_key = sequence.active_key if active_key and \ not config.scanner.enabled: self.key_up(sequence) self.stop_drag() self.stop_long_press() # reset cursor when there was no cursor motion point = sequence.point hit_key = self.get_key_at_location(point) self.do_set_cursor_at(point, hit_key) # reset touch handles self.reset_touch_handles() self.start_touch_handles_auto_hide() # There's no reliable enter/leave for touch input # -> start inactivity timer on touch end if sequence.is_touch() and \ self.inactivity_timer.is_enabled(): self.inactivity_timer.begin_transition(False) def on_drag_gesture_begin(self, num_touches): self.stop_long_press() if Handle.MOVE in self.get_drag_handles() and \ num_touches and \ not self.is_drag_initiated(): self.show_touch_handles() self.start_move_window() return True def on_drag_gesture_end(self, num_touches): self.stop_move_window() return True def on_tap_gesture(self, num_touches): if num_touches == 3: self.show_touch_handles() return True return False def _on_long_press(self, sequence): long_pressed = self.keyboard.key_long_press(sequence.active_key, self, sequence.button) sequence.cancel_key_action = long_pressed # cancel generating key-stroke def stop_long_press(self): self._long_press_timer.stop() def key_down(self, sequence): self.keyboard.key_down(sequence.active_key, self, sequence) self._auto_release_timer.start() def key_down_update(self, sequence, old_key): assert(not old_key or not old_key.activated) # old_key must be undoable self.keyboard.key_up(old_key, self, sequence, False) self.keyboard.key_down(sequence.active_key, self, sequence, False) def key_up(self, sequence): self.keyboard.key_up(sequence.active_key, self, sequence, not sequence.cancel_key_action) def is_dwelling(self): return not self.dwell_key is None def already_dwelled(self, key): return self.last_dwelled_key is key def start_dwelling(self, key): self.cancel_dwelling() self.dwell_key = key self.last_dwelled_key = key key.start_dwelling() self.dwell_timer = GLib.timeout_add(50, self._on_dwell_timer) def cancel_dwelling(self): self.stop_dwelling() self.last_dwelled_key = None def stop_dwelling(self): if self.dwell_timer: GLib.source_remove(self.dwell_timer) self.dwell_timer = None self.redraw([self.dwell_key]) self.dwell_key.stop_dwelling() self.dwell_key = None def _on_dwell_timer(self): if self.dwell_key: self.redraw([self.dwell_key]) if self.dwell_key.is_done(): key = self.dwell_key self.stop_dwelling() sequence = InputSequence() sequence.button = 0 sequence.event_type = EventType.DWELL sequence.active_key = key sequence.point = key.get_canvas_rect().get_center() sequence.root_point = \ canvas_to_root_window_point(self, sequence.point) self.key_down(sequence) self.key_up(sequence) return False return True def _on_query_tooltip(self, widget, x, y, keyboard_mode, tooltip): if config.show_tooltips and \ not self.is_drag_initiated() and \ not self.last_event_was_touch(): key = self.get_key_at_location((x, y)) if key and key.tooltip: r = Gdk.Rectangle() r.x, r.y, r.width, r.height = key.get_canvas_rect() tooltip.set_tip_area(r) # no effect on Oneiric? tooltip.set_text(_(key.tooltip)) return True return False def show_touch_handles(self, show = True, auto_hide = True): """ Show/hide the enlarged resize/move handels. Initiates an opacity fade. """ if show and config.lockdown.disable_touch_handles: return if show: self.touch_handles.set_prelight(None) self.touch_handles.set_pressed(None) self.touch_handles.active = True self.touch_handles_auto_hide = auto_hide size, size_mm = get_monitor_dimensions(self) self.touch_handles.set_monitor_dimensions(size, size_mm) self.update_touch_handles_positions() if auto_hide: self.start_touch_handles_auto_hide() start, end = 0.0, 1.0 else: self.stop_touch_handles_auto_hide() start, end = 1.0, 0.0 if self.touch_handles_fade.target_value != end: self.touch_handles_fade.time_step = 0.025 self.touch_handles_fade.fade_to(start, end, 0.2, self._on_touch_handles_opacity) def reset_touch_handles(self): if self.touch_handles.active: self.touch_handles.set_prelight(None) self.touch_handles.set_pressed(None) def start_touch_handles_auto_hide(self): """ (re-) starts the timer to hide touch handles """ if self.touch_handles.active and self.touch_handles_auto_hide: self.touch_handles_hide_timer.start(4, self.show_touch_handles, False) def stop_touch_handles_auto_hide(self): """ stops the timer to hide touch handles """ self.touch_handles_hide_timer.stop() def _on_touch_handles_opacity(self, opacity, done): if done and opacity < 0.1: self.touch_handles.active = False self.touch_handles.opacity = opacity # Convoluted workaround for a weird cairo glitch (Precise). # When queuing all handles for drawing, the background under # the move handle is clipped erroneously and remains transparent. # -> Divide handles up into two groups, draw only one # group at a time and fade with twice the frequency. if 0: self.touch_handles.redraw() else: for handle in self.touch_handles.handles: if bool(self.touch_handles_fade.iteration & 1) != \ (handle.id in [Handle.MOVE, Handle.NORTH, Handle.SOUTH]): handle.redraw() if done: # draw the missing final step GLib.idle_add(self._on_touch_handles_opacity, 1.0, False) def update_touch_handles_positions(self): self.touch_handles.update_positions(self.get_keyboard_frame_rect()) def _on_draw(self, widget, context): context.push_group() decorated = LayoutView.draw(self, widget, context) # draw touch handles (enlarged move and resize handles) if self.touch_handles.active: corner_radius = config.CORNER_RADIUS if decorated else 0 self.touch_handles.set_corner_radius(corner_radius) self.touch_handles.draw(context) context.pop_group_to_source() context.paint_with_alpha(self._opacity) def _overcome_initial_key_resistance(self, sequence): """ Drag-select: Increase the hit area of the initial key to make it harder to leave the the key the button was pressed down on. """ DRAG_SELECT_INITIAL_KEY_ENLARGEMENT = 0.4 active_key = sequence.active_key if active_key and active_key is sequence.initial_active_key: rect = active_key.get_canvas_border_rect() k = min(rect.w, rect.h) * DRAG_SELECT_INITIAL_KEY_ENLARGEMENT rect = rect.inflate(k) if rect.is_point_within(sequence.point): return False return True def get_kbd_window(self): return self.get_parent() def can_draw_frame(self): """ Overload for LayoutView """ co = self.get_kbd_window().get_orientation_config_object() return not config.is_dock_expanded(co) def can_draw_sidebars(self): """ Overload for LayoutView """ co = self.get_kbd_window().get_orientation_config_object() return config.is_keep_docking_frame_aspect_ratio_enabled(co) def get_frame_width(self): """ Width of the frame around the keyboard; canvas coordinates. """ if config.xid_mode: return config.UNDECORATED_FRAME_WIDTH if config.has_window_decoration(): return 0.0 co = self.get_kbd_window().get_orientation_config_object() if config.is_dock_expanded(co): return 2.0 if config.window.transparent_background: return 3.0 return config.UNDECORATED_FRAME_WIDTH def get_hit_frame_width(self): return 10 def _get_active_drag_handles(self, all_handles = False): if config.xid_mode: # none when xembedding handles = () else: if config.is_docking_enabled(): expand = self.get_kbd_window().get_dock_expand() if expand: handles = (Handle.NORTH, Handle.SOUTH, Handle.WEST, Handle.EAST, Handle.MOVE) else: handles = Handle.RESIZE_MOVE else: handles = Handle.RESIZE_MOVE if not all_handles: # filter through handles enabled in config config_handles = config.window.window_handles handles = tuple(set(handles).intersection(set(config_handles))) return handles def get_handle_function(self, handle): if handle in (Handle.WEST, Handle.EAST) and \ config.is_docking_enabled() and \ self.get_kbd_window().get_dock_expand(): return HandleFunction.ASPECT_RATIO return HandleFunction.NORMAL def get_click_type_button_rects(self): """ Returns bounding rectangles of all click type buttons in root window coordinates. """ keys = self.keyboard.find_items_from_ids(["singleclick", "secondaryclick", "middleclick", "doubleclick", "dragclick"]) rects = [] for key in keys: r = key.get_canvas_border_rect() r = canvas_to_root_window_rect(self, r) # scale coordinates in response to changes to # org.gnome.desktop.interface scaling-factor scale = config.window_scaling_factor if scale and scale != 1.0: r = r.scale(scale) rects.append(r) return rects def get_key_screen_rect(self, key): """ Returns bounding rectangles of key in in root window coordinates. """ r = key.get_canvas_border_rect() x0, y0 = self.get_window().get_root_coords(r.x, r.y) x1, y1 = self.get_window().get_root_coords(r.x + r.w, r.y + r.h) return Rect(x0, y0, x1 - x0, y1 -y0) def on_layout_updated(self): # experimental support for keeping window aspect ratio # Currently, in Oneiric, neither lightdm, nor gnome-screen-saver # appear to honor these hints. layout = self.get_layout() aspect_ratio = None co = self.get_kbd_window().get_orientation_config_object() if config.is_keep_window_aspect_ratio_enabled(co): log_rect = layout.get_border_rect() aspect_ratio = log_rect.w / float(log_rect.h) aspect_ratio = layout.get_log_aspect_ratio() if self._window_aspect_ratio != aspect_ratio: window = self.get_kbd_window() if window: geom = Gdk.Geometry() if aspect_ratio is None: window.set_geometry_hints(self, geom, 0) else: geom.min_aspect = geom.max_aspect = aspect_ratio window.set_geometry_hints(self, geom, Gdk.WindowHints.ASPECT) self._window_aspect_ratio = aspect_ratio def refresh_pango_layouts(self): """ When the systems font dpi setting changes, our pango layout object still caches the old setting, leading to wrong font scaling. Refresh the pango layout object. """ _logger.info("Refreshing pango layout, new font dpi setting is '{}'" \ .format(Gtk.Settings.get_default().get_property("gtk-xft-dpi"))) Key.reset_pango_layout() self.invalidate_label_extents() self.keyboard.invalidate_ui() self.keyboard.commit_ui_updates() def show_popup_alternative_chars(self, key, alternatives): """ Popup with alternative chars. """ popup = self._create_key_popup(self.get_kbd_window()) result = LayoutBuilderAlternatives \ .build(key, self.get_color_scheme(), alternatives) popup.set_layout(*result) self._show_key_popup(popup, key) self._key_popup = popup self.keyboard.hide_touch_feedback() def show_popup_layout(self, key, layout): """ Popup with predefined layout items. """ popup = self._create_key_popup(self.get_kbd_window()) result = LayoutBuilder \ .build(key, self.get_color_scheme(), layout) popup.set_layout(*result) self._show_key_popup(popup, key) self._key_popup = popup self.keyboard.hide_touch_feedback() def close_key_popup(self): if self._key_popup: self._key_popup.destroy() self._key_popup = None def _create_key_popup(self, parent): popup = LayoutPopup(self.keyboard, self.close_key_popup) popup.supports_alpha = self.supports_alpha popup.set_transient_for(parent) popup.set_opacity(self.get_opacity()) return popup def _show_key_popup(self, popup, key): r = key.get_canvas_border_rect() root_rect = canvas_to_root_window_rect(self, r) popup.position_at(root_rect.x + root_rect.w * 0.5, root_rect.y, 0.5, 1.0) popup.show_all() return popup def show_snippets_dialog(self, snippet_id): """ Show dialog for creating a new snippet """ label, text = config.snippets.get(snippet_id, (None, None)) if snippet_id in config.snippets: # Title of the snippets dialog for existing snippets title = _format("Edit snippet #{}", snippet_id) message = "" else: # Title of the snippets dialog for new snippets title = _("New snippet") # Message in the snippets dialog for new snippets message = _format("Enter a new snippet for button #{}:", snippet_id) # turn off AT-SPI listeners to prevent D-BUS deadlocks (Quantal). self.keyboard.on_focusable_gui_opening() dialog = Gtk.Dialog(title=title, transient_for=self.get_toplevel(), flags=0) # Translators: cancel button of the snippets dialog. It used to # be stock item STOCK_CANCEL until Gtk 3.10 deprecated those. dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL) dialog.add_button(_("_Save snippet"), Gtk.ResponseType.OK) # Don't hide dialog behind the keyboard in force-to-top mode. if config.is_force_to_top(): dialog.set_position(Gtk.WindowPosition.CENTER) dialog.set_default_response(Gtk.ResponseType.OK) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12, border_width=5) dialog.get_content_area().add(box) if message: msg_label = Gtk.Label(label=message, xalign=0.0) box.add(msg_label) label_entry = Gtk.Entry(hexpand=True) text_entry = Gtk.Entry(hexpand=True, activates_default = True, width_chars=35) label_label = Gtk.Label(label=_("_Button label:"), xalign=0.0, use_underline=True, mnemonic_widget=label_entry) text_label = Gtk.Label(label=_("S_nippet:"), xalign=0.0, use_underline=True, mnemonic_widget=text_entry) grid = Gtk.Grid(row_spacing=6, column_spacing=3) grid.attach(label_label, 0, 0, 1, 1) grid.attach(text_label, 0, 1, 1, 1) grid.attach(label_entry, 1, 0, 1, 1) grid.attach(text_entry, 1, 1, 1, 1) box.add(grid) # Init entries, mainly the label for the case when text is empty. label, text = config.snippets.get(snippet_id, (None, None)) if label: label_entry.set_text(label) if text: text_entry.set_text(text) if label and not text: text_entry.grab_focus() else: label_entry.grab_focus() dialog.connect("response", self._on_snippet_dialog_response, \ snippet_id, label_entry, text_entry) dialog.show_all() def _on_snippet_dialog_response(self, dialog, response, snippet_id, \ label_entry, text_entry): if response == Gtk.ResponseType.OK: label = label_entry.get_text() text = text_entry.get_text() if sys.version_info.major == 2: label = label.decode("utf-8") text = text.decode("utf-8") config.set_snippet(snippet_id, (label, text)) dialog.destroy() self.keyboard.on_snippets_dialog_closed() # Reenable AT-SPI keystroke listeners. # Delay this until the dialog is really gone. GLib.idle_add(self.keyboard.on_focusable_gui_closed) def show_language_menu(self, key, button, closure = None): self._language_menu.popup(key, button, closure) def is_language_menu_showing(self): return self._language_menu.is_showing() def show_prediction_menu(self, key, button, closure = None): self._suggestion_menu.popup(key, button, closure) class KeyMenu: """ Popup menu for keys """ def __init__(self, keyboard_widget): self._keyboard_widget = keyboard_widget self._keyboard = self._keyboard_widget.keyboard self._menu = None self._closure = None self._x_align = 0.0 # horizontal alignment of the menu position def is_showing(self): return not self._menu is None def popup(self, key, button, closure = None): self._closure = closure self._keyboard.on_focusable_gui_opening() menu = self.create_menu(key, button) self._menu = menu menu.connect("unmap", self._on_menu_unmap) menu.show_all() menu.popup(None, None, self._menu_positioning_func, key, button, Gtk.get_current_event_time()) def create_menu(self, key, button): """ Overload this in derived class """ raise NotImplementedError() def _on_menu_unmap(self, menu): Timer(0.5, self._keyboard.on_focusable_gui_closed) self._menu = None if self._closure: self._closure() def _menu_positioning_func(self, *params): # work around change in number of paramters in Wily with Gtk 3.16 if len(params) == 4: menu, x, y, key = params # new in Wily else: menu, key = params r = self._keyboard_widget.get_key_screen_rect(key) menu_size = (menu.get_allocated_width(), menu.get_allocated_width()) x, y = self.get_menu_position(r, menu_size) return x, y, False def get_menu_position(self, rkey, menu_size): return rkey.left() + (rkey.w - menu_size[0]) * self._x_align, \ rkey.bottom() class LanguageMenu(KeyMenu): """ Popup menu for the language button """ def create_menu(self, key, button): keyboard = self._keyboard languagedb = keyboard._languagedb active_lang_id = keyboard.get_active_lang_id() system_lang_id = config.get_system_default_lang_id() lang_ids = set(languagedb.get_language_ids()) if system_lang_id in lang_ids: lang_ids.remove(system_lang_id) max_mru_languages = config.typing_assistance.max_recent_languages all_mru_lang_ids = config.typing_assistance.recent_languages mru_lang_ids = [id for id in all_mru_lang_ids if id in lang_ids] \ [:max_mru_languages] other_lang_ids = set(lang_ids).difference(mru_lang_ids) other_langs = [] for lang_id in other_lang_ids: name = languagedb.get_language_full_name(lang_id) if name: other_langs.append((name, lang_id)) # language sub menu lang_menu = Gtk.Menu() for name, lang_id in sorted(other_langs): item = Gtk.MenuItem.new_with_label(name) item.connect("activate", self._on_other_language_activated, lang_id) lang_menu.append(item) # popup menu menu = Gtk.Menu() active_lang_id = keyboard.get_active_lang_id() name = languagedb.get_language_full_name(system_lang_id) item = Gtk.CheckMenuItem.new_with_mnemonic(name) item.set_draw_as_radio(True) item.set_active(not active_lang_id) item.connect("activate", self._on_language_activated, "") menu.append(item) item = Gtk.SeparatorMenuItem.new() menu.append(item) for lang_id in mru_lang_ids: name = languagedb.get_language_full_name(lang_id) if name: item = Gtk.CheckMenuItem.new_with_label(name) item.set_draw_as_radio(True) item.set_active(lang_id == active_lang_id) item.connect("activate", self._on_language_activated, lang_id) menu.append(item) if mru_lang_ids: item = Gtk.SeparatorMenuItem.new() menu.append(item) if other_langs: item = Gtk.MenuItem.new_with_mnemonic(_("Other _Languages")) item.set_submenu(lang_menu) menu.append(item) return menu def _on_language_activated(self, menu, lang_id): system_lang_id = config.get_system_default_lang_id() if lang_id == system_lang_id: lang_id = "" self._set_active_lang_id(lang_id) def _on_other_language_activated(self, menu, lang_id): if lang_id: # empty string = system default self._set_mru_lang_id(lang_id) self._set_active_lang_id(lang_id) def _set_active_lang_id(self, lang_id): self._keyboard.set_active_lang_id(lang_id) def _set_mru_lang_id(self, lang_id): max_recent_languages = config.typing_assistance.max_recent_languages recent_languages = config.typing_assistance.recent_languages[:] if lang_id in recent_languages: recent_languages.remove(lang_id) recent_languages.insert(0, lang_id) recent_languages = recent_languages[:max_recent_languages] config.typing_assistance.recent_languages = recent_languages class SuggestionMenu(KeyMenu): """ Popup menu for word suggestion buttons """ def __init__(self, keyboard_widget): KeyMenu.__init__(self, keyboard_widget) self._x_align = 0.5 def create_menu(self, key, button): self._choice_index = key.code # popup menu menu = Gtk.Menu() item = Gtk.MenuItem.new_with_mnemonic(_("_Remove suggestion…")) item.connect("activate", self._on_remove_suggestion, key) menu.append(item) return menu def _on_remove_suggestion(self, menu_item, key): keyboard = self._keyboard wordlist = key.get_parent() suggestion, history = \ keyboard.get_prediction_choice_and_history(wordlist, self._choice_index) history = history[-1:] # only single word history supported dialog = RemoveSuggestionConfirmationDialog( self._keyboard_widget.get_kbd_window(), keyboard, suggestion, history) context_length = dialog.run() if context_length: context = [suggestion] if context_length == 2: context = history[-1:] + context keyboard.remove_prediction_context(context) # Refresh word suggestions explicitely, the dialog # disabled AT-SPI events. keyboard.invalidate_context_ui() keyboard.commit_ui_updates() def get_menu_position(self, rkey, menu_size): if menu_size[0] > rkey.w: return rkey.left(), rkey.bottom() else: return super(SuggestionMenu, self) \ .get_menu_position(rkey, menu_size) class RemoveSuggestionConfirmationDialog(Gtk.MessageDialog): """ Confirm removal of a word suggestion """ def __init__(self, parent, keyboard, suggestion, history): self._keyboard = keyboard self._radio1 = None self._radio2 = None Gtk.MessageDialog.__init__(self, message_type=Gtk.MessageType.QUESTION, buttons=Gtk.ButtonsType.OK_CANCEL, title=_("Onboard")) if parent: self.set_transient_for(parent) # Don't hide dialog behind the keyboard in force-to-top mode. if config.is_force_to_top(): self.set_position(Gtk.WindowPosition.CENTER) markup = "" + _("Remove word suggestion") + "" markup = escape_markup(markup, preserve_tags=True) self.set_markup(markup) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) label = self._get_remove_context_length_2_label(suggestion, history) self._radio2 = Gtk.RadioButton.new_with_label(None, label) box.add(self._radio2) self._radio1 = Gtk.RadioButton.new_with_label_from_widget(self._radio2, _format("Remove '{}' everywhere.", suggestion)) box.add(self._radio1) if not history: # This should rarly happen, if ever. Edited text usually # start with the begin of text marker, so there exists a history # even if the text appears to be empty. self._radio2.set_sensitive(False) self._radio1.set_active(True) label = Gtk.Label(label=_("This will only affect learned suggestions."), xalign=0.0) box.add(label) self.get_message_area().add(box) self.show_all() @staticmethod def _get_remove_context_length_2_label(suggestion, history): """ Label of radio button for remove context length 2. Doctests: >>> from Onboard.KeyboardWidget import RemoveSuggestionConfirmationDialog >>> test = RemoveSuggestionConfirmationDialog._get_remove_context_length_2_label >>> def _format(msgstr, *args, **kwargs): ... return msgstr.format(*args, **kwargs) >>> import builtins >>> builtins.__dict__['_format'] = _format >>> test("word", []) "Remove 'word' only where it occures at text begin." >>> test("word", ["word2"]) "Remove 'word' only where it occures after 'word2'." >>> test("word", ["word2", "word3"]) "Remove 'word' only where it occures after 'word2 word3'." >>> test("word", [""]) "Remove 'word' only where it occures after ''." >>> test("word", [""]) "Remove 'word' only where it occures at sentence begin." >>> test("word", [""]) "Remove 'word' only where it occures after numbers." """ hist0 = history[-1] if history else None if not hist0 or hist0.startswith("": label = _format("Remove '{}' only where it occures at sentence begin.", suggestion) elif hist0 == "": label = _format("Remove '{}' only where it occures after numbers.", suggestion) else: label = _format("Remove '{}' only where it occures after '{}'.", suggestion, " ".join(history)) return label def run(self): keyboard = self._keyboard if keyboard: keyboard.on_focusable_gui_opening() response = Gtk.Dialog.run(self) self.destroy() if keyboard: keyboard.on_focusable_gui_closed() # return length of the remove context if response == Gtk.ResponseType.OK: if self._radio2.get_active(): return 2 if self._radio1.get_active(): return 1 return 0