1996 lines
70 KiB
Python
1996 lines
70 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright © 2009 Chris Jones <tortoise@tortuga>
|
|
# Copyright © 2012 Gerd Kohlberger <lowfi@chello.at>
|
|
# Copyright © 2009, 2011-2017 marmuta <marmvta@gmail.com>
|
|
#
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
""" 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 = "<big>" + _("Remove word suggestion") + "</big>"
|
|
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", ["<unk>"])
|
|
"Remove 'word' only where it occures after '<unk>'."
|
|
|
|
>>> test("word", ["<s>"])
|
|
"Remove 'word' only where it occures at sentence begin."
|
|
|
|
>>> test("word", ["<num>"])
|
|
"Remove 'word' only where it occures after numbers."
|
|
"""
|
|
hist0 = history[-1] if history else None
|
|
if not hist0 or hist0.startswith("<bot:"):
|
|
label = _format("Remove '{}' only where it occures at text begin.",
|
|
suggestion)
|
|
elif hist0 == "<s>":
|
|
label = _format("Remove '{}' only where it occures at sentence begin.",
|
|
suggestion)
|
|
elif hist0 == "<num>":
|
|
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
|
|
|
|
|