onboard-1.4.1/ 0000755 0001750 0001750 00000000000 13051420243 013363 5 ustar frafu frafu 0000000 0000000 onboard-1.4.1/layouts/ 0000755 0001750 0001750 00000000000 13051420243 015063 5 ustar frafu frafu 0000000 0000000 onboard-1.4.1/layouts/Whiteboard_wide.onboard 0000644 0001750 0001750 00000037137 13051012134 021540 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Compact.onboard 0000644 0001750 0001750 00000027254 13051012134 020025 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Whiteboard-Alpha.svg 0000644 0001750 0001750 00000022572 13051012134 020723 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Small-Fn.svg 0000644 0001750 0001750 00000022267 13051012134 017222 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Whiteboard-Emoji.svg 0000644 0001750 0001750 00000022714 13051012134 020737 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Phone-Syms.svg 0000644 0001750 0001750 00000020532 13051012134 017604 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Small.onboard 0000644 0001750 0001750 00000054061 13051012134 017503 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Full Keyboard-Numpad.svg 0000644 0001750 0001750 00000016711 13051012134 021453 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Phone-Numbers.svg 0000644 0001750 0001750 00000020457 13051012134 020272 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Small-Syms.svg 0000644 0001750 0001750 00000025431 13051012134 017606 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Compact-Numbers.svg 0000644 0001750 0001750 00000017250 13051012134 020604 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/word_suggestions.xml 0000644 0001750 0001750 00000004560 13051012134 021213 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Whiteboard_wide-Alpha.svg 0000644 0001750 0001750 00000056064 13051012134 021736 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Whiteboard-Numbers.svg 0000644 0001750 0001750 00000022662 13051012134 021311 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Compact-Utils.svg 0000644 0001750 0001750 00000012055 13051012134 020267 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Whiteboard.onboard 0000644 0001750 0001750 00000045137 13051012134 020527 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Small-Snippets.svg 0000644 0001750 0001750 00000016226 13051012134 020462 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Phone-Alpha.svg 0000644 0001750 0001750 00000055632 13051012134 017707 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Full Keyboard.onboard 0000644 0001750 0001750 00000027435 13051012134 021063 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Grid.onboard 0000644 0001750 0001750 00000012412 13051012134 017312 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Phone.onboard 0000644 0001750 0001750 00000036606 13051012134 017511 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Compact-Alpha.svg 0000644 0001750 0001750 00000041674 13051012134 020225 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Whiteboard-Arrows.svg 0000644 0001750 0001750 00000022657 13051012134 021157 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/ 0000755 0001750 0001750 00000000000 13051420243 016330 5 ustar frafu frafu 0000000 0000000 onboard-1.4.1/layouts/images/show-click.svg 0000644 0001750 0001750 00000003131 13051012134 021106 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/hover-click.svg 0000644 0001750 0001750 00000003620 13051012134 021254 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/right-click.svg 0000644 0001750 0001750 00000003055 13051012134 021250 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/middle-click.svg 0000644 0001750 0001750 00000003140 13051012134 021364 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/double-click.svg 0000644 0001750 0001750 00000003543 13051012134 021407 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/arrow-right.svg 0000644 0001750 0001750 00000003001 13051012134 021304 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/close.svg 0000644 0001750 0001750 00000003755 13051012134 020164 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/drag-click.svg 0000644 0001750 0001750 00000004701 13051012134 021047 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/snippets.svg 0000644 0001750 0001750 00000003211 13051012134 020707 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/erase.svg 0000644 0001750 0001750 00000003364 13051012134 020152 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/arrow-left.svg 0000644 0001750 0001750 00000005035 13051012134 021132 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/single-click.svg 0000644 0001750 0001750 00000003027 13051012134 021413 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/erase-left.svg 0000644 0001750 0001750 00000003357 13051012134 021104 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/hide.svg 0000644 0001750 0001750 00000003257 13051012134 017765 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/pause.svg 0000644 0001750 0001750 00000003160 13051012134 020162 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/preferences.svg 0000644 0001750 0001750 00000005007 13051012134 021350 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/move.svg 0000644 0001750 0001750 00000004525 13051012134 020021 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/images/arrow-down.svg 0000644 0001750 0001750 00000003003 13051012134 021140 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Whiteboard-Syms.svg 0000644 0001750 0001750 00000022662 13051012134 020631 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Whiteboard-Greek.svg 0000644 0001750 0001750 00000022660 13051012134 020731 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Phone-Emoji.svg 0000644 0001750 0001750 00000021624 13051012134 017717 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/key_defs.xml 0000644 0001750 0001750 00000026421 13051012134 017377 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Full Keyboard-Alpha.svg 0000644 0001750 0001750 00000043304 13051012134 021252 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Grid-Alpha.svg 0000644 0001750 0001750 00000036773 13051012134 017530 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Small-Emoji.svg 0000644 0001750 0001750 00000026617 13051012134 017725 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Small-Alpha.svg 0000644 0001750 0001750 00000064163 13051012134 017705 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/layouts/Small-Numbers.svg 0000644 0001750 0001750 00000027666 13051012134 020302 0 ustar frafu frafu 0000000 0000000
onboard-1.4.1/COPYING.BSD3 0000644 0001750 0001750 00000002762 13051012134 015113 0 ustar frafu frafu 0000000 0000000 BSD-3-clause license:
Copyright (c) The Regents of the University of California.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the University nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
onboard-1.4.1/Onboard/ 0000755 0001750 0001750 00000000000 13051420243 014747 5 ustar frafu frafu 0000000 0000000 onboard-1.4.1/Onboard/KeyCommon.py 0000644 0001750 0001750 00000113332 13051012134 017221 0 ustar frafu frafu 0000000 0000000 # -*- coding: UTF-8 -*-
# Copyright © 2007 Martin Böhme
# Copyright © 2008-2009 Chris Jones
# Copyright © 2010 Francesco Fumanti
# 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 .
"""
KeyCommon hosts the abstract classes for the various types of Keys.
UI-specific keys should be defined in KeyGtk or KeyKDE files.
"""
from __future__ import division, print_function, unicode_literals
from math import pi
import re
from Onboard.utils import Rect, LABEL_MODIFIERS, Modifiers, \
polygon_to_rounded_path
from Onboard.Layout import LayoutItem
### Logging ###
import logging
_logger = logging.getLogger("KeyCommon")
###############
### Config Singleton ###
from Onboard.Config import Config
config = Config()
########################
(
CHAR_TYPE,
KEYSYM_TYPE,
KEYCODE_TYPE,
MACRO_TYPE,
SCRIPT_TYPE,
KEYPRESS_NAME_TYPE,
BUTTON_TYPE,
LEGACY_MODIFIER_TYPE,
WORD_TYPE,
CORRECTION_TYPE,
) = tuple(range(1, 11))
(
SINGLE_STROKE_ACTION, # press on button down, release on up (default)
DELAYED_STROKE_ACTION, # press+release on button up (MENU)
DOUBLE_STROKE_ACTION, # press+release on button down and up, (CAPS, NMLK)
) = tuple(range(3))
actions = {
"single-stroke" : SINGLE_STROKE_ACTION,
"delayed-stroke" : DELAYED_STROKE_ACTION,
"double-stroke" : DOUBLE_STROKE_ACTION,
}
class StickyBehavior:
""" enum for sticky key behaviors """
(
CYCLE,
DOUBLE_CLICK,
LATCH_ONLY,
LOCK_ONLY,
LATCH_LOCK_NOCYCLE,
DOUBLE_CLICK_NOCYCLE,
LATCH_NOCYCLE,
LOCK_NOCYCLE,
PUSH_BUTTON,
) = tuple(range(9))
values = {"cycle" : CYCLE,
"dblclick" : DOUBLE_CLICK,
"latch" : LATCH_ONLY,
"lock" : LOCK_ONLY,
"latch-lock-nocycle" : LATCH_LOCK_NOCYCLE,
"dblclick-nocycle" : DOUBLE_CLICK_NOCYCLE,
"latch-nocycle" : LATCH_NOCYCLE,
"lock-nocycle" : LOCK_NOCYCLE,
"push" : PUSH_BUTTON,
}
@staticmethod
def from_string(str_value):
""" Raises KeyError """
return StickyBehavior.values[str_value]
@staticmethod
def is_valid(behavior):
return behavior in StickyBehavior.values.values()
@staticmethod
def can_latch(behavior):
"""
Can sticky key enter latched state?
Latched keys are automatically released when a
non-sticky key is pressed.
"""
return behavior in (StickyBehavior.CYCLE,
StickyBehavior.DOUBLE_CLICK,
StickyBehavior.LATCH_ONLY,
StickyBehavior.LATCH_LOCK_NOCYCLE,
StickyBehavior.DOUBLE_CLICK_NOCYCLE,
StickyBehavior.LATCH_NOCYCLE)
@staticmethod
def can_lock(behavior):
return StickyBehavior.can_lock_on_single_click(behavior) or \
StickyBehavior.can_lock_on_double_click(behavior)
@staticmethod
def can_lock_on_single_click(behavior):
"""
Can sticky key enter locked state?
Locked keys stay active until they are pressed again.
"""
return behavior in (StickyBehavior.CYCLE,
StickyBehavior.LOCK_ONLY,
StickyBehavior.LATCH_LOCK_NOCYCLE,
StickyBehavior.LOCK_NOCYCLE)
@staticmethod
def can_lock_on_double_click(behavior):
"""
Can sticky key enter locked state on double click?
Locked keys stay active until they are pressed again.
"""
return behavior == StickyBehavior.DOUBLE_CLICK or \
behavior == StickyBehavior.DOUBLE_CLICK_NOCYCLE
@staticmethod
def can_cycle(behavior):
"""
Can sticky key return to normal state?
Latched keys are still automatically released when a
non-sticky key is pressed.
"""
return behavior in (StickyBehavior.CYCLE,
StickyBehavior.DOUBLE_CLICK,
StickyBehavior.LATCH_ONLY,
StickyBehavior.LOCK_ONLY)
class LOD:
""" enum for level of detail """
(
MINIMAL, # clearly visible reduced detail, fastest
REDUCED, # slightly reduced detail
FULL, # full detail
) = tuple(range(3))
class ImageSlot:
NORMAL = 0
ACTIVE = 1
class KeyCommon(LayoutItem):
"""
library-independent key class. Specific rendering options
are stored elsewhere.
"""
# extended id for key specific theme tweaks
# e.g. theme_id=DELE.numpad (with id=DELE)
theme_id = None
# extended id for layout specific tweaks
# e.g. "hide.wordlist", for hide button in wordlist mode
svg_id = None
# optional id of a sublayout used as long-press popup
popup_id = None
# Type of action to do when key is pressed.
action = None
# Type of key stroke to send
type = None
# Data used in sending key strokes.
code = None
# Keys that stay stuck when pressed like modifiers.
sticky = False
# Behavior if sticky is enabled, see StickyBehavior.
sticky_behavior = None
# modifier bit
modifier = None
# True when key is being hovered over (not implemented yet)
prelight = False
# True when key is being pressed.
pressed = False
# True when key stays 'on'
active = False
# True when key is sticky and pressed twice.
locked = False
# True when Onboard is in scanning mode and key is highlighted
scanned = False
# True when action was triggered e.g. key-strokes were sent on press
activated = False
# Size to draw the label text in Pango units
font_size = 1
# Labels which are displayed by this key
labels = None # {modifier_mask : label, ...}
# label that is currently displayed by this key
label = ""
# mod_mask for the currently configured label
mod_mask = 0
# smaller label of a currently invisible modifier level
secondary_label = ""
# Images displayed by this key (optional)
image_filenames = None
# horizontal label alignment
label_x_align = config.DEFAULT_LABEL_X_ALIGN
# vertical label alignment
label_y_align = config.DEFAULT_LABEL_Y_ALIGN
# label margin (x, y)
label_margin = config.LABEL_MARGIN
# tooltip text
tooltip = None
# can show label popup
label_popup = True
###################
def __init__(self):
LayoutItem.__init__(self)
def configure_label(self, mod_mask):
SHIFT = Modifiers.SHIFT
labels = self.labels
if labels is None:
self.label = self.secondary_label = ""
return
# primary label
label = labels.get(mod_mask)
if label is None:
mask = mod_mask & LABEL_MODIFIERS
label = labels.get(mask)
# secondary label, usually the label of the shift state
secondary_label = None
if not label is None:
if mod_mask & SHIFT:
mask = mod_mask & ~SHIFT
else:
mask = mod_mask | SHIFT
secondary_label = labels.get(mask)
if secondary_label is None:
mask = mask & LABEL_MODIFIERS
secondary_label = labels.get(mask)
# Only keep secondary labels that show different characters
if not secondary_label is None and \
secondary_label.upper() == label.upper():
secondary_label = None
if label is None:
# legacy fallback for 0.98 behavior and virtkey until 0.61.0
if mod_mask & Modifiers.SHIFT:
if mod_mask & Modifiers.ALTGR and 129 in labels:
label = labels[129]
elif 1 in labels:
label = labels[1]
elif 2 in labels:
label = labels[2]
elif mod_mask & Modifiers.ALTGR and 128 in labels:
label = labels[128]
elif mod_mask & Modifiers.CAPS: # CAPS lock
if 2 in labels:
label = labels[2]
elif 1 in labels:
label = labels[1]
if label is None:
label = labels.get(0)
if label is None:
label = ""
self.mod_mask = mod_mask
self.label = label
self.secondary_label = secondary_label
# Don't let erroneous labels shrink their whole size group.
self.ignore_group = label.startswith("0x")
def draw_label(self, context = None):
raise NotImplementedError()
def set_labels(self, labels):
self.labels = labels
self.configure_label(0)
def get_label(self):
return self.label
def get_secondary_label(self):
return self.secondary_label
def is_active(self):
return not self.type is None
def get_id(self):
return ""
def get_svg_id(self):
return ""
def set_id(self, id, theme_id = None, svg_id = None):
self.theme_id, self.id = self.parse_id(id)
if theme_id:
self.theme_id = theme_id
self.svg_id = self.id if not svg_id else svg_id
@staticmethod
def parse_id(value):
"""
The theme id has the form ., where
the identifier should be a description of the location of
the key relative to its surroundings, e.g. 'DELE.next-to-backspace'.
Don't use layout names or layer ids for the theme id, they lose
their meaning when layouts are copied or renamed by users.
"""
theme_id = value
id = value.split(".")[0]
return theme_id, id
@staticmethod
def split_theme_id(theme_id):
"""
Simple split in prefix (id) before the dot and suffix after the dot.
"""
components = theme_id.split(".")
if len(components) == 1:
return components[0], ""
return components[0], components[1]
@staticmethod
def build_theme_id(prefix, postfix):
if postfix:
return prefix + "." + postfix
return prefix
def get_similar_theme_id(self, prefix = None):
if prefix is None:
prefix = self.id
theme_id = prefix
comps = self.theme_id.split(".")[1:]
if comps:
theme_id += "." + comps[0]
return theme_id
def is_layer_button(self):
return self.id.startswith("layer")
def is_prediction_key(self):
return self.id.startswith("prediction")
def is_correction_key(self):
return self.id.startswith("correction") or \
self.id in ["expand-corrections"]
def is_word_suggestion(self):
return self.is_prediction_key() or self.is_correction_key()
def is_modifier(self):
"""
Modifiers are all latchable/lockable non-button keys:
"LWIN", "RTSH", "LFSH", "RALT", "LALT",
"RCTL", "LCTL", "CAPS", "NMLK"
"""
return bool(self.modifier)
def is_click_type_key(self):
return self.id in ["singleclick",
"secondaryclick",
"middleclick",
"doubleclick",
"dragclick"]
def is_button(self):
return self.type == BUTTON_TYPE
def is_pressed_only(self):
return self.pressed and not (self.active or \
self.locked or \
self.scanned)
def is_text_changing(self):
if not self.is_modifier() and \
self.type in [KEYCODE_TYPE,
KEYSYM_TYPE,
CHAR_TYPE,
KEYPRESS_NAME_TYPE,
MACRO_TYPE,
WORD_TYPE,
CORRECTION_TYPE]:
id = self.id
if not (id.startswith("F") and id[1:].isdigit()) and \
not id in set(["LEFT", "RGHT", "UP", "DOWN",
"HOME", "END", "PGUP", "PGDN",
"INS", "ESC", "MENU",
"Prnt", "Pause", "Scroll"]):
return True
return False
def is_return(self):
id = self.id
return (id == "RTRN" or
id == "KPEN")
def is_separator(self):
id = self.id
return (id == "SPCE" or
id == "TAB")
def is_separator_cancelling(self):
""" Should this key cancel pending word separators? """
return (self.is_correction_key() or
self.is_return() or
self.id in set(["SPCE", "TAB",
# Don't cancel for Backspace. We want to have
# it appear to delete the pending separator.
# This way it inserts a space, then immediately
# deletes it.
# "BKSP",
"DELE",
"LEFT", "RGHT", "UP", "DOWN",
"HOME", "END", "PGUP", "PGDN",
"INS", "ESC", "MENU",
"Prnt", "Pause", "Scroll"]))
def get_layer_index(self):
assert(self.is_layer_button())
return int(self.id[5:])
def get_popup_layout(self):
if self.popup_id:
return self.find_sublayout(self.popup_id)
return None
def can_show_label_popup(self):
return not self.is_modifier() and \
not self.is_layer_button() and \
not self.type is None and \
bool(self.label_popup)
class RectKeyCommon(KeyCommon):
""" An abstract class for rectangular keyboard buttons """
# optional path data for keys with arbitrary shapes
geometry = None
# size of rounded corners at 100% round_rect_radius
chamfer_size = None
# Optional key_style to override the default theme's style.
style = None
# Toggles for what gets drawn.
show_face = True
show_border = True
show_label = True
show_image = True
# Allow to display active state, i.e. either latched or locked state.
# Depending on sticky_behavior the button will still become logically
# active, it just isn't shown. Used for layer0 buttons, mainly. They don't
# need to stick out, it's usually obvious when the first layer is active.
show_active = True
def __init__(self, id, border_rect):
KeyCommon.__init__(self)
self.id = id
self.colors = {}
self.context.log_rect = border_rect \
if not border_rect is None else Rect()
def get_id(self):
return self.id
def get_svg_id(self):
return self.svg_id
def get_state(self):
state = {}
state["prelight"] = self.prelight
state["pressed"] = self.pressed
state["active"] = self.active
state["locked"] = self.locked
state["scanned"] = self.scanned
state["sensitive"] = self.sensitive
return state
def draw(self, context = None):
pass
def align_label(self, label_size, key_size, ltr = True):
""" returns x- and yoffset of the aligned label """
label_x_align = self.label_x_align
label_y_align = self.label_y_align
if not ltr: # right to left script?
label_x_align = 1.0 - label_x_align
xoffset = label_x_align * (key_size[0] - label_size[0])
yoffset = label_y_align * (key_size[1] - label_size[1])
return xoffset, yoffset
def align_secondary_label(self, label_size, key_size, ltr = True):
""" returns x- and yoffset of the aligned label """
label_x_align = 0.97
label_y_align = 0.0
if not ltr: # right to left script?
label_x_align = 1.0 - label_x_align
xoffset = label_x_align * (key_size[0] - label_size[0])
yoffset = label_y_align * (key_size[1] - label_size[1])
return xoffset, yoffset
def align_popup_indicator(self, label_size, key_size, ltr = True):
""" returns x- and yoffset of the aligned label """
label_x_align = 1.0
label_y_align = self.label_y_align
if not ltr: # right to left script?
label_x_align = 1.0 - label_x_align
xoffset = label_x_align * (key_size[0] - label_size[0])
yoffset = label_y_align * (key_size[1] - label_size[1])
return xoffset, yoffset
def get_style(self):
if not self.style is None:
return self.style
return config.theme_settings.key_style
def get_stroke_width(self):
return config.theme_settings.key_stroke_width / 100.0
def get_stroke_gradient(self):
return config.theme_settings.key_stroke_gradient / 100.0
def get_light_direction(self):
return config.theme_settings.key_gradient_direction * pi / 180.0
def get_fill_color(self):
return self._get_color("fill")
def get_stroke_color(self):
return self._get_color("stroke")
def get_label_color(self):
return self._get_color("label")
def get_secondary_label_color(self):
return self._get_color("secondary-label")
def get_dwell_progress_color(self):
return self._get_color("dwell-progress")
def get_dwell_progress_canvas_rect(self):
rect = self.get_label_rect().inflate(0.5)
return self.context.log_to_canvas_rect(rect)
def _get_color(self, element):
color_key = (element, self.prelight, self.pressed,
self.active, self.locked,
self.sensitive, self.scanned)
rgba = self.colors.get(color_key)
if not rgba:
if self.color_scheme:
rgba = self.color_scheme.get_key_rgba(self, element)
elif element == "label":
rgba = [0.0, 0.0, 0.0, 1.0]
else:
rgba = [1.0, 1.0, 1.0, 1.0]
self.colors[color_key] = rgba
return rgba
def get_fullsize_rect(self):
""" Get bounding box of the key at 100% size in logical coordinates """
return LayoutItem.get_rect(self)
def get_canvas_fullsize_rect(self):
""" Get bounding box of the key at 100% size in canvas coordinates """
return self.context.log_to_canvas_rect(self.get_fullsize_rect())
def get_unpressed_rect(self):
"""
Get bounding box in logical coordinates.
Just the relatively static unpressed rect withough fake key action.
"""
rect = self.get_fullsize_rect()
return self._apply_key_size(rect)
def get_rect(self):
""" Get bounding box in logical coordinates """
return self.get_sized_rect()
def get_sized_rect(self, horizontal = None):
rect = self.get_fullsize_rect()
# fake physical key action
if self.pressed:
dx, dy, dw, dh = self.get_pressed_deltas()
rect.x += dx
rect.y += dy
rect.w += dw
rect.h += dh
return self._apply_key_size(rect, horizontal)
@staticmethod
def _apply_key_size(rect, horizontal = None):
""" shrink keys to key_size """
scale = (1.0 - config.theme_settings.key_size / 100.0) * 0.5
bx = rect.w * scale
by = rect.h * scale
if horizontal is None:
horizontal = rect.h < rect.w
if horizontal:
# keys with aspect > 1.0, e.g. space, shift
bx = by
else:
# keys with aspect < 1.0, e.g. click, move, number block + and enter
by = bx
return rect.deflate(bx, by)
def get_pressed_deltas(self):
"""
dx, dy, dw, dh for fake physical key action of pressed keys.
Logical coordinate system.
"""
key_style = self.get_style()
if key_style == "gradient":
k = 0.2
elif key_style == "dish":
k = 0.45
else:
k = 0.0
return k, 2*k, 0.0, 0.0
def get_label_rect(self, rect = None):
""" Label area in logical coordinates """
if rect is None:
rect = self.get_rect()
style = self.get_style()
if style == "dish":
stroke_width = self.get_stroke_width()
border_x, border_y = config.DISH_KEY_BORDER
border_x *= stroke_width
border_y *= stroke_width
rect = rect.deflate(border_x, border_y)
rect.y -= config.DISH_KEY_Y_OFFSET * stroke_width
return rect
else:
return rect.deflate(*self.label_margin)
def get_canvas_label_rect(self):
log_rect = self.get_label_rect()
return self.context.log_to_canvas_rect(log_rect)
def get_border_path(self):
""" Original path including border in logical coordinates. """
return self.geometry.get_full_size_path()
def get_path(self):
"""
Path of the key geometry in logical coordinates.
Key size and fake press movement are applied.
"""
offset_x, offset_y, size_x, size_y = self.get_key_offset_size()
return self.geometry.get_transformed_path(offset_x, offset_y,
size_x, size_y)
def get_canvas_border_path(self):
path = self.get_border_path()
return self.context.log_to_canvas_path(path)
def get_canvas_path(self):
path = self.get_path()
return self.context.log_to_canvas_path(path)
def get_hit_path(self):
return self.get_canvas_border_path()
def get_chamfer_size(self, rect = None):
""" Max size of the rounded corner areas in logical coordinates. """
if not self.chamfer_size is None:
return self.chamfer_size
if not rect:
if self.geometry:
rect = self.get_border_path().get_bounds()
else:
rect = self.get_rect()
return min(rect.w, rect.h) * 0.5
def get_key_offset_size(self, geometry = None):
size_x = size_y = config.theme_settings.key_size / 100.0
offset_x = offset_y = 0.0
if self.pressed:
offset_x, offset_y, dw, dh = self.get_pressed_deltas()
if dw != 0.0 or dh != 0.0:
if geometry is None:
geometry = self.geometry
dw, dh = geometry.scale_log_to_size((dw, dh))
size_x += dw * 0.5
size_y += dh * 0.5
return offset_x, offset_y, size_x, size_y
def get_canvas_polygons(self, geometry,
offset_x, offset_y, size_x, size_y,
radius_pct, chamfer_size):
path = geometry.get_transformed_path(offset_x, offset_y, size_x, size_y)
canvas_path = self.context.log_to_canvas_path(path)
polygons = list(canvas_path.iter_polygons())
polygon_paths = \
[polygon_to_rounded_path(p, radius_pct, chamfer_size) \
for p in polygons]
return polygons, polygon_paths
class InputlineKeyCommon(RectKeyCommon):
""" An abstract class for InputLine keyboard buttons """
line = ""
word_infos = None
cursor = 0
def __init__(self, name, border_rect):
RectKeyCommon.__init__(self, name, border_rect)
def get_label(self):
return ""
class KeyGeometry:
"""
Full description of a key's shape.
This class generates path variants for a given key_size by path
interpolation. This allows for key_size dependent shape changes,
controlled solely by a SVG layout file. See 'Return' key in
'Full Keyboard' layout for an example.
"""
path0 = None # KeyPath at 100% size
path1 = None # KepPath at 50% size, optional
@staticmethod
def from_paths(paths):
assert(len(paths) >= 1)
path0 = paths[0]
path1 = None
if len(paths) >= 2:
path1 = paths[1]
# Equal number of path segments?
if len(path0.segments) != len(path1.segments):
raise ValueError(
"paths to interpolate differ in number of segments "
"({} vs. {})" \
.format(len(path0.segments), len(path1.segments)))
# Same operations in all path segments?
for i in range(len(path0.segments)):
op0, coords0 = path0.segments[i]
op1, coords1 = path1.segments[i]
if op0 != op1:
raise ValueError(
"paths to interpolate have different operations "
"at segment {} (op. {} vs. op. {})" \
.format(i, op0, op1))
geometry = KeyGeometry()
geometry.path0 = path0
geometry.path1 = path1
return geometry
@staticmethod
def from_rect(rect):
geometry = KeyGeometry()
geometry.path0 = KeyPath.from_rect(rect)
return geometry
def get_transformed_path(self, offset_x = 0.0, offset_y = 0.0,
size_x = 1.0, size_y = 1.0):
"""
Everything in the logical coordinate system.
size: 1.0 => path0, 0.5 => path1
"""
path0 = self.path0
path1 = self.path1
if path1:
pos_x = (1 - size_x) * 2.0
pos_y = (1 - size_y) * 2.0
return path0.linint(path1, pos_x, pos_y, offset_x, offset_y)
else:
r0 = self.get_full_size_bounds()
r1 = self.get_half_size_bounds()
rect = r1.inflate((size_x - 0.5) * (r0.w - r1.w),
(size_y - 0.5) * (r0.h - r1.h))
rect.x += offset_x
rect.y += offset_y
return path0.fit_in_rect(rect)
def get_full_size_path(self):
return self.path0
def get_full_size_bounds(self):
"""
Bounding box at size 1.0.
"""
return self.path0.get_bounds()
def get_half_size_bounds(self):
"""
Bounding box at size 0.5.
"""
path1 = self.path1
if path1:
rect = path1.get_bounds()
else:
rect = self.path0.get_bounds()
if rect.h < rect.w:
dx = dy = rect.h * 0.25
else:
dy = dx = rect.w * 0.25
rect = rect.deflate(dx, dy)
return rect
def scale_log_to_size(self, v):
""" Scale from logical distances to key size. """
r0 = self.get_full_size_bounds()
r1 = self.get_half_size_bounds()
log_h = (r0.h - r1.h) * 2.0
log_w = (r0.w - r1.w) * 2.0
return (v[0] / log_h,
v[1] / log_w)
def scale_size_to_log(self, v):
""" Scale from logical distances to key size. """
r0 = self.get_full_size_bounds()
r1 = self.get_half_size_bounds()
log_h = (r0.h - r1.h) * 2.0
log_w = (r0.w - r1.w) * 2.0
return (v[0] * log_h,
v[1] * log_w)
class KeyPath:
"""
Cairo-friendly path description for non-rectangular keys.
Can handle straight line-loops/polygons, but not arcs and splines.
"""
(
MOVE_TO,
LINE_TO,
CLOSE_PATH,
) = range(3)
_last_abs_pos = (0.0, 0.0)
_bounds = None # cached bounding box
def __init__(self):
self.segments = [] # normalized list of path segments (all absolute)
@staticmethod
def from_svg_path(path_str):
path = KeyPath()
path.append_svg_path(path_str)
return path
@staticmethod
def from_rect(rect):
x0 = rect.x
y0 = rect.y
x1 = rect.right()
y1 = rect.bottom()
path = KeyPath()
path.segments = [[KeyPath.MOVE_TO, [x0, y0]],
[KeyPath.LINE_TO, [x1, y0, x1, y1, x0, y1]],
[KeyPath.CLOSE_PATH, []]]
path._bounds = rect.copy()
return path
_svg_path_pattern = re.compile("([+-]?[0-9.]+)")
def copy(self):
result = KeyPath()
for op, coords in self.segments:
result.segments.append([op, coords[:]])
return result
def append_svg_path(self, path_str):
"""
Append a SVG path data string to the path.
Doctests:
# absolute move_to command
>>> p = KeyPath.from_svg_path("M 100 200 120 -220")
>>> print(p.segments)
[[0, [100.0, 200.0]], [1, [120.0, -220.0]]]
# relative move_to command
>>> p = KeyPath.from_svg_path("m 100 200 10 -10")
>>> print(p.segments)
[[0, [100.0, 200.0]], [1, [110.0, 190.0]]]
# relative move_to and close_path segments
>>> p = KeyPath.from_svg_path("m 100 200 10 -10 z")
>>> print(p.segments)
[[0, [100.0, 200.0]], [1, [110.0, 190.0]], [2, []]]
# spaces and commas and are optional where possible
>>> p = KeyPath.from_svg_path("m100,200 10-10z")
>>> print(p.segments)
[[0, [100.0, 200.0]], [1, [110.0, 190.0]], [2, []]]
"""
cmd_str = ""
coords = []
tokens = self._tokenize_svg_path(path_str)
for token in tokens:
try:
val = float(token) # raises value error
coords.append(val)
except ValueError:
if token.isalpha():
if cmd_str:
self.append_command(cmd_str, coords)
cmd_str = token
coords = []
elif token == ",":
pass
else:
raise ValueError(
"unexpected token '{}' in svg path data" \
.format(token))
if cmd_str:
self.append_command(cmd_str, coords)
def append_command(self, cmd_str, coords):
"""
Append a single command and it's coordinate data to the path.
Doctests:
# first lowercase move_to position is absolute
>>> p = KeyPath()
>>> p.append_command("m", [100, 200])
>>> print(p.segments)
[[0, [100, 200]]]
# move_to segments become line_to segments after the first position
>>> p = KeyPath()
>>> p.append_command("M", [100, 200, 110, 190])
>>> print(p.segments)
[[0, [100, 200]], [1, [110, 190]]]
# further lowercase move_to positions are relative, must become absolute
>>> p = KeyPath()
>>> p.append_command("m", [100, 200, 10, -10, 10, -10])
>>> print(p.segments)
[[0, [100, 200]], [1, [110, 190, 120, 180]]]
# further lowercase segments must still be become absolute
>>> p = KeyPath()
>>> p.append_command("m", [100, 200, 10, -10, 10, -10])
>>> p.append_command("l", [1, -1, 1, -1])
>>> print(p.segments)
[[0, [100, 200]], [1, [110, 190, 120, 180]], [1, [121, 179, 122, 178]]]
"""
# Convert lowercase segments from relative to absolute coordinates.
if cmd_str in ("m", "l"):
# Don't convert the very first coordinate, it is already absolute.
if self.segments:
start = 0
x, y = self._last_abs_pos
else:
start = 2
x, y = coords[0], coords[1]
for i in range(start, len(coords), 2):
x += coords[i]
y += coords[i+1]
coords[i] = x
coords[i+1] = y
cmd = cmd_str.lower()
if cmd == "m":
self.segments.append([self.MOVE_TO, coords[:2]])
if len(coords) > 2:
self.segments.append([self.LINE_TO, coords[2:]])
elif cmd == "l":
self.segments.append([self.LINE_TO, coords])
elif cmd == "z":
self.segments.append([self.CLOSE_PATH, []])
# remember last absolute position
if len(coords) >= 2:
self._last_abs_pos = coords[-2:]
@staticmethod
def _tokenize_svg_path(path_str):
"""
Split SVG path date into command and coordinate tokens.
Doctests:
>>> KeyPath._tokenize_svg_path("m 10,20")
['m', '10', ',', '20']
>>> KeyPath._tokenize_svg_path(" m 10 , \\n 20 ")
['m', '10', ',', '20']
>>> KeyPath._tokenize_svg_path("m 10,20 30,40 z")
['m', '10', ',', '20', '30', ',', '40', 'z']
>>> KeyPath._tokenize_svg_path("m10,20 30,40z")
['m', '10', ',', '20', '30', ',', '40', 'z']
>>> KeyPath._tokenize_svg_path("M100.32 100.09 100. -100.")
['M', '100.32', '100.09', '100.', '-100.']
>>> KeyPath._tokenize_svg_path("m123+23 20,-14L200,200")
['m', '123', '+23', '20', ',', '-14', 'L', '200', ',', '200']
>>> KeyPath._tokenize_svg_path("m123+23 20,-14L200,200")
['m', '123', '+23', '20', ',', '-14', 'L', '200', ',', '200']
"""
tokens = [token.strip() \
for token in KeyPath._svg_path_pattern.split(path_str)]
return [token for token in tokens if token]
def get_bounds(self):
bounds = self._bounds
if bounds is None:
bounds = self._calc_bounds()
self._bounds = bounds
return bounds
def _calc_bounds(self):
"""
Compute the bounding box of the path.
Doctests:
# Simple move_to path, something inkscape would create.
>>> p = KeyPath.from_svg_path("m 100,200 10,-10 z")
>>> print(p.get_bounds())
Rect(x=100.0 y=190.0 w=10.0 h=10.0)
"""
try:
xmin = xmax = self.segments[0][1][0]
ymin = ymax = self.segments[0][1][1]
except IndexError:
return Rect()
for command in self.segments:
coords = command[1]
for i in range(0, len(coords), 2):
x = coords[i]
y = coords[i+1]
if xmin > x:
xmin = x
if xmax < x:
xmax = x
if ymin > y:
ymin = y
if ymax < y:
ymax = y
return Rect(xmin, ymin, xmax - xmin, ymax - ymin)
def inflate(self, dx, dy = None):
"""
Returns a new path which is larger by dx and dy on all sides.
"""
rect = self.get_bounds().inflate(dx, dy)
return self.fit_in_rect(rect)
def fit_in_rect(self, rect):
"""
Scales and translates the path so that rect
becomes its new bounding box.
"""
result = self.copy()
bounds = self.get_bounds()
scalex = rect.w / bounds.w
scaley = rect.h / bounds.h
dorgx, dorgy = bounds.get_center()
dx = rect.x - (dorgx + (bounds.x - dorgx) * scalex)
dy = rect.y - (dorgy + (bounds.y - dorgy) * scaley)
for op, coords in result.segments:
for i in range(0, len(coords), 2):
coords[i] = dx + dorgx + (coords[i] - dorgx) * scalex
coords[i+1] = dy + dorgy + (coords[i+1] - dorgy) * scaley
return result
def linint(self, path1, pos_x = 1.0, pos_y = 1.0,
offset_x = 0.0, offset_y = 0.0):
"""
Interpolate between self and path1.
Paths must have the same structure (length and operations).
pos: 0.0 = self, 1.0 = path1.
"""
result = self.copy()
segments = result.segments
segments1 = path1.segments
for i in range(len(segments)):
op, coords = segments[i]
op1, coords1 = segments1[i]
for j in range(0, len(coords), 2):
x = coords[j]
y = coords[j+1]
x1 = coords1[j]
y1 = coords1[j+1]
dx = x1 - x
dy = y1 - y
coords[j] = x + pos_x * dx + offset_x
coords[j+1] = y + pos_y * dy + offset_y
return result
def iter_polygons(self):
"""
Loop through all independent polygons in the path.
Can't handle splines and arcs, everything has to
be polygons from here.
"""
polygon = []
for op, coords in self.segments:
if op == self.LINE_TO:
polygon.extend(coords)
elif op == self.MOVE_TO:
polygon = []
polygon.extend(coords)
elif op == self.CLOSE_PATH:
yield polygon
def is_point_within(self, point):
for polygon in self.iter_polygons():
if self.is_point_in_polygon(polygon, point[0], point[1]):
return True
@staticmethod
def is_point_in_polygon(vertices, x, y):
c = False
n = len(vertices)
try:
x0 = vertices[n - 2]
y0 = vertices[n - 1]
except IndexError:
return False
for i in range(0, n, 2):
x1 = vertices[i]
y1 = vertices[i+1]
if (y1 <= y and y < y0 or y0 <= y and y < y1) and \
(x < (x0 - x1) * (y - y1) / (y0 - y1) + x1):
c = not c
x0 = x1
y0 = y1
return c
onboard-1.4.1/Onboard/KeyboardWidget.py 0000644 0001750 0001750 00000214052 13051012134 020225 0 ustar frafu frafu 0000000 0000000 # -*- 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
onboard-1.4.1/Onboard/utils.py 0000644 0001750 0001750 00000136734 13051012134 016473 0 ustar frafu frafu 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright © 2007-2009 Chris Jones
# Copyright © 2008, 2010 Francesco Fumanti
# Copyright © 2012 Gerd Kohlberger
# Copyright © 2011-2016 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 .
from __future__ import division, print_function, unicode_literals
import sys
import os
import time
import re
import colorsys
import gettext
import subprocess
from math import pi, sin, cos, sqrt, log
from contextlib import contextmanager
import logging
from functools import reduce
from gi.repository import GLib
_logger = logging.getLogger("utils")
# keycodes
class KeyCode:
Return = 36
KP_Enter = 104
C = 54
class Modifiers:
# 1 2 4 8 16 32 64 128
SHIFT, CAPS, CTRL, ALT, NUMLK, MOD3, SUPER, ALTGR = \
(1<>> parse_key_combination(["TAB"], ["TAB"])
[('TAB', 0)]
>>> parse_key_combination(["LALT", "TAB"], ["TAB"])
[('TAB', 8)]
>>> parse_key_combination(["LALT", "LFSH", "TAB"], ["TAB"])
[('TAB', 9)]
>>> parse_key_combination(["LWIN", "RTSH", "LFSH", "RALT", "LALT", "RCTL", "LCTL", "CAPS", "NMLK", "TAB"], ["TAB"])
[('TAB', 223)]
# modifier groups
>>> parse_key_combination(["CTRL", "SHIFT", "TAB"], ["TAB"])
[('TAB', 5)]
# regex
>>> parse_key_combination(["F\d+"], ["TAB", "F1", "F2", "F3", "F9"])
[('F1', 0), ('F2', 0), ('F3', 0), ('F9', 0)]
"""
modifiers = combo[:-1]
key_pattern = combo[-1]
# find modifier mask
mod_mask = parse_modifier_strings(modifiers)
if mod_mask is None:
return None
# match regex key id with all available ids
results = []
pattern = re.compile(key_pattern)
for key_id in avaliable_key_ids:
match = pattern.match(key_id)
if match and match.group() == key_id:
results.append((key_id, mod_mask))
return results
def parse_modifier_strings(modifiers):
""" Build modifier mask from modifier strings. """
mod_mask = 0
for modifier in modifiers:
m = modDic.get(modifier)
if not m is None:
mod_mask |= m[1]
else:
group = modGroups.get(modifier)
if not group is None:
for mod in group:
mod_mask |= modDic[mod][1]
else:
_logger.warning("unrecognized modifier '{}'; try one of {}" \
.format(modifier, ",".join(m[0] for m in modList)))
mod_mask = None
break
return mod_mask
def run_script(script):
a =__import__(script)
a.run()
def toprettyxml(domdoc):
ugly_xml = domdoc.toprettyxml(indent=' ')
# Join lines with text elements with their tag lines
pattern = re.compile('>\n\s+([^<>\s].*?)\n\s+', re.DOTALL)
pretty_xml = pattern.sub('>\g<1>', ugly_xml)
# Work around http://bugs.python.org/issue5752
pretty_xml = re.sub(
'"[^"]*"',
lambda m: m.group(0).replace("\n", "
"),
pretty_xml)
# remove empty lines
pretty_xml = os.linesep.join( \
[s for s in pretty_xml.splitlines() if s.strip()])
return pretty_xml
def dec_to_hex_colour(dec):
hexString = hex(int(255*dec))[2:]
if len(hexString) == 1:
hexString = "0" + hexString
return hexString
def xml_get_text(dom_node, tag_name):
""" extract text from a dom node """
nodelist = dom_node.getElementsByTagName(tag_name)
if not nodelist:
return None
rc = []
for node in nodelist[0].childNodes:
if node.nodeType == node.TEXT_NODE:
rc.append(node.data)
return ''.join(rc).strip()
def matmult(m, v):
""" Matrix-vector multiplication """
nrows = len(m)
w = [None] * nrows
for row in range(nrows):
w[row] = reduce(lambda x,y: x+y, list(map(lambda x,y: x*y, m[row], v)))
return w
def hexstring_to_float(hexString):
return float(int(hexString, 16))
def hexcolor_to_rgba(color):
"""
convert '#rrggbb' or '#rrggbbaa' to (r, g, b, a)
Doctests:
>>> def test(color):
... rgba = hexcolor_to_rgba(color)
... if rgba is None:
... print(repr(rgba))
... else:
... print(repr([round(c, 2) for c in rgba]))
>>> test("#1a2b3c")
[0.1, 0.17, 0.24, 1.0]
>>> test("#1a2b3c4d")
[0.1, 0.17, 0.24, 0.3]
>>> test("")
None
>>> test("1a2b3c")
None
>>> test("1a2b3c4d")
None
>>> test("#1a2b3c4dx")
None
>>> test("#1a2b3cx")
None
>>> test("#1a2bx")
None
>>> test("#1aXb3c4d")
None
"""
rgba = None
n = len(color)
if n == 7 or n == 9:
try:
rgba = [hexstring_to_float(color[1:3])/255,
hexstring_to_float(color[3:5])/255,
hexstring_to_float(color[5:7])/255]
if n == 9:
rgba.append(hexstring_to_float(color[7:9])/255)
else:
rgba.append(1.0)
except ValueError:
rgba = None
return rgba
class dictproperty(object):
""" Property implementation for dictionaries """
class _proxy(object):
def __init__(self, obj, fget, fset, fdel):
self._obj = obj
self._fget = fget
self._fset = fset
self._fdel = fdel
def __getitem__(self, key):
if self._fget is None:
raise TypeError("can't read item")
return self._fget(self._obj, key)
def __setitem__(self, key, value):
if self._fset is None:
raise TypeError("can't set item")
self._fset(self._obj, key, value)
def __delitem__(self, key):
if self._fdel is None:
raise TypeError("can't delete item")
self._fdel(self._obj, key)
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self._fget = fget
self._fset = fset
self._fdel = fdel
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
return self._proxy(obj, self._fget, self._fset, self._fdel)
def unpack_name_value_list(_list, num_values=2, key_type = str):
"""
Converts a list of strings into a dict of tuples.
Sample list: ['LWIN:label:super', ...]
":" in a value must be escaped as "\:"
"\" in a value must be escaped as "\\"
"""
result = {}
# Awkward fixed regexes; todo: Allow arbirary number of values
if num_values == 1:
pattern = re.compile(r"""([^\s:]+) # name
: ((?:\\.|[^\\:])*) # first value
""", re.VERBOSE)
elif num_values == 2:
pattern = re.compile(r"""([^\s:]+) # name
: ((?:\\.|[^\\:])*) # first value
: ((?:\\.|[^\\:])*) # second value
""", re.VERBOSE)
else:
assert(False) # unsupported number of values
for text in _list:
tuples = pattern.findall(text)
if tuples:
a = []
for t in tuples[0]:
t = t.replace("\\\\", "\\") # unescape backslash
t = t.replace("\\:", ":") # unescape separator
a.append(t)
if key_type == str:
item = {a[0] : (a[1:])}
elif key_type == int:
item = {int(a[0]) : (a[1:])}
else:
assert(False)
result.update(item)
return result
def pack_name_value_list(tuples, field_sep=":", name_sep=":"):
"""
Converts a dict of tuples to a string array. It creates one string
per dict key, with the key-string separated by and
individual tuple elements separated by .
"""
result = []
for t in list(tuples.items()):
text = str(t[0])
sep = name_sep
for value in t[1]:
value = value.replace("\\", "\\\\") # escape backslash
value = value.replace(sep, "\\"+sep) # escape separator
text += sep + '%s' % value
sep = field_sep
result.append(text)
return result
def merge_tuple_strings(text1, text2):
"""
Existing entries in text1 will be kept or overwritten by text2.
"""
tuples1 = unpack_name_value_tuples(text1)
tuples2 = unpack_name_value_tuples(text2)
for key,values in list(tuples2.items()):
tuples1[key] = values
return pack_name_value_tuples(tuples1)
class Rect:
"""
Simple rectangle class.
Left and top are included, right and bottom excluded.
Attributes can be accessed by name or by index, e.g. rect.x or rect[0].
"""
attributes = ("x", "y", "w", "h")
def __init__(self, x = 0, y = 0, w = 0, h = 0):
self.x = x
self.y = y
self.w = w
self.h = h
def __len__(self):
return 4
def __getitem__(self, index):
""" Collection interface for rvalues, unpacking with '*' operator """
return getattr(self, self.attributes[index])
def __setitem__(self, index, value):
""" Collection interface for lvalues """
return setattr(self, self.attributes[index], value)
def __str__(self):
return "Rect(" + \
" ".join("{}={:.1f}".format(a, getattr(self, a)) \
for a in self.attributes) + \
")"
def __repr__(self):
return self.__str__()
def __eq__(self, other):
return self.x == other.x and \
self.y == other.y and \
self.w == other.w and \
self.h == other.h
def __ne__(self, other):
return self.x != other.x or \
self.y != other.y or \
self.w != other.w or \
self.h != other.h
@staticmethod
def from_extents(x0, y0, x1, y1):
"""
New Rect from two points.
x0 and y0 are considered inside, x1 and y1 are just outside the Rect.
"""
return Rect(x0, y0, x1 - x0, y1 - y0)
@staticmethod
def from_position_size(position, size):
"""
New Rect from two tuples.
"""
return Rect(position[0], position[1], size[0], size[1])
@staticmethod
def from_points(p0, p1):
"""
New Rect from two points, left-top and right-botton.
The former lies inside, while the latter is considered to be
just outside the rect.
"""
return Rect(p0[0], p0[1], p1[0] - p0[0], p1[1] - p0[1])
def to_extents(self):
return self.x, self.y , self.x + self.w, self.y + self.h
def to_position_size(self):
return (self.x, self.y), (self.w, self.h)
def copy(self):
return Rect(self.x, self.y, self.w, self.h)
def is_empty(self):
return self.w <= 0 or self.h <= 0
def get_position(self):
return (self.x, self.y)
def get_size(self):
return (self.w, self.h)
def get_center(self):
return (self.x + self.w / 2.0, self.y + self.h / 2.0)
def top(self):
return self.y
def left(self):
return self.x
def right(self):
return self.x + self.w
def bottom(self):
return self.y + self.h
def left_top(self):
return self.x, self.y
def is_point_within(self, point):
""" True, if the given point lies inside the rectangle """
if self.x <= point[0] and \
self.x + self.w > point[0] and \
self.y <= point[1] and \
self.y + self.h > point[1]:
return True
return False
def round(self):
return Rect(round(self.x), round(self.y), round(self.w), round(self.h))
def int(self):
return Rect(int(self.x), int(self.y), int(self.w), int(self.h))
def scale(self, kx, ky = None):
if ky == None:
ky = kx
return Rect(self.x * kx, self.y * ky, self.w * kx, self.h * ky)
def offset(self, dx, dy):
"""
Returns a new Rect displaced by dx and dy.
"""
return Rect(self.x + dx, self.y + dy, self.w, self.h)
def inflate(self, dx, dy = None):
"""
Returns a new Rect which is larger by dx and dy on all sides.
"""
if dy is None:
dy = dx
return Rect(self.x-dx, self.y-dy, self.w+2*dx, self.h+2*dy)
def apply_border(self, left, top, right, bottom):
"""
Returns a new Rect which is larger by l, t, r, b on all sides.
"""
return Rect(self.x-left, self.y-top,
self.w+left+right, self.h+top+bottom)
def deflate(self, dx, dy = None):
"""
Returns a new Rect which is smaller by dx and dy on all sides.
"""
if dy is None:
dy = dx
return Rect(self.x+dx, self.y+dy, self.w-2*dx, self.h-2*dy)
def grow(self, kx, ky = None):
"""
Returns a new Rect with its size multiplied by kx, ky.
"""
if ky is None:
ky = kx
w = self.w * kx
h = self.h * ky
return Rect(self.x + (self.w - w) / 2.0,
self.y + (self.h - h) / 2.0,
w, h)
def intersects(self, rect):
"""
Doctests:
>>> Rect(0, 0, 1, 1).intersects(Rect(0, 0, 1, 1))
True
>>> Rect(0, 0, 1, 1).intersects(Rect(1, 0, 1, 1))
False
>>> Rect(1, 0, 1, 1).intersects(Rect(0, 0, 1, 1))
False
>>> Rect(0, 0, 1, 1).intersects(Rect(0, 1, 1, 1))
False
>>> Rect(0, 1, 1, 1).intersects(Rect(0, 0, 1, 1))
False
"""
#return not self.intersection(rect).is_empty()
return not (self.x >= rect.x + rect.w or \
self.x + self.w <= rect.x or \
self.y >= rect.y + rect.h or \
self.y + self.h <= rect.y)
def intersection(self, rect):
x0 = max(self.x, rect.x)
y0 = max(self.y, rect.y)
x1 = min(self.x + self.w, rect.x + rect.w)
y1 = min(self.y + self.h, rect.y + rect.h)
if x0 > x1 or y0 > y1:
return Rect()
else:
return Rect(x0, y0, x1 - x0, y1 - y0)
def union(self, rect):
x0 = min(self.x, rect.x)
y0 = min(self.y, rect.y)
x1 = max(self.x + self.w, rect.x + rect.w)
y1 = max(self.y + self.h, rect.y + rect.h)
return Rect(x0, y0, x1 - x0, y1 - y0)
def inscribe_with_aspect(self, rect, x_align = 0.5, y_align = 0.5):
""" Returns a new Rect with the aspect ratio of self
that fits inside the given rectangle.
"""
if self.is_empty() or rect.is_empty():
return Rect()
src_aspect = self.w / float(self.h)
dst_aspect = rect.w / float(rect.h)
result = rect.copy()
if dst_aspect > src_aspect:
result.w = rect.h * src_aspect
result.x += x_align * (rect.w - result.w)
else:
result.h = rect.w / src_aspect
result.y += y_align * (rect.h - result.h)
return result
def resize_to_aspect(self, aspect_rect):
"""
Resize self to the aspect ratio of aspect_rect.
"""
if self.is_empty() or aspect_rect.is_empty():
return Rect()
src_aspect = aspect_rect.w / float(aspect_rect.h)
dst_aspect = self.w / float(self.h)
result = self.copy()
if dst_aspect > src_aspect:
result.w = self.h * src_aspect
else:
result.h = self.w / src_aspect
return result
def resize_to_aspect_range(self, aspect_rect, aspect_change_range):
"""
Resize self to get the aspect ratio of aspect_rect, but limited
to the given aspect range.
"""
if self.is_empty() or aspect_rect.is_empty():
return Rect()
r = aspect_rect
if r.h:
a0 = r.w / float(r.h)
a0_max = a0 * aspect_change_range[1]
a1 = self.w / float(self.h)
a = min(a1, a0_max)
r = Rect(0, 0, a, 1.0)
r = Rect(0, 0, a, 1.0)
return self.resize_to_aspect(r)
def align_rect(self, rect, x_align = 0.5, y_align = 0.5):
"""
Aligns the given rect inside of self.
x/y_align = 0.5 centers rect.
"""
x = self.x + (self.w - rect.w) * x_align
y = self.y + (self.h - rect.h) * y_align
return Rect(x, y, rect.w, rect.h)
def align_at_point(self, x, y, x_align = 0.5, y_align = 0.5):
"""
Aligns the given rect to a point.
x/y_align = 0.5 centers rect.
"""
x = x - self.w * x_align
y = y - self.h * y_align
return Rect(x, y, self.w, self.h)
def subdivide(self, columns, rows, x_spacing = None, y_spacing = None):
""" Divide self into columns x rows sub-rectangles """
if y_spacing is None:
y_spacing = x_spacing
if x_spacing is None:
x_spacing = 0
x, y, w, h = self
ws = (self.w - (columns - 1) * x_spacing) / float(columns)
hs = (self.h - (rows - 1) * y_spacing) / float(rows)
rects = []
y = self.y
for row in range(rows):
x = self.x
for column in range(columns):
rects.append(Rect(x, y, ws, hs))
x += ws + x_spacing
y += hs + y_spacing
return rects
def brighten(amount, r, g, b, a=0.0):
""" Make the given color brighter by amount a [-1.0...1.0] """
h, l, s = colorsys.rgb_to_hls(r, g, b)
l += amount
if l > 1.0:
l = 1.0
if l < 0.0:
l = 0.0
return list(colorsys.hls_to_rgb(h, l, s)) + [a]
def linint_rgba(k, rgba1, rgba2):
""" interpolate between two colors """
linint = lambda k, a, b: a + (b - a) * k
return [linint(k, rgba1[0], rgba12[0]),
linint(k, rgba1[1], rgba12[1]),
linint(k, rgba1[2], rgba12[2]),
linint(k, rgba1[3], rgba12[3])]
def roundrect_arc(context, rect, r = 15):
x0,y0 = rect.x, rect.y
x1,y1 = x0 + rect.w, y0 + rect.h
# top left
context.move_to(x0+r, y0)
# top right
context.line_to(x1-r,y0)
context.arc(x1-r, y0+r, r, -pi/2, 0)
# bottom right
context.line_to(x1, y1-r)
context.arc(x1-r, y1-r, r, 0, pi/2)
# bottom left
context.line_to(x0+r, y1)
context.arc(x0+r, y1-r, r, pi/2, pi)
# top left
context.line_to(x0, y0+r)
context.arc(x0+r, y0+r, r, pi, pi*1.5)
context.close_path ()
def roundrect_curve(context, rect, r_pct = 100):
"""
Uses B-splines for less even looks than with arcs, but
still allows for approximate circles at r_pct = 100.
"""
x0 = rect.x
y0 = rect.y
w = rect.w
h = rect.h
x1 = x0 + w
y1 = y0 + h
r = min(w, h) * min(r_pct/100.0, 0.5) # full range at 50%
k = (r-1) * r_pct/200.0 # position of control points for circular curves
line_to = context.line_to
curve_to = context.curve_to
# top left
context.move_to(x0+r, y0)
# top right
line_to(x1-r,y0)
curve_to(x1-k, y0, x1, y0+k, x1, y0+r)
# bottom right
line_to(x1, y1-r)
curve_to(x1, y1-k, x1-k, y1, x1-r, y1)
# bottom left
line_to(x0+r, y1)
curve_to(x0+k, y1, x0, y1-k, x0, y1-r)
# top left
line_to(x0, y0+r)
curve_to(x0, y0+k, x0+k, y0, x0+r, y0)
context.close_path ()
def rounded_polygon(cr, coords, r_pct, chamfer_size):
path = polygon_to_rounded_path(coords, r_pct, chamfer_size)
rounded_polygon_path_to_cairo_path(cr, path)
def polygon_to_rounded_path(coords, r_pct, chamfer_size):
"""
Doctests:
# simple rectangle, chamfer radius 0.
>>> coords = [0, 0, 10, 0, 10, 10, 0, 10]
>>> polygon_to_rounded_path(coords, 0, 0) # doctest: +NORMALIZE_WHITESPACE
[(0.0, 0.0), (10.0, 0.0), (10.0, 0.0, 10.0, 0.0, 10.0, 0.0),
(10.0, 10.0), (10.0, 10.0, 10.0, 10.0, 10.0, 10.0),
(0.0, 10.0), (0.0, 10.0, 0.0, 10.0, 0.0, 10.0),
(0.0, 0.0), (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)]
"""
path = []
r = chamfer_size * 2.0 * min(r_pct/100.0, 0.5) # full range at 50%
n = len(coords)
for i in range(0, n, 2):
i0 = i
i1 = i + 2
if i1 >= n:
i1 -= n
i2 = i + 4
if i2 >= n:
i2 -= n
x0 = coords[i0]
y0 = coords[i0+1]
x1 = coords[i1]
y1 = coords[i1+1]
x2 = coords[i2]
y2 = coords[i2+1]
vax = x1 - x0
vay = y1 - y0
la = sqrt(vax*vax + vay*vay)
uax = vax / la
uay = vay / la
vbx = x2 - x1
vby = y2 - y1
lb = sqrt(vbx*vbx + vby*vby)
ubx = vbx / lb
uby = vby / lb
ra = min(r, la * 0.5) # offset of curve begin and end
rb = min(r, lb * 0.5)
ka = (ra-1) * r_pct/200.0 # offset of control points
kb = (rb-1) * r_pct/200.0
if i == 0:
x = x0 + ra*uax
y = y0 + ra*uay
path.append((x, y))
x = x1 - ra*uax
y = y1 - ra*uay
path.append((x, y))
x = x1 + rb*ubx
y = y1 + rb*uby
c0x = x1 - ka*uax
c0y = y1 - ka*uay
c1x = x1 + kb*ubx
c1y = y1 + kb*uby
path.append((x, y, c0x, c0y, c1x, c1y))
return path
def rounded_polygon_path_to_cairo_path(cr, path):
if path:
cr.move_to(*path[0])
for i in range(1, len(path), 2):
p = path[i]
cr.line_to(p[0], p[1])
p = path[i+1]
cr.curve_to(p[2], p[3], p[4], p[5], p[0], p[1])
cr.close_path()
def rounded_path(cr, path, r_pct, chamfer_size):
for polygon in path.iter_polygons():
rounded_polygon(cr, polygon, r_pct, chamfer_size)
def round_corners(cr, r, x, y, w, h):
"""
Paint 4 round corners.
Currently x, y are ignored and assumed to be 0.
"""
# top-left
cr.curve_to (0, r, 0, 0, r, 0)
cr.line_to (0, 0)
cr.close_path()
cr.fill()
# top-right
cr.curve_to (w, r, w, 0, w - r, 0)
cr.line_to (w, 0)
cr.close_path()
cr.fill()
# bottom-left
cr.curve_to (r, h, 0, h, 0, h - r)
cr.line_to (0, h)
cr.close_path()
cr.fill()
# bottom-right
cr.curve_to (w, h - r, w, h, w - r, h)
cr.line_to (w, h)
cr.close_path()
cr.fill()
def gradient_line(rect, alpha):
# Find rotated gradient start and end points.
# Line end points follow the largest extent of the rotated rectangle.
# The gradient reaches across the entire rectangle.
x0, y0, w, h = rect.x, rect.y, rect.w, rect.h
a = w / 2.0
b = h / 2.0
coords = [(-a, -b), (a, -b), (a, b), (-a, b)]
vx = [c[0]*cos(alpha)-c[1]*sin(alpha) for c in coords]
dx = max(vx) - min(vx)
r = dx / 2.0
return (r * cos(alpha) + x0 + a,
r * sin(alpha) + y0 + b,
-r * cos(alpha) + x0 + a,
-r * sin(alpha) + y0 + b)
import cairo
def drop_shadow(cr, pattern, bounds, blur_radius = 4.0, offset = (0, 0),
alpha=0.06, steps=4):
"""
Mostly works, but has issues with clipping artefacts for
damage rects smaller than the full window rect.
"""
origin = bounds.get_center()
cr.set_source_rgba(0.0, 0.0, 0.0, alpha)
for i in range(steps):
x = (i if i else 0.5) / float(steps)
k = sqrt(abs(log(1-x))) * 0.7 * blur_radius # gaussian
#k = i / float(steps) * blur_radius # linear
x_scale = (bounds.w + k) / bounds.w
y_scale = (bounds.h + k) / bounds.h
cr.save()
cr.translate(*origin)
cr.scale(x_scale, y_scale)
cr.translate(-origin[0] + offset[0], -origin[1] + offset[1])
cr.mask(pattern)
cr.restore()
@contextmanager
def timeit(s, out=sys.stdout):
import time, gc
if out:
gc.collect()
gc.collect()
gc.collect()
t = time.time()
text = s if s else "timeit"
out.write("%-15s " % text)
out.flush()
yield None
out.write("%10.3fms\n" % ((time.time() - t)*1000))
else:
yield None
class Fade:
""" Helper for opacity fading """
@staticmethod
def sin_fade(start_time, duration, start_value, target_value):
elapsed = time.time() - start_time
if duration:
lin_progress = min(1.0, elapsed / duration)
else:
lin_progress = 1.0
return(Fade.sin_int(lin_progress, start_value, target_value),
lin_progress >= 1.0)
@staticmethod
def sin_int(lin_progress, start_value, target_value):
sin_progress = (sin(lin_progress * pi - pi / 2.0) + 1.0) / 2.0
return sin_progress * (target_value - start_value) + start_value
class TreeItem(object):
"""
Abstract base class of tree nodes.
Base class of nodes in layout- and color scheme tree.
"""
# id string of the item
id = None
# parent item in the tree
parent = None
# child items
items = ()
def set_items(self, items):
self.items = items
for item in items:
item.parent = self
def append_item(self, item):
if self.items:
self.items.append(item)
else:
self.items = [item]
item.parent = self
def append_items(self, items):
if self.items:
self.items += items
else:
self.items = items
for item in items:
item.parent = self
def get_parent(self):
return self.parent
def find_ids(self, ids):
""" find all items with matching id """
for item in self.iter_items():
if item.id in ids:
yield item
def find_classes(self, item_classes):
""" find all items with matching id """
for item in self.iter_items():
if isinstance(item, item_classes):
yield item
def iter_items(self):
"""
Iterates through all items of the tree.
"""
yield self
for item in self.items:
for child in item.iter_items():
yield child
def iter_depth_first(self):
"""
Iterates depth first through the tree.
"""
for item in self.items:
for child in item.iter_depth_first():
yield child
yield self
def iter_to_root(self):
item = self
while item:
yield item
item = item.parent
class Version(object):
""" Simple class to encapsulate a version number """
major = 0
minor = 0
def __init__(self, major, minor = 0):
self.major = major
self.minor = minor
def __str__(self):
return self.to_string()
@staticmethod
def from_string(version):
components = version.split(".")
major = 0
minor = 0
try:
if len(components) >= 1:
major = int(components[0])
if len(components) >= 2:
minor = int(components[1])
except ValueError:
pass
return Version(major, minor)
def to_string(self):
return "{major}.{minor}".format(major=self.major, minor=self.minor)
def __eq__(self, other): return self._cmp(other) == 0
def __ne__(self, other): return self._cmp(other) != 0
def __lt__(self, other): return self._cmp(other) < 0
def __le__(self, other): return self._cmp(other) <= 0
def __gt__(self, other): return self._cmp(other) > 0
def __ge__(self, other): return self._cmp(other) >= 0
def _cmp(self, other):
if self.major < other.major:
return -1
if self.major > other.major:
return 1
if self.minor < other.minor:
return -1
if self.minor > other.minor:
return 1
return 0
class Process:
""" Process utilities """
@staticmethod
def get_cmdline(pid):
""" Returns the command line for process id pid """
cmdline = ""
with open("/proc/%s/cmdline" % pid) as f:
cmdline = f.read()
return cmdline.split("\0")
@staticmethod
def get_process_name(pid):
cmdline = Process.get_cmdline(pid)
if cmdline:
return os.path.basename(cmdline[0])
return ""
@staticmethod
def get_launch_process_cmdline():
""" Checks if this process was launched by """
ppid = os.getppid()
if ppid:
cmdline = Process.get_cmdline(ppid)
return cmdline
return []
@staticmethod
def was_launched_by(process_name):
""" Checks if this process was launched by """
cmdline = " ".join(Process.get_launch_process_cmdline())
return process_name in cmdline
def exists_in_path(basename):
"""
Does a file with this basename exist anywhere in PATH's directories?
"""
for path in os.environ["PATH"].split(os.pathsep):
filename = os.path.join(path, basename)
if os.path.isfile(filename):
return True
return False
def chmodtree(path, mode = 0o777, only_dirs = False):
"""
Change permissions of all files of the given directory tree.
Raises OSError.
"""
os.chmod(path, mode)
for root, dirs, files in os.walk(path):
for d in dirs:
os.chmod(os.path.join(root, d), mode)
if not only_dirs:
for f in files:
os.chmod(os.path.join(root, f), mode)
def unicode_str(obj, encoding = "utf-8"):
"""
Safe str() function that always returns an unicode string.
Do nothing if the string was already unicode.
"""
if sys.version_info.major >= 3: # python 3?
return str(obj)
if type(obj) == unicode: # unicode string?
return obj
if hasattr(obj, "__unicode__"): # Exception object?
return unicode(obj)
return str(obj).decode("utf-8") # strings, numbers, ...
def open_utf8(filename, mode = "r"):
"""
Python 2 compatible replacement for builtin open().
Python 3 added the encoding parameter.
"""
if sys.version_info.major == 2:
return open(filename, mode)
else:
return open(filename, mode=mode, encoding="UTF-8")
def permute_mask(mask):
"""
Return all permutations of the bits in mask.
Doctests:
>>> permute_mask(1)
[0, 1]
>>> permute_mask(5)
[0, 1, 4, 5]
>>> permute_mask(14)
[0, 2, 4, 6, 8, 10, 12, 14]
"""
bit_masks = [bit_mask for bit_mask in (1<>> old_env = os.environ.copy()
>>> os.environ["HOME"] = "/home/test_user"
# XDG_CONFIG_HOME unavailable
>>> os.environ["XDG_CONFIG_HOME"] = ""
>>> XDGDirs.get_config_home("onboard/test.dat")
'/home/test_user/.config/onboard/test.dat'
# XDG_CONFIG_HOME available
>>> os.environ["XDG_CONFIG_HOME"] = "/home/test_user/.config_home"
>>> XDGDirs.get_config_home("onboard/test.dat")
'/home/test_user/.config_home/onboard/test.dat'
# XDG_DATA_HOME unavailable
>>> os.environ["XDG_DATA_HOME"] = ""
>>> XDGDirs.get_data_home("onboard/test.dat")
'/home/test_user/.local/share/onboard/test.dat'
# XDG_DATA_HOME available
>>> os.environ["XDG_DATA_HOME"] = "/home/test_user/.data_home"
>>> XDGDirs.get_data_home("onboard/test.dat")
'/home/test_user/.data_home/onboard/test.dat'
# XDG_CONFIG_DIRS unvailable
>>> os.environ["XDG_CONFIG_HOME"] = ""
>>> os.environ["XDG_CONFIG_DIRS"] = ""
>>> XDGDirs.get_all_config_dirs("onboard/test.dat")
['/home/test_user/.config/onboard/test.dat', '/etc/xdg/onboard/test.dat']
# XDG_CONFIG_DIRS available
>>> os.environ["XDG_CONFIG_HOME"] = ""
>>> os.environ["XDG_CONFIG_DIRS"] = "/etc/xdg/xdg-ubuntu:/etc/xdg"
>>> XDGDirs.get_all_config_dirs("onboard/test.dat")
['/home/test_user/.config/onboard/test.dat', \
'/etc/xdg/xdg-ubuntu/onboard/test.dat', \
'/etc/xdg/onboard/test.dat']
# XDG_DATA_DIRS unvailable
>>> os.environ["XDG_DATA_HOME"] = ""
>>> os.environ["XDG_DATA_DIRS"] = ""
>>> XDGDirs.get_all_data_dirs("onboard/test.dat")
['/home/test_user/.local/share/onboard/test.dat', \
'/usr/local/share/onboard/test.dat', \
'/usr/share/onboard/test.dat']
# XDG_DATA_DIRS available
>>> os.environ["XDG_DATA_HOME"] = ""
>>> os.environ["XDG_DATA_DIRS"] = "/usr/share/gnome:/usr/local/share/:/usr/share/"
>>> XDGDirs.get_all_data_dirs("onboard/test.dat")
['/home/test_user/.local/share/onboard/test.dat', \
'/usr/share/gnome/onboard/test.dat', \
'/usr/local/share/onboard/test.dat', \
'/usr/share/onboard/test.dat']
>>> os.environ = old_env
"""
@staticmethod
def get_config_home(file = None):
"""
User specific config directory.
"""
path = os.environ.get("XDG_CONFIG_HOME")
if path and not os.path.isabs(path):
_logger.warning("XDG_CONFIG_HOME doesn't contain an absolute path,"
"ignoring.")
path = None
if not path:
path = os.path.join(os.path.expanduser("~"), ".config")
if file:
path = os.path.join(path, file)
return path
@staticmethod
def get_config_dirs():
"""
Config directories ordered by preference.
"""
paths = []
value = os.environ.get("XDG_CONFIG_DIRS")
if not value:
value = "/etc/xdg"
paths = value.split(":")
paths = [p for p in paths if os.path.isabs(p)]
return paths
@staticmethod
def get_all_config_dirs(file = None):
paths = [XDGDirs.get_config_home()] + XDGDirs.get_config_dirs()
if file:
paths = [os.path.join(p, file) for p in paths]
return paths
@staticmethod
def find_config_files(file):
""" Find file in all config directories, highest priority first. """
paths = XDGDirs.get_all_config_dirs(file)
return [p for p in paths if os.path.isfile(path) and \
os.access(filename, os.R_OK)]
@staticmethod
def find_config_file(file):
""" Find file of highest priority """
paths = XDGDirs.find_config_files(file)
if paths:
return paths[0]
return None
@staticmethod
def get_data_home(file = None):
"""
User specific data directory.
"""
path = os.environ.get("XDG_DATA_HOME")
if path and not os.path.isabs(path):
_logger.warning("XDG_DATA_HOME doesn't contain an absolute path,"
"ignoring.")
path = None
if not path:
path = os.path.join(os.path.expanduser("~"), ".local", "share")
if file:
path = os.path.join(path, file)
return path
@staticmethod
def get_data_dirs():
"""
Data directories ordered by preference.
"""
paths = []
value = os.environ.get("XDG_DATA_DIRS")
if not value:
value = "/usr/local/share/:/usr/share/"
paths = value.split(":")
paths = [p for p in paths if os.path.isabs(p)]
return paths
@staticmethod
def get_all_data_dirs(file = None):
paths = [XDGDirs.get_data_home()] + XDGDirs.get_data_dirs()
if file:
paths = [os.path.join(p, file) for p in paths]
return paths
@staticmethod
def find_data_files(file):
""" Find file in all data directories, highest priority first. """
paths = XDGDirs.get_all_data_dirs(file)
return [p for p in paths if os.path.isfile(path) and \
os.access(filename, os.R_OK)]
@staticmethod
def find_data_file(file):
""" Find file of highest priority """
paths = XDGDirs.find_data_files(file)
if paths:
return paths[0]
return None
def assure_user_dir_exists(path):
"""
If necessary create user XDG directory.
Raises OSError.
"""
exists = os.path.exists(path)
if not exists:
try:
os.makedirs(path, mode = 0o700)
exists = True
except OSError as ex:
_logger.error(_format("failed to create directory '{}': {}",
path, unicode_str(ex)))
raise ex
return exists
_tag_pattern = re.compile(
"""(?:
<[\w\-_]+ # tag
(?:\s+[\w\-_]+=["'][^"']*["'])* # attributes
/?>
) |
(?:
?[\w\-_]+>
)
""", re.UNICODE|re.DOTALL|re.VERBOSE)
def _iter_markup(markup):
"""
Iterate over tag and non-tag sections of a markup string.
Doctests:
# Never yield for empty string
>>> list(_iter_markup(""))
[]
# must return tag- as well as non-tag sections
>>> list(_iter_markup("testtest2"))
[('', True), ('test', False), ('', True), ('test2', False)]
# should recognize tags with attributes
>>> list(_iter_markup(''))
[('', True)]
>>> list(_iter_markup(''))
[('', True)]
# should recognize tags with end shortcut
>>> list(_iter_markup(' '))
[('', True), (' ', False), ('', True)]
# must not modify input, i.e. concatenated result must equal input text
>>> markup = "asd t est\\n ds te st2 "
>>> "".join([text for text, tag in _iter_markup(markup)]) == markup
True
"""
pos = 0
matches = _tag_pattern.finditer(markup)
for m in matches:
text = markup[pos:m.start()]
if text:
yield text, False
yield m.group(), True
pos = m.end()
text = markup[pos:]
if text:
yield text, False
def escape_markup(markup, preserve_tags = False):
"""
Escape strings of uncertain content for use in Gtk markup.
If requested, markup tags are skipped and won't be escaped.
Doctests:
>>> escape_markup("&<>")
'&<>'
# tags must be escaped when preserve_tags is False
>>> escape_markup("&<>")
'<big>&<></big>'
# tags must not be escaped when preserve_tags is True
>>> escape_markup('&><1<3', True)
'&><1<3'
# whitespace must be preserved
>>> escape_markup("test test2& test3", True)
'test test2& test3'
"""
result = ""
for text, is_tag in _iter_markup(markup):
if is_tag and preserve_tags:
result += text
else:
try:
result += GLib.markup_escape_text(text)
except Exception as ex: # private exception gi._glib.GError
_logger.error("markup_escape_text failed for "
"'{}': {}" \
.format(text, unicode_str(ex)))
return result
class TermColors(object):
""" Singleton class providing ANSI terminal color codes """
# sequence ids
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, \
BOLD, RESET = range(10)
# sequence cache
sequences = {}
def __new__(cls, *args, **kwargs):
""" Singleton magic. """
if not hasattr(cls, "self"):
cls.self = object.__new__(cls, *args, **kwargs)
cls.self.construct()
return cls.self
def __init__(self):
""" Called multiple times, do not use. """
pass
def construct(self):
""" Singleton constructor, runs only once. """
def get(self, seq_id):
"""
Return ANSI character sequence for the given sequence id,
e.g. color index.
"""
seq = self.sequences.get(seq_id)
if seq is None:
seq = ""
if not seq_id is None:
if seq_id >= self.BLACK and seq_id <= self.WHITE:
seq = self._tput("setaf " + str(seq_id))
elif seq_id == self.BOLD:
seq = self._tput("bold")
elif seq_id == self.RESET:
seq = self._tput("sgr0")
self.sequences[seq_id] = seq
return seq
@staticmethod
def _tput(params):
try:
s = subprocess.check_output(("tput " + params).split())
return s.decode("ASCII")
except subprocess.CalledProcessError:
return ""
onboard-1.4.1/Onboard/TextDomain.py 0000644 0001750 0001750 00000107644 13051012134 017405 0 ustar frafu frafu 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright © 2012-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 .
from __future__ import division, print_function, unicode_literals
import os
import re
import glob
import logging
from Onboard.Version import require_gi_versions
require_gi_versions()
try:
from gi.repository import Atspi
except ImportError as e:
pass
from Onboard.TextChanges import TextSpan
from Onboard.utils import KeyCode, Modifiers, unicode_str
_logger = logging.getLogger("TextDomain")
class TextDomains:
""" Collection of all recognized text domains. """
def __init__(self):
# default domain has to be last
self._domains = [
DomainTerminal(),
DomainURL(),
DomainPassword(),
DomainGenericText(),
DomainNOP()
]
def find_match(self, **kwargs):
for domain in self._domains:
if domain.matches(**kwargs):
return domain
return None # should never happen, default domain always matches
def get_nop_domain(self):
return self._domains[-1]
class TextDomain:
"""
Abstract base class as a catch-all for domain specific functionalty.
"""
def __init__(self):
self._url_parser = PartialURLParser()
def matches(self, **kwargs):
# Weed out unity text entries that report being editable but don't
# actually provide methods of the Atspi.Text interface.
return "Text" in kwargs.get("interfaces", [])
def init_domain(self):
""" Called on being selected as the currently active domain. """
pass
def read_context(self, keyboard, accessible):
return NotImplementedError()
def get_text_begin_marker(self):
return ""
def get_auto_separator(self, context):
"""
Get word separator to add after inserting a prediction choice.
Doctests:
>>> from os.path import join
# URL
>>> d = DomainGenericText()
>>> d.get_auto_separator("word http")
' '
>>> d.get_auto_separator("word http://www")
'.'
>>> d.get_auto_separator("word http://www.domain.org/path")
'/'
>>> d.get_auto_separator("word http://www.domain.org/path/document.ext")
''
# filename
>>> import tempfile
>>> from os.path import abspath, dirname, join
>>> def touch(fn):
... with open(fn, mode="w") as f: n = f.write("")
>>> td = tempfile.TemporaryDirectory(prefix="test_onboard _")
>>> dir = td.name
>>> touch(join(dir, "onboard-defaults.conf.example"))
>>> touch(join(dir, "onboard-defaults.conf.example.another"))
>>>
>>> d.get_auto_separator("/etc")
'/'
>>> d.get_auto_separator(join(dir, "onboard-defaults"))
'.'
>>> d.get_auto_separator(join(dir, "onboard-defaults.conf"))
'.'
>>> d.get_auto_separator(join(dir, "onboard-defaults.conf.example"))
''
>>> d.get_auto_separator(join(dir, "onboard-defaults.conf.example.another"))
' '
# filename in directory with spaces
>>> import tempfile
>>> td = tempfile.TemporaryDirectory(prefix="test onboard _")
>>> dir = td.name
>>> fn = os.path.join(dir, "onboard-defaults.conf.example")
>>> touch(fn)
>>> d.get_auto_separator(join(dir, "onboard-defaults"))
'.'
# no false positives for valid filenames before the current token
>>> d.get_auto_separator(dir + "/onboard-defaults no-file")
' '
"""
separator = " "
# Split at whitespace to catch whole URLs/file names and
# keep separators.
strings = re.split('(\s+)', context)
if strings:
string = strings[-1]
if self._url_parser.is_maybe_url(string):
separator = self._url_parser.get_auto_separator(string)
else:
fn = self._search_valid_file_name(strings)
if fn:
string = fn
if self._url_parser._is_maybe_filename(string):
url = "file://" + string
separator = self._url_parser.get_auto_separator(url)
return separator
def _search_valid_file_name(self, strings):
"""
Search for a valid filename backwards across separators.
Doctests:
>>> import tempfile, re, os.path
>>>
>>> d = DomainGenericText()
>>>
>>> td = tempfile.TemporaryDirectory(prefix="test onboard _")
>>> dir = td.name
>>> fn1 = os.path.join(dir, "file")
>>> fn2 = os.path.join(dir, "file with many spaces")
>>> with open(fn1, mode="w") as f: n = f.write("")
>>> with open(fn2, mode="w") as f: n = f.write("")
# simple file in dir with spaces must return as filename
>>> strings = re.split('(\s+)', fn1)
>>> "/test onboard" in d._search_valid_file_name(strings)
True
# file with spaces in dir with spaces must return as filename
>>> strings = re.split('(\s+)', fn2)
>>> "/test onboard" in d._search_valid_file_name(strings)
True
# random string after a valid file must not be confused with a filename
>>> strings = re.split('(\s+)', fn2 + " no-file")
>>> d._search_valid_file_name(strings) is None
True
"""
# Search backwards across spaces for an absolute filename.
max_sections = 16 # allow this many path sections (separators+tokens)
for i in range(min(max_sections, len(strings))):
fn = "".join(strings[-1-i:])
# Is it (part of) a valid absolute filename?
# Do least impact checks first.
if self._url_parser._is_maybe_filename(fn) and \
os.path.isabs(fn):
# Does a file or directory of this name exist?
if os.path.exists(fn):
return fn
# Check if it is at least an incomplete filename of
# an existing file
files = glob.glob(fn + "*")
if files:
return fn
return None
def grow_learning_span(self, text_span):
"""
Grow span before learning to include e.g. whole URLs.
Doctests:
>>> d = DomainGenericText()
# Span doesn't grow for simple words
>>> d.grow_learning_span(TextSpan(8, 1, "word1 word2 word3"))
(8, 1, 'r')
# Span grows to include a complete URL
>>> d.grow_learning_span(TextSpan(13, 1, "http://www.domain.org"))
(0, 21, 'http://www.domain.org')
# Span grows to include multiple complete URLs
>>> d.grow_learning_span(TextSpan(19, 13, "http://www.domain.org word http://slashdot.org"))
(0, 46, 'http://www.domain.org word http://slashdot.org')
# Span grows to include a complete filename
>>> d.grow_learning_span(TextSpan(10, 1, "word1 /usr/bin/bash word2"))
(6, 13, '/usr/bin/bash')
# Edge cases
>>> d.grow_learning_span(TextSpan(6, 0, "word1 /usr/bin/bash word2"))
(6, 0, '')
>>> d.grow_learning_span(TextSpan(19, 0, "word1 /usr/bin/bash word2"))
(19, 0, '')
>>> d.grow_learning_span(TextSpan(6, 1, "word1 /usr/bin/bash word2"))
(6, 13, '/usr/bin/bash')
>>> d.grow_learning_span(TextSpan(18, 1, "word1 /usr/bin/bash word2"))
(6, 13, '/usr/bin/bash')
# Large text with text offset>0: returned position must be offset too
>>> d.grow_learning_span(TextSpan(116, 1,
... "word1 /usr/bin/bash word2", 100))
(106, 13, '/usr/bin/bash')
"""
text = text_span.get_text()
offset = text_span.text_begin()
begin = text_span.begin() - offset
end = text_span.end() - offset
sections, spans = self._split_growth_sections(text)
for i, s in enumerate(spans):
if begin < s[1] and end > s[0]: # intersects?
section = sections[i]
span = spans[i]
if self._url_parser.is_maybe_url(section) or \
self._url_parser._is_maybe_filename(section):
begin = min(begin, span[0])
end = max(end, span[1])
return begin + offset, end - begin, text[begin:end]
def can_record_insertion(self, accessible, pos, length):
return True
def can_give_keypress_feedback(self):
return True
def can_spell_check(self, section_span):
return False
def can_auto_correct(self, section_span):
return False
def can_auto_punctuate(self, has_begin_of_text):
return False
def can_suggest_before_typing(self):
""" Can give word suggestions before typing has started? """
return True
def handle_key_press(self, keycode, mod_mask):
return True, None # entering_text, end_of_editing
_growth_sections_pattern = re.compile("[^\s?#@]+", re.DOTALL)
def _split_growth_sections(self, text):
"""
Split text at whitespace and other delimiters where
growing learning spans should stop.
Doctests:
>>> d = DomainGenericText()
>>> d._split_growth_sections("word1 www.domain.org word2. http://test")
(['word1', 'www.domain.org', 'word2.', 'http://test'], [(0, 5), (6, 20), (21, 27), (28, 39)])
>>> d._split_growth_sections("http://www.domain.org/?p=1#anchor")
(['http://www.domain.org/', 'p=1', 'anchor'], [(0, 22), (23, 26), (27, 33)])
>>> d._split_growth_sections("http://user:pass@www.domain.org")
(['http://user:pass', 'www.domain.org'], [(0, 16), (17, 31)])
"""
matches = self._growth_sections_pattern.finditer(text)
tokens = []
spans = []
for m in matches:
tokens.append(m.group())
spans.append(m.span())
return tokens, spans
class DomainNOP(TextDomain):
""" Do-nothing domain, no focused accessible. """
def matches(self, **kwargs):
return True
def read_context(self, keyboard, accessible):
return "", "", 0, TextSpan(), False, 0
def get_auto_separator(self, context):
""" Get word separator to add after inserting a prediction choice. """
return ""
class DomainPassword(DomainNOP):
""" Do-nothing domain for password entries """
def matches(self, **kwargs):
return kwargs.get("role") == Atspi.Role.PASSWORD_TEXT
def can_give_keypress_feedback(self):
return False
class DomainGenericText(TextDomain):
""" Default domain for generic text entry """
def matches(self, **kwargs):
return TextDomain.matches(self, **kwargs)
def read_context(self, keyboard, accessible):
""" Extract prediction context from the accessible """
# get caret position from selection
selection = accessible.get_selection()
# get text around the caret position
try:
count = accessible.get_character_count()
if selection is None:
offset = accessible.get_caret_offset()
# In Zesty, firefox 50.1 often returns caret position -1
# when typing into the urlbar. Assume we are at the end
# of the text when that happens.
if offset < 0:
_logger.warning("DomainGenericText.read_context(): "
"Atspi.Text.get_caret_offset() "
"returned invalid {}. "
"Pretending the cursor is at the end "
"of the text at offset {}."
.format(offset, count))
offset = count
selection = (offset, offset)
r = accessible.get_text_at_offset(
selection[0], Atspi.TextBoundaryType.LINE_START)
except Exception as ex: # Private exception gi._glib.GError when
# gedit became unresponsive.
_logger.info("DomainGenericText.read_context(), text: " +
unicode_str(ex))
return None
line = unicode_str(r.content).replace("\n", "")
line_caret = max(selection[0] - r.start_offset, 0)
begin = max(selection[0] - 256, 0)
end = min(selection[0] + 100, count)
try:
text = accessible.get_text(begin, end)
except Exception as ex: # Private exception gi._glib.GError when
# gedit became unresponsive.
_logger.info("DomainGenericText.read_context(), text2: " +
unicode_str(ex))
return None
text = unicode_str(text)
# Not all text may be available for large selections. We only need the
# part before the begin of the selection/caret.
selection_span = TextSpan(selection[0], selection[1] - selection[0],
text, begin)
context = text[:selection[0] - begin]
begin_of_text = begin == 0
begin_of_text_offset = 0
return (context, line, line_caret, selection_span,
begin_of_text, begin_of_text_offset)
def can_spell_check(self, section_span):
"""
Can we auto-correct this span?.
Doctests:
>>> d = DomainGenericText()
>>> d.can_spell_check(TextSpan(0, 3, "abc"))
True
>>> d.can_spell_check(TextSpan(4, 3, "abc def"))
True
>>> d.can_spell_check(TextSpan(0, 20, "http://www.domain.org"))
False
"""
section = section_span.get_span_text()
return not self._url_parser.is_maybe_url(section) and \
not self._url_parser._is_maybe_filename(section)
return True
def can_auto_correct(self, section_span):
"""
Can we auto-correct this span?.
"""
return self.can_spell_check(section_span)
def can_auto_punctuate(self, has_begin_of_text):
return True
def get_text_begin_marker(self):
return ""
class DomainTerminal(TextDomain):
""" Terminal entry, in particular gnome-terminal """
_prompt_patterns = tuple(re.compile(p, re.UNICODE) for p in
(
"^gdb$ ",
"^>>> ", # python
"^In \[[0-9]*\]: ", # ipython
"^:", # vi command mode
"^/", # vi search
"^\?", # vi reverse search
"\$ ", # generic prompt
"# ", # root prompt
"^.*?@.*?/.*?> " # fish
)
)
_prompt_blacklist_patterns = tuple(re.compile(p, re.UNICODE) for p in
(
"^\(.*\)`.*': ", # bash incremental search
)
)
def matches(self, **kwargs):
return TextDomain.matches(self, **kwargs) and \
kwargs.get("role") == Atspi.Role.TERMINAL
def init_domain(self):
pass
def read_context(self, keyboard, accessible):
"""
Extract prediction context from the accessible
"""
try:
offset = accessible.get_caret_offset()
except Exception as ex: # Private exception gi._glib.GError
# when gedit became unresponsive.
_logger.info("DomainTerminal.read_context(): " +
unicode_str(ex))
return None
context_lines, prompt_length, line, line_start, line_caret = \
self._get_text_after_prompt(
accessible, offset,
keyboard.get_last_typed_was_separator())
if prompt_length:
begin_of_text = True
begin_of_text_offset = line_start
else:
begin_of_text = False
begin_of_text_offset = None
context = "".join(context_lines)
before_line = "".join(context_lines[:-1])
selection_span = TextSpan(offset, 0,
before_line + line,
line_start - len(before_line))
result = (context, line, line_caret, selection_span,
begin_of_text, begin_of_text_offset)
return result
def _get_text_after_prompt(self, accessible, caret_offset,
last_typed_was_separator=None):
"""
Return text from the input area of the terminal after the prompt.
Doctests:
>>> class AtspiTextRangeMockup:
... pass
>>> class AccessibleMockup:
... def __init__(self, text, width):
... self._text = text
... self._width = width
... def get_text_at_offset(self, offset, boundary):
... line = offset // self._width
... lbegin = line * self._width
... r = AtspiTextRangeMockup()
... r.content = self._text[lbegin:lbegin+self._width]
... r.start_offset = lbegin
... return r
... def get_text_before_offset(self, offset, boundary):
... return self.get_text_at_offset(offset - self._width, boundary)
... def is_byobu(self):
... return False
>>> d = DomainTerminal()
# Single line
>>> a = AccessibleMockup("abc$ ls /etc\\n", 15)
>>> d._get_text_after_prompt(a, 12)
(['ls /etc'], 5, 'ls /etc\\n', 5, 7)
# Two lines
>>> a = AccessibleMockup("abc$ ls /e"
... "tc\\n", 10)
>>> d._get_text_after_prompt(a, 12)
(['ls /e', 'tc'], 5, 'tc\\n', 10, 2)
# Three lines: prompt not detected
# More that two lines are not supported. The probability of detecting
# "prompts" in random scrolling data rises with each additional line.
>>> a = AccessibleMockup("abc$ ls /e"
... "tc/X11/xor"
... "g.conf.d\\n", 10)
>>> d._get_text_after_prompt(a, 28)
(['tc/X11/xor', 'g.conf.d'], 0, 'g.conf.d\\n', 20, 8)
# Two lines with slash at the beginning of the second: detect vi
# search prompt. Not ideal, but vi is important too.
>>> a = AccessibleMockup("abc$ ls /etc"
... "/X11\\n", 12)
>>> d._get_text_after_prompt(a, 16)
(['X11'], 1, 'X11\\n', 13, 3)
"""
r = accessible.get_text_at_offset(
caret_offset, Atspi.TextBoundaryType.LINE_START)
line = unicode_str(r.content)
line_start = r.start_offset
line_caret = caret_offset - line_start
# remove prompt from the current or previous lines
context_lines = []
prompt_length = None
l = line[:line_caret]
# Zesty: byobu running in gnome-terminal doesn't report trailing
# spaces in text and caret-position.
# Awful hack: assume there is always a trailing space when the caret
# is at the end of the line and we just typed a separator.
if line[line_caret:] == "\n" and \
last_typed_was_separator and \
accessible.is_byobu():
l += " "
for i in range(2):
# matching blacklisted prompt? -> cancel whole context
if self._find_blacklisted_prompt(l):
context_lines = []
prompt_length = None
break
prompt_length = self._find_prompt(l)
context_lines.insert(0, l[prompt_length:])
if i == 0:
line = line[prompt_length:] # cut prompt from input line
line_start += prompt_length
line_caret -= prompt_length
if prompt_length:
break
# no prompt yet -> let context reach
# across one more line break
r = accessible.get_text_before_offset(
caret_offset, Atspi.TextBoundaryType.LINE_START)
l = unicode_str(r.content)
result = (context_lines, prompt_length,
line, line_start, line_caret)
return result
def _find_prompt(self, context):
"""
Search for a prompt and return the offset where the user input starts.
Until we find a better way just look for some common prompt patterns.
"""
for pattern in self._prompt_patterns:
match = pattern.search(context)
if match:
return match.end()
return 0
def _find_blacklisted_prompt(self, context):
for pattern in self._prompt_blacklist_patterns:
match = pattern.search(context)
if match:
return match.end()
return None
def get_text_begin_marker(self):
return ""
def can_record_insertion(self, accessible, offset, length):
# Only record (for learning) when there is a known prompt in sight.
# Problem: learning won't happen for uncommon prompts, but less random
# junk scrolling by should enter the user model in return.
context_lines, prompt_length, line, line_start, line_caret = \
self._get_text_after_prompt(accessible, offset)
return bool(prompt_length)
def can_suggest_before_typing(self):
""" Can give word suggestions before typing has started? """
# Mostly prevent updates to word suggestions while text is scrolling by
return False
def handle_key_press(self, keycode, mod_mask):
"""
End recording and learn when pressing [Return]
because text that is scrolled out of view is
lost in a terminal.
"""
if keycode == KeyCode.Return or \
keycode == KeyCode.KP_Enter:
return False, True
elif keycode == KeyCode.C and mod_mask & Modifiers.CTRL:
return False, False
return True, None # entering_text, end_of_editing
def can_auto_punctuate(self, has_begin_of_text):
"""
Only auto-punctuate in Terminal when no prompt was detected.
Intention is to allow punctuation assistance in editors, but disable
it when entering commands at the prompt, e.g. for "cd ..".
"""
return not has_begin_of_text
class DomainURL(DomainGenericText):
""" (Firefox) address bar """
def matches(self, **kwargs):
return kwargs.get("is_urlbar", False)
def get_auto_separator(self, context):
"""
Get word separator to add after inserting a prediction choice.
"""
return self._url_parser.get_auto_separator(context)
def get_text_begin_marker(self):
return ""
def can_spell_check(self, section_span):
return False
class PartialURLParser:
"""
Parse partial URLs and predict separators.
Parsing is neither complete nor RFC prove but probably doesn't
have to be either. The goal is to save key strokes for the
most common cases.
Doctests:
>>> p = PartialURLParser()
>>> p.tokenize_url('http://user:pass@www.do-mai_n.nl/path/name.ext')
['http', '://', 'user', ':', 'pass', '@', 'www', '.', 'do-mai_n', '.', 'nl', '/', 'path', '/', 'name', '.', 'ext']
>>> p.tokenize_url('user:pass@www.do-mai_n.nl/path/name.ext')
['user', ':', 'pass', '@', 'www', '.', 'do-mai_n', '.', 'nl', '/', 'path', '/', 'name', '.', 'ext']
>>> p.tokenize_url('www.do-mai_n.nl/path/name.ext')
['www', '.', 'do-mai_n', '.', 'nl', '/', 'path', '/', 'name', '.', 'ext']
>>> p.tokenize_url('www.do-mai_n.nl')
['www', '.', 'do-mai_n', '.', 'nl']
"""
_gTLDs = ["aero", "asia", "biz", "cat", "com", "coop", "info", "int",
"jobs", "mobi", "museum", "name", "net", "org", "pro", "tel",
"travel", "xxx"]
_usTLDs = ["edu", "gov", "mil"]
_ccTLDs = ["ac", "ad", "ae", "af", "ag", "ai", "al", "am", "an", "ao",
"aq", "ar", "as", "at", "au", "aw", "ax", "az", "ba", "bb",
"bd", "be", "bf", "bg", "bh", "bi", "bj", "bm", "bn", "bo",
"br", "bs", "bt", "bv", "bw", "by", "bz", "ca", "cc", "cd",
"cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr",
"cs", "cu", "cv", "cx", "cy", "cz", "dd", "de", "dj", "dk",
"dm", "do", "dz", "ec", "ee", "eg", "eh", "er", "es", "et",
"eu", "fi", "fj", "fk", "fm", "fo", "fr", "ga", "gb", "gd",
"ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq",
"gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr",
"ht", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir",
"is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki",
"km", "kn", "kp", "kr", "kw", "ky", "kz", "la", "lb", "lc",
"li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc",
"md", "me", "mg", "mh", "mk", "ml", "mm", "mn", "mo", "mp",
"mq", "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz",
"na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr",
"nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl",
"pm", "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro",
"rs", "ru", "rw", "sa", "sb", "sc", "sd", "se", "sg", "sh",
"si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "ss", "st",
"su", "sv", "sy", "sz", "tc", "td", "tf", "tg", "th", "tj",
"tk", "tl", "tm", "tn", "to", "tp", "tr", "tt", "tv", "tw",
"tz", "ua", "ug", "uk", "us", "uy", "uz", "va", "vc", "ve",
"vg", "vi", "vn", "vu", "wf", "ws", "ye", "yt", "yu", "za",
"zm", "zw"]
_TLDs = frozenset(_gTLDs + _usTLDs + _ccTLDs)
_schemes = ["http", "https", "ftp", "file"]
_protocols = ["mailto", "apt"]
_all_schemes = _schemes + _protocols
_url_pattern = re.compile("([\w-]+)|(\W+)", re.UNICODE)
def iter_url(self, url):
return self._url_pattern.finditer(url)
def tokenize_url(self, url):
return[group for match in self.iter_url(url)
for group in match.groups() if not group is None]
def is_maybe_url(self, context):
"""
Is this maybe something looking like an URL?
Doctests:
>>> p = PartialURLParser()
>>> p.is_maybe_url("http")
False
>>> p.is_maybe_url("http:")
True
>>> p.is_maybe_url("http://www.domain.org")
True
>>> p.is_maybe_url("www.domain.org")
True
>>> p.is_maybe_url("www.domain")
False
>>> p.is_maybe_url("www")
False
"""
tokens = self.tokenize_url(context)
# with scheme
if len(tokens) >= 2:
token = tokens[0]
septok = tokens[1]
if token in self._all_schemes and septok.startswith(":"):
return True
# without scheme
if len(tokens) >= 5:
if tokens[1] == "." and tokens[3] == ".":
try:
index = tokens.index("/")
except ValueError:
index = 0
if index >= 4:
hostname = tokens[:index]
else:
hostname = tokens
if hostname[-1] in self._TLDs:
return True
return False
def _is_maybe_filename(self, string):
return "/" in string
def get_auto_separator(self, context):
"""
Get word separator to add after inserting a prediction choice.
Doctests:
>>> p = PartialURLParser()
>>> p.get_auto_separator("http")
'://'
>>> p.get_auto_separator("www")
'.'
>>> p.get_auto_separator("domain.org")
'/'
>>> p.get_auto_separator("www.domain.org")
'/'
>>> p.get_auto_separator("http://www.domain")
'.'
>>> p.get_auto_separator("http://www.domain.org")
'/'
>>> p.get_auto_separator("http://www.domain.co") # ambiguous co/ or co.uk/
'/'
>>> p.get_auto_separator("http://www.domain.co.uk")
'/'
>>> p.get_auto_separator("http://www.domain.co.uk/home")
'/'
>>> p.get_auto_separator("http://www.domain.co/home")
'/'
>>> p.get_auto_separator("http://www.domain.org/home")
'/'
>>> p.get_auto_separator("http://www.domain.org/home/index.html")
''
>>> p.get_auto_separator("mailto")
':'
# local files
>>> import tempfile
>>> from os.path import abspath, dirname, join
>>> def touch(fn):
... with open(fn, mode="w") as f: n = f.write("")
>>> td = tempfile.TemporaryDirectory(prefix="test onboard _")
>>> dir = td.name
>>> touch(join(dir, "onboard-defaults.conf.example"))
>>> touch(join(dir, "onboard-defaults.conf.example.another"))
>>>
>>> import glob
>>> #glob.glob(dir+"/**")
>>> p.get_auto_separator("file")
':///'
>>> p.get_auto_separator("file:///home")
'/'
>>> p.get_auto_separator("file://"+join(dir, "onboard-defaults"))
'.'
>>> p.get_auto_separator("file://"+join(dir, "onboard-defaults.conf"))
'.'
>>> p.get_auto_separator("file://"+join(dir, "onboard-defaults.conf.example"))
''
>>> p.get_auto_separator("file://"+join(dir, "onboard-defaults.conf.example.another"))
' '
# Non-existing filename: we don't know, don't guess a separator
>>> p.get_auto_separator("file:///tmp/onboard1234")
''
# Non-existing filename: if basename has an extension assume we're done
>>> p.get_auto_separator("file:///tmp/onboard1234.txt")
' '
# Relative filename: we don't know current dir, return empty separator
>>> p.get_auto_separator("file://tmp")
''
"""
separator = None
SCHEME, PROTOCOL, DOMAIN, PATH = range(4)
component = SCHEME
last_septok = ""
matches = tuple(self.iter_url(context))
for index, match in enumerate(matches):
groups = match.groups()
token = groups[0]
septok = groups[1]
if septok:
last_septok = septok
if index < len(matches)-1:
next_septok = matches[index+1].groups()[1]
else:
next_septok = ""
if component == SCHEME:
if token:
if token == "file":
separator = ":///"
component = PATH
continue
if token in self._schemes:
separator = "://"
component = DOMAIN
continue
elif token in self._protocols:
separator = ":"
component = PROTOCOL
continue
else:
component = DOMAIN
if component == DOMAIN:
if token:
separator = "."
if last_septok == "." and \
next_septok != "." and \
token in self._TLDs:
separator = "/"
component = PATH
continue
if component == PATH:
separator = ""
if component == PROTOCOL:
separator = ""
if component == PATH and not separator:
file_scheme = "file://"
if context.startswith(file_scheme):
filename = context[len(file_scheme):]
separator = self._get_filename_separator(filename)
else:
if not last_septok == ".":
separator = "/"
if separator is None:
separator = " " # may be entering search terms, keep space as default
return separator
def _get_filename_separator(self, filename):
"""
Get auto separator for a partial filename.
"""
separator = None
if os.path.isabs(filename):
files = glob.glob(filename + "*")
files += glob.glob(filename + "/*") # look inside directories too
separator = self._get_separator_from_file_list(filename, files)
if separator is None:
basename = os.path.basename(filename)
if "." in basename:
separator = " "
else:
separator = ""
return separator
@staticmethod
def _get_separator_from_file_list(filename, files):
"""
Extract separator from a list of matching filenames.
Doctests:
>>> p = PartialURLParser
# no matching files: return None, assume new file we can't check
>>> p._get_separator_from_file_list("/dir/file", [])
# complete file: we're done, continue with space separator
>>> p._get_separator_from_file_list("/dir/file.ext", ["/dir/file.ext"])
' '
# multiple files with identical separator: return that separator
>>> p._get_separator_from_file_list("/dir/file",
... ["/dir/file.ext1", "/dir/file.ext2"])
'.'
# multiple files with different separators: return empty separator
>>> p._get_separator_from_file_list("/dir/file",
... ["/dir/file.ext", "/dir/file-ext"])
''
# directory
>>> p._get_separator_from_file_list("/dir",
... ["/dir/file.ext1", "/dir/file.ext2"])
'/'
>>> p._get_separator_from_file_list("/dir",
... ["/dir", "/dir/file.ext2"])
'/'
# multiple extensions
>>> files = ["/dir/dir/file.ext1.ext2", "/dir/dir/file.ext1.ext3"]
>>> p._get_separator_from_file_list("/dir/dir/file", files)
'.'
>>> p._get_separator_from_file_list("/dir/dir/file.ext1", files)
'.'
>>> p._get_separator_from_file_list("/dir/dir/file.ext1.ext2", files)
' '
# partial path match
>>> files = ["/dir/dir/file", "/dir/dir/file.ext1",
... "/dir/dir/file.ext2"]
>>> p._get_separator_from_file_list("/dir/dir/file", files)
''
>>> p._get_separator_from_file_list("/dir/dir/file.ext1", files)
' '
"""
separator = None
l = len(filename)
separators = set([f[l:l+1] for f in files \
if f.startswith(filename)])
# directory?
if len(separators) == 2 and "/" in separators and "" in separators:
separator = "/"
# end of filename?
elif len(separators) == 1 and "" in separators:
separator = " "
# unambigous separator?
elif len(separators) == 1:
separator = separators.pop()
# multiple separators
elif separators:
separator = ""
return separator
onboard-1.4.1/Onboard/WindowUtils.py 0000644 0001750 0001750 00000115452 13051012134 017615 0 ustar frafu frafu 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright © 2012-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 .
""" Window manipulation and other helpers """
from __future__ import division, print_function, unicode_literals
import time
from math import sqrt, pi
from Onboard.Version import require_gi_versions
require_gi_versions()
from gi.repository import GLib, Gtk, Gdk
from Onboard.utils import Rect, Version
from Onboard.Timer import Timer
from Onboard.definitions import Handle, HandleFunction
import Onboard.osk as osk
### Logging ###
import logging
_logger = logging.getLogger("WindowUtils")
###############
class WindowManipulator(object):
"""
Adds resize and move capability to windows.
Meant for resizing windows without decoration or resize gripper.
Quirks to remember:
Keyboard window:
- Always use threshold when move button was pressed
in order to support long press to show the touch handles.
- Never use the threshold for the enlarged touch handles.
They are only temporarily visible and thus don't need protection.
IconPalette:
- Always use threshold when trying to move, otherwise
clicking to unhide the keyboard window won't work.
"""
def __init__(self):
self.hit_frame_width = 10 # size of resize corners and edges
self.drag_protection = True # enable protection threshold
self._temporary_unlock_time = None
# seconds until protection threshold returns
# - counts from drag end in fallback mode
# - counts from drag start in system mode
# (unfortunately)
self.temporary_unlock_delay = 6.0
self.min_window_size = (50, 50)
self._drag_start_pointer = None
self._drag_start_offset = None
self._drag_start_rect = None
self._drag_handle = None
self._drag_handles = Handle.ALL
self._drag_active = False # has window move/resize actually started yet?
self._drag_threshold = 8
self._drag_snap_threshold = 16
self._lock_x_axis = False
self._lock_y_axis = False
self._last_drag_handle = None
self._monitor_rects = None # cache them to save the lookup time
def set_min_window_size(self, w, h):
self.min_window_size = (w, h)
def get_min_window_size(self):
return self.min_window_size
def get_hit_frame_width(self):
return self.hit_frame_width
def enable_drag_protection(self, enable):
self.drag_protection = enable
def reset_drag_protection(self):
self._temporary_unlock_time = None
def get_resize_frame_rect(self):
try:
return self.get_keyboard_frame_rect()
except AttributeError:
return Rect(0, 0,
self.get_allocated_width(),
self.get_allocated_height())
def get_drag_start_rect(self):
return self._drag_start_rect
def get_drag_window(self):
return self
def get_drag_handles(self):
return self._drag_handles
def set_drag_handles(self, handles):
self._drag_handles = handles
def get_handle_function(self, handle):
return HandleFunction.NORMAL
def get_drag_threshold(self):
return 8
def get_always_visible_rect(self):
""" Rectangle in canvas coordinates that must not leave the screen. """
return None
def lock_x_axis(self, lock):
""" Set to False to constraint movement in x. """
self._lock_x_axis = lock
def lock_y_axis(self, lock):
""" Set to True to constraint movement in y. """
self._lock_y_axis = lock
def handle_press(self, sequence, move_on_background = False):
hit = self.hit_test_move_resize(sequence.point)
if not hit is None:
if hit == Handle.MOVE:
self.start_move_window(sequence.root_point)
else:
self.start_resize_window(hit, sequence.root_point)
function = self.get_handle_function(hit)
if function == HandleFunction.ASPECT_RATIO:
self.on_handle_aspect_ratio_pressed()
return True
if move_on_background and \
Handle.MOVE in self.get_drag_handles():
self.start_move_window(sequence.root_point)
return True
return False
def handle_motion(self, sequence, fallback = False):
if not self.is_drag_initiated():
return
snap_to_cursor = False
x_root, y_root = sequence.root_point
dx = x_root - self._drag_start_pointer[0]
dy = y_root - self._drag_start_pointer[1]
# distance threshold, protection from accidental drags
if not self._drag_active:
d = sqrt(dx*dx + dy*dy)
drag_active = not self.drag_protection
if self.drag_protection:
# snap off for temporary unlocking
if self._temporary_unlock_time is None and \
d >= self._drag_threshold:
self._temporary_unlock_time = 1
# Snap to cursor position for large drag thresholds
# Dragging is smoother without snapping, but for large
# thresholds, the cursor ends up far away from the
# window and there is a danger of windows going offscreen.
if d >= self._drag_snap_threshold:
snap_to_cursor = True
else:
self._drag_start_offset[0] += dx
self._drag_start_offset[1] += dy
if not self._temporary_unlock_time is None:
drag_active = True
else:
self._temporary_unlock_time = 1 # unlock for touch handles too
self._drag_active |= drag_active
# move/resize
if self._drag_active:
if fallback:
self._handle_motion_fallback(dx, dy)
else:
self._handle_motion_system(dx, dy, snap_to_cursor, sequence)
# give keyboard window a chance to react
self.on_drag_activated()
def _handle_motion_system(self, dx, dy, snap_to_cursor, sequence):
"""
Let the window manager do the moving
This fixes issues like not reaching edges at high move speed
and not being able to snap off a maximized window.
Does nothing in force-to-top mode (override redirect or
type hint "DOCK").
"""
window = self.get_drag_window()
if window:
x, y = sequence.root_point
if self.is_moving():
if snap_to_cursor:
x = x - dx # snap to cursor
y = y - dy
window.begin_move_drag(1, x, y, sequence.time)
elif self.is_resizing():
# Compensate for weird begin_resize_drag behaviour:
# catch up with the mouse cursor
if snap_to_cursor:
if not self._drag_start_rect.is_point_within((x, y)):
x, y = x + dx, y + dy
window.begin_resize_drag(self._drag_handle, 1,
x, y, sequence.time)
def stop_system_drag(self):
"""
Call this when the system drag has ended.
We need this to kick off the on_drag_done() call for KbdWindow.
"""
self.stop_drag()
def _handle_motion_fallback(self, dx, dy):
""" handle dragging for window move and resize """
if not self.is_drag_initiated():
return
function = self.get_handle_function(self._drag_handle)
if function == HandleFunction.ASPECT_RATIO:
if self._drag_handle == Handle.WEST:
dx *= -1
self.on_handle_aspect_ratio_motion(dx, dy)
else:
wx = self._drag_start_pointer[0] + dx - self._drag_start_offset[0]
wy = self._drag_start_pointer[1] + dy - self._drag_start_offset[1]
if self._drag_handle == Handle.MOVE:
# contrain axis movement
if self._lock_x_axis:
wx = self.get_drag_window().get_position()[0]
if self._lock_y_axis:
wx = self.get_drag_window().get_position()[1]
# move window
x, y = self.limit_position(wx, wy)
w, h = None, None
else:
# resize window
wmin, hmin = self.get_min_window_size()
rect = self._drag_start_rect
x0, y0, x1, y1 = rect.to_extents()
w, h = rect.get_size()
if self._drag_handle in [Handle.NORTH,
Handle.NORTH_WEST,
Handle.NORTH_EAST]:
y0 = min(wy, y1 - hmin)
if self._drag_handle in [Handle.WEST,
Handle.NORTH_WEST,
Handle.SOUTH_WEST]:
x0 = min(wx, x1 - wmin)
if self._drag_handle in [Handle.EAST,
Handle.NORTH_EAST,
Handle.SOUTH_EAST]:
x1 = max(wx + w, x0 + wmin)
if self._drag_handle in [Handle.SOUTH,
Handle.SOUTH_WEST,
Handle.SOUTH_EAST]:
y1 = max(wy + h, y0 + hmin)
x, y, w, h = x0, y0, x1 -x0, y1 - y0
self._move_resize(x, y, w, h)
def on_handle_aspect_ratio_pressed(self):
"""
Overload this to process start of dragging of
handles with ASPECT_RATIO function.
"""
pass
def on_handle_aspect_ratio_motion(self, wx, wy):
"""
Overload this to process motion changes of
handles with ASPECT_RATIO function.
"""
pass
def set_drag_cursor_at(self, point, allow_drag_cursors = True):
""" set the mouse cursor """
window = self.get_window()
if not window:
return
cursor_type = None
if allow_drag_cursors or \
not self._drag_handle is None: # already dragging a handle?
cursor_type = self.get_drag_cursor_at(point)
# set/reset cursor
if not cursor_type is None:
cursor = Gdk.Cursor(cursor_type)
if cursor:
window.set_cursor(cursor)
else:
window.set_cursor(None)
def reset_drag_cursor(self):
""" set the mouse cursor """
window = self.get_window()
if not window:
return
if self._drag_handle is None: # not dragging a handle?
window.set_cursor(None)
def get_drag_cursor_at(self, point):
hit = self._drag_handle
if hit is None:
hit = self.hit_test_move_resize(point)
if not hit is None and \
not hit == Handle.MOVE or self.is_drag_active(): # delay it for move
return Handle.CURSOR_TYPES[hit]
return None
def start_move_window(self, point = None):
self.start_drag(point)
self._drag_handle = Handle.MOVE
self._last_drag_handle = self._drag_handle
def stop_move_window(self):
self.stop_drag()
def start_resize_window(self, handle, point = None):
self.start_drag(point)
self._drag_handle = handle
self._last_drag_handle = self._drag_handle
def start_drag(self, point = None):
self._monitor_rects = None
# Find the pointer position for the occasions when we are
# not being called from an event (move button).
if not point:
rootwin = Gdk.get_default_root_window()
dunno, x_root, y_root, mask = rootwin.get_pointer()
point = (x_root, y_root)
# rmember pointer and window positions
window = self.get_drag_window()
x, y = window.get_position()
self._drag_start_pointer = point
self._drag_start_offset = [point[0] - x, point[1] - y]
self._drag_start_rect = Rect.from_position_size(window.get_position(),
window.get_size())
# not yet actually moving the window
self._drag_active = False
# get the threshold
self._drag_threshold = self.get_drag_threshold()
# check if the temporary threshold unlocking has expired
if not self.drag_protection or \
not self._temporary_unlock_time is None and \
time.time() - self._temporary_unlock_time > \
self.temporary_unlock_delay:
self._temporary_unlock_time = None
# give keyboard window a chance to react
self.on_drag_initiated()
def stop_drag(self):
if self.is_drag_initiated():
if self._temporary_unlock_time is None:
# snap back to start position
if self.drag_protection:
self._move_resize(*self._drag_start_rect)
else:
# restart the temporary unlock period
self._temporary_unlock_time = time.time()
self._drag_start_offset = None
self._drag_handle = None
self._drag_active = False
self.move_into_view()
# give keyboard window a chance to react
self.on_drag_done()
def on_drag_initiated(self):
"""
User controlled drag initiated, but drag hasn't actually begun yet.
"""
pass
def on_drag_activated(self):
"""
Moving/resizing has begun.
"""
pass
def on_drag_done(self):
"""
User controlled drag ended.
overload this in derived classes.
"""
pass
def is_drag_initiated(self):
""" Button pressed down on a drag handle, not yet actually dragging """
return bool(self._drag_start_offset)
def is_drag_active(self):
""" Are we actually moving/resizing """
return self.is_drag_initiated() and self._drag_active
def is_moving(self):
return self.is_drag_initiated() and self._drag_handle == Handle.MOVE
def was_moving(self):
return self._last_drag_handle == Handle.MOVE
def is_resizing(self):
return self.is_drag_initiated() and self._drag_handle != Handle.MOVE
def move_into_view(self):
"""
If the window has somehow ended up off-screen,
move the always-visible-rect back into view.
"""
window = self.get_drag_window()
if window: # don't crash on exit
x, y = window.get_position()
_x, _y = self.limit_position(x, y)
if _x != x or _y != y:
self._move_resize(_x, _y)
def force_into_view(self):
self.move_into_view()
if False: # Only for system drag, not needed when using fallback mode
GLib.idle_add(self._do_force_into_view)
def _do_force_into_view(self):
""" Works mostly, but occasionally the window disappears... """
window = self.get_drag_window()
x, y = window.get_position()
_x, _y = self.limit_position(x, y)
if _x != x or _y != y:
window.hide()
self._move_resize(_x, _y)
window.show()
def limit_size(self, rect):
"""
Limit the given window rect to fit on screen.
"""
screen = self.get_screen()
limits = Rect(0, 0, screen.get_width(), screen.get_height())
r = rect.copy()
if not limits.is_empty(): # LP #1633284
if r.w > limits.w:
r.w = limits.w - 40
if r.h > limits.h:
r.h = limits.h - 20
return r
def limit_position(self, x, y, visible_rect = None, limit_rects = None):
"""
Limits the given window position to keep the current
always_visible_rect fully in view.
"""
# rect to stay always visible, in canvas coordinates
if visible_rect is None:
visible_rect = self.get_always_visible_rect()
if not limit_rects:
if not self._monitor_rects:
self._monitor_rects = get_monitor_rects(self.get_screen())
limit_rects = self._monitor_rects
x, y = limit_window_position(x, y, visible_rect, limit_rects)
return x, y
def hit_test_move_resize(self, point):
canvas_rect = self.get_resize_frame_rect()
handles = self.get_drag_handles()
hit_frame_width = self.get_hit_frame_width()
w = min(canvas_rect.w / 2, hit_frame_width)
h = min(canvas_rect.h / 2, hit_frame_width)
x, y = point
x0, y0, x1, y1 = canvas_rect.to_extents()
# try corners first
for handle in handles:
if handle == Handle.NORTH_WEST:
if x >= x0 and x < x0 + w and \
y >= y0 and y < y0 + h:
return handle
if handle == Handle.NORTH_EAST:
if x <= x1 and x > x1 - w and \
y >= y0 and y < y0 + h:
return handle
if handle == Handle.SOUTH_EAST:
if x <= x1 and x > x1 - w and \
y <= y1 and y > y1 - h:
return handle
if handle == Handle.SOUTH_WEST:
if x >= x0 and x < x0 + w and \
y <= y1 and y > y1 - h:
return handle
# then check the edges
for handle in handles:
if handle == Handle.WEST:
if x < x0 + w and x >= x0 - 1:
return handle
if handle == Handle.EAST:
if x > x1 - w and x <= x1 + 1:
return handle
if handle == Handle.NORTH:
if y < y0 + h:
return handle
if handle == Handle.SOUTH:
if y > y1 - h:
return handle
return None
def _move_resize(self, x, y, w = None, h = None):
#print("_move_resize", x, y, w, h)
window = self.get_drag_window()
gdk_win = window.get_window()
if w is None:
# Stop inserting edge move for now. In unity, when
# jamming onboard into the lower left corner the keyboard
# window disappears (Precise).
#self._insert_edge_move(window, x, y)
window.move(x, y)
#print("_move_resize: move ", x, y, " position ", window.get_position(), " origin ", _win.get_origin(), " root origin ", _win.get_root_origin())
else:
if hasattr(window, "move_resize"):
window.move_resize(x, y, w, h) # keyboard window
else:
gdk_win.move_resize(x, y, w, h) # icon palette
def _insert_edge_move(self, window, x, y):
"""
Compiz and potentially other window managers silently ignore
moves outside of some screen edges. When hitting the edge at
high speed, onboard gets stuck some distance away from it.
Fix this by inserting an intermediate move right to the edge.
Does not help with the edge below unity bar.
"""
limits = self.get_screen_limits()
one_more_x = x
one_more_y = y
pos = window.get_position()
size = window.get_size()
if pos[0] > limits.left() and \
x < limits.left():
one_more_x = limits.left()
if pos[0] + size[0] < limits.right() and \
x + size[0] > limits.right():
one_more_x = limits.right()
if pos[1] > limits.top() and \
y < limits.top():
one_more_y = limits.top()
if pos[1] + size[1] < limits.bottom() and \
y + size[1] > limits.bottom():
one_more_x = limits.right()
if one_more_x != x or one_more_y != y:
window.move(one_more_x, one_more_y)
class Orientation:
""" enum for screen orientation """
class LANDSCAPE: pass
class PORTRAIT: pass
class WindowRectTracker:
"""
Keeps track of the window rectangle when moving/resizing.
Gtk only updates the position and size asynchrounously on
configure events and hidden windows return invalid values.
Auto-show et al need valid values from get_position and
get_size at all times.
"""
def __init__(self):
self._window_rect = None
self._origin = None
self._client_offset = (0, 0)
self._override_redirect = False
def cleanup(self):
pass
def update_window_rect(self):
"""
Call this on configure event, the only time when
get_position, get_size, etc. can be trusted.
"""
visible = self.is_visible()
if visible:
pos = Gtk.Window.get_position(self)
size = Gtk.Window.get_size(self)
origin = self.get_window().get_origin()
if len(origin) == 3: # What is the first parameter for? Gdk bug?
origin = origin[1:]
if _logger.isEnabledFor(logging.DEBUG):
_logger.debug("update_window_rect1: pos {}, size {}, origin {}"
.format(pos, size, origin))
pos = self._apply_window_scaling_factor(pos)
self._window_rect = Rect.from_position_size(pos, size)
self._origin = origin
self._client_offset = (origin[0] - pos[0], origin[1] - pos[1])
self._screen_orientation = self.get_screen_orientation()
if _logger.isEnabledFor(logging.DEBUG):
_logger.debug("update_window_rect2: pos {}, client_offset {}, "
"screen_orientation {}"
.format(pos,
self._client_offset,
self._screen_orientation))
def move(self, x, y):
Gtk.Window.move(self, x, y)
def resize(self, w, h):
Gtk.Window.resize(self, w, h)
def move_resize(self, x, y, w, h):
win = self.get_window()
if win:
win.move_resize(x, y, w, h)
def get_position(self):
if self._window_rect is None:
pos = Gtk.Window.get_position(self)
pos = self._apply_window_scaling_factor(pos)
else:
pos = self._window_rect.get_position()
return pos
def get_size(self):
if self._window_rect is None:
return Gtk.Window.get_size(self)
else:
return self._window_rect.get_size()
def get_origin(self):
if self._origin is None:
win = self.get_window()
if win:
origin = win.get_origin()
if len(origin) == 3: # What is the first parameter for? Gdk bug?
origin = origin[1:]
return origin
return 0
else:
return self._origin
def get_client_offset(self):
return self._client_offset
def get_rect(self):
return self._window_rect
def get_override_redirect(self):
return self._override_redirect
def set_override_redirect(self, value):
self._override_redirect = value
self.get_window().set_override_redirect(True)
def get_scale_factor(self):
gdk_win = self.get_window()
if gdk_win:
try:
return gdk_win.get_scale_factor()
except AttributeError: # from Gdk 3.10
pass
return None
def _apply_window_scaling_factor(self, values):
"""
Gdk doesn't scale position of override redirect windows (Trusty)
"""
if self._override_redirect:
scale = self.get_scale_factor()
if not scale is None:
scale = 1.0 / scale
values = (values[0] * scale, values[1] * scale)
return values
def get_screen_orientation(self):
"""
Current orientation of the screen (tablet rotation).
Only the aspect ratio is taken into account at this time.
This appears to cover more cases than looking at monitor rotation,
in particular with multi-monitor screens.
"""
screen = self.get_screen()
if screen.get_width() >= screen.get_height():
return Orientation.LANDSCAPE
else:
return Orientation.PORTRAIT
class WindowRectPersist(WindowRectTracker):
"""
Save and restore window position and size.
"""
def __init__(self):
WindowRectTracker.__init__(self)
self._screen_orientation = None
self._save_position_timer = Timer()
# init detection of screen "rotation"
screen = self.get_screen()
screen.connect('size-changed', self.on_screen_size_changed)
def cleanup(self):
self._save_position_timer.finish()
def is_visible(self):
""" This is overloaded in KbdWindow """
return Gtk.Window.get_visible(self)
def on_screen_size_changed(self, screen):
""" detect screen rotation (tablets)"""
# Give the screen time to settle, the window manager
# may block the move to previously invalid positions and
# when docked, the slide animation may be drowned out by all
# the action in other processes.
Timer(1.5, self.on_screen_size_changed_delayed, screen)
def on_screen_size_changed_delayed(self, screen):
self.restore_window_rect()
def restore_window_rect(self, startup = False):
"""
Restore window size and position.
"""
# Run pending save operations now, so they don't
# interfere with the window rect after it was restored.
self._save_position_timer.finish()
orientation = self.get_screen_orientation()
rect = self.read_window_rect(orientation)
self._screen_orientation = orientation
self._window_rect = rect
_logger.debug("restore_window_rect {rect}, {orientation}" \
.format(rect = rect, orientation = orientation))
# Give the derived class a chance to modify the rect,
# for example to correct the position for auto-show.
rect = self.on_restore_window_rect(rect)
self._window_rect = rect
# move/resize the window
if startup:
# gnome-shell doesn't take kindly to an initial move_resize().
# The window ends up at (0, 0) on and goes back there
# repeatedly when hiding and unhiding.
self.set_default_size(rect.w, rect.h)
self.move(rect.x, rect.y)
else:
self.move_resize(rect.x, rect.y, rect.w, rect.h)
# Initialize shadow variables with valid values so they
# don't get taken from the unreliable window.
# Fixes bad positioning of the very first auto-show.
if startup:
self._window_rect = rect.copy()
# Ignore frame dimensions; still better than asking the window.
self._origin = rect.left_top()
self._screen_orientation = self.get_screen_orientation()
def on_restore_window_rect(self, rect):
return rect
def save_window_rect(self, orientation=None, rect=None):
"""
Save window size and position.
"""
if orientation is None:
orientation = self._screen_orientation
if rect is None:
rect = self._window_rect
# Give the derived class a chance to modify the rect,
# for example to override it for auto-show.
rect = self.on_save_window_rect(rect)
self.write_window_rect(orientation, rect)
_logger.debug("save_window_rect {rect}, {orientation}" \
.format(rect=rect, orientation=orientation))
def on_save_window_rect(self, rect):
return rect
def read_window_rect(self, orientation, rect):
"""
Read orientation dependent rect.
Overload this in derived classes.
"""
raise NotImplementedError()
def write_window_rect(self, orientation, rect):
"""
Write orientation dependent rect.
Overload this in derived classes.
"""
raise NotImplementedError()
def start_save_position_timer(self):
"""
Trigger saving position and size to gsettings
Delay this a few seconds to avoid excessive disk writes.
Remember the current rect and rotation as the screen may have been
rotated when the saving happens.
"""
self._save_position_timer.start(5,
self.save_window_rect,
self.get_screen_orientation(),
self.get_rect())
def stop_save_position_timer(self):
self._save_position_timer.stop()
def set_unity_property(window):
"""
Set custom X window property to tell unity 3D this is an on-screen
keyboard that wants to be raised on top of dash. See LP 739812, 915250.
Since onboard started detecting dash itself this isn't really needed
for unity anymore. Leave it anyway, it may come in handy in the future.
"""
gdk_win = window.get_window()
if gdk_win:
if hasattr(gdk_win, "get_xid"): # not on wayland
xid = gdk_win.get_xid()
osk.Util().set_x_property(xid, "ONSCREEN_KEYBOARD", 1)
class DwellProgress(object):
# dwell time in seconds
dwell_delay = 4
# time of dwell start
dwell_start_time = None
opacity = 1.0
def is_dwelling(self):
return self.dwell_start_time is not None
def is_done(self):
return time.time() > self.dwell_start_time + self.dwell_delay
def start_dwelling(self):
self.dwell_start_time = time.time()
def stop_dwelling(self):
self.dwell_start_time = None
def draw(self, context, rect, rgba=(1, 0, 0, .75), rgba_bg = None):
if self.is_dwelling():
if self.opacity <= 0.0:
pass
if self.opacity >= 1.0:
self._draw_dwell_progress(context, rect, rgba, rgba_bg)
else:
context.save()
context.rectangle(*rect.int())
context.clip()
context.push_group()
self._draw_dwell_progress(context, rect, rgba, rgba_bg)
context.pop_group_to_source()
context.paint_with_alpha(self.opacity)
context.restore()
def _draw_dwell_progress(self, context, rect, rgba, rgba_bg):
xc, yc = rect.get_center()
radius = min(rect.w, rect.h) / 2.0
alpha0 = -pi / 2.0
k = (time.time() - self.dwell_start_time) / self.dwell_delay
k = min(k, 1.0)
alpha = k * pi * 2.0
if rgba_bg:
context.set_source_rgba(*rgba_bg)
context.move_to(xc, yc)
context.arc(xc, yc, radius, 0, 2 * pi)
context.close_path()
context.fill()
context.move_to(xc, yc)
context.arc(xc, yc, radius, alpha0, alpha0 + alpha)
context.close_path()
context.set_source_rgba(*rgba)
context.fill_preserve()
context.set_source_rgba(0, 0, 0, 1)
context.set_line_width(0)
context.stroke()
def limit_window_position(x, y, always_visible_rect, limit_rects = None):
"""
Limits the given window position to keep the
always_visible_rect fully in view.
"""
# rect to stay always visible, in canvas coordinates
r = always_visible_rect
if r is not None:
r = r.int() # avoid rounding errors
# transform always visible rect to screen coordinates,
# take window decoration into account.
rs = r.copy()
rs.x += x
rs.y += y
dmin = None
rsmin = None
for limits in limit_rects:
# get limited candidate rect
rsc = rs.copy()
rsc.x = max(rsc.x, limits.left())
rsc.x = min(rsc.x, limits.right() - rsc.w)
rsc.y = max(rsc.y, limits.top())
rsc.y = min(rsc.y, limits.bottom() - rsc.h)
# closest candidate rect wins
cx, cy = rsc.get_center()
dx, dy = rs.x - rsc.x, rs.y - rsc.y
d = dx * dx + dy * dy
if dmin is None or d < dmin:
dmin = d
rsmin = rsc
x = rsmin.x - r.x
y = rsmin.y - r.y
return x, y
def get_monitor_rects(screen):
"""
Screen limits, one rect per monitor. Monitors may have
different sizes and arbitrary relative positions.
"""
rects = []
if screen:
for i in range(screen.get_n_monitors()):
r = screen.get_monitor_geometry(i)
rects.append(Rect(r.x, r.y, r.width, r.height))
else:
rootwin = Gdk.get_default_root_window()
r = Rect.from_position_size(rootwin.get_position(),
(rootwin.get_width(), rootwin.get_height()))
rects.append(r)
return rects
def canvas_to_root_window_rect(window, rect):
"""
Convert rect in canvas coordinates to root window coordinates.
"""
gdk_win = window.get_window()
if gdk_win:
x0, y0 = gdk_win.get_root_coords(rect.x, rect.y)
x1, y1 = gdk_win.get_root_coords(rect.x + rect.w,
rect.y + rect.h)
rect = Rect.from_extents(x0, y0, x1, y1)
else:
rect = Rect()
return rect
def canvas_to_root_window_point(window, point):
"""
Convert point in canvas coordinates to root window coordinates.
"""
gdk_win = window.get_window()
if gdk_win:
point = gdk_win.get_root_coords(*point)
else:
point(0, 0)
return point
def get_monitor_dimensions(window):
""" Geometry and physical size of the monitor at window. """
gdk_win = window.get_window()
screen = window.get_screen()
if gdk_win and screen:
monitor = screen.get_monitor_at_window(gdk_win)
r = screen.get_monitor_geometry(monitor)
size = (r.width, r.height)
size_mm = (screen.get_monitor_width_mm(monitor),
screen.get_monitor_height_mm(monitor))
# Nexus7 simulation
device = None # keep this at None
# device = 1
if device == 0: # dimension unavailable
size_mm = 0, 0
elif device == 1: # Nexus 7, as it should report
size = 1280, 800
size_mm = 150, 94
return size, size_mm
else:
return (0, 0), (0, 0)
def physical_to_monitor_pixel_size(window, size_mm, fallback_size = (0, 0)):
"""
Convert a physical size in mm to pixels of windows's monitor,
"""
sz, sz_mm = get_monitor_dimensions(window)
if sz[0] > 0 and sz[1] > 0 and \
sz_mm[0] > 0 and sz_mm[1] > 0:
w = sz[0] * size_mm[0] / sz_mm[0] \
if sz_mm[0] else fallback_size[0]
h = sz[1] * size_mm[1] / sz_mm[1] \
if sz_mm[0] else fallback_size[1]
else:
w, h = fallback_size
return w, h
def show_error_dialog(error_string):
""" Show an error dialog """
error_dlg = Gtk.MessageDialog(message_type=Gtk.MessageType.ERROR,
message_format=error_string,
buttons=Gtk.ButtonsType.OK)
error_dlg.run()
error_dlg.destroy()
def show_ask_string_dialog(question, parent=None):
question_dialog = Gtk.MessageDialog(message_type=Gtk.MessageType.QUESTION,
buttons=Gtk.ButtonsType.OK_CANCEL)
if parent:
question_dialog.set_transient_for(parent)
question_dialog.set_markup(question)
entry = Gtk.Entry()
entry.connect("activate", lambda event:
question_dialog.response(Gtk.ResponseType.OK))
question_dialog.get_message_area().add(entry)
question_dialog.show_all()
response = question_dialog.run()
text = entry.get_text() if response == Gtk.ResponseType.OK else None
question_dialog.destroy()
return text
def show_confirmation_dialog(question, parent=None, center=False, title=None):
"""
Show this dialog to ask confirmation before executing a task.
"""
if title is None:
# Default dialog title: name of the application
title = _("Onboard")
dlg = Gtk.MessageDialog(message_type=Gtk.MessageType.QUESTION,
text=question,
title=title,
buttons=Gtk.ButtonsType.YES_NO)
if parent:
dlg.set_transient_for(parent)
if center:
dlg.set_position(Gtk.WindowPosition.CENTER)
response = dlg.run()
dlg.destroy()
return response == Gtk.ResponseType.YES
def show_new_device_dialog(name, config_string, is_pointer, callback):
"""
Show a "New Input Device" dialog.
"""
dialog = Gtk.MessageDialog(message_type=Gtk.MessageType.OTHER,
title=_("New Input Device"),
text=_("Onboard has detected a new input device"))
if is_pointer:
dialog.set_image(Gtk.Image(icon_name="input-mouse",
icon_size=Gtk.IconSize.DIALOG))
else:
dialog.set_image(Gtk.Image(icon_name="input-keyboard",
icon_size=Gtk.IconSize.DIALOG))
secondary = "{}\n\n".format(name)
secondary += _("Do you want to use this device for keyboard scanning?")
dialog.format_secondary_markup(secondary)
# Translators: cancel button of "New Input Device" 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(_("Use device"), Gtk.ResponseType.ACCEPT).grab_default()
dialog.connect("response", _show_new_device_dialog_response,
callback, config_string)
dialog.show_all()
def _show_new_device_dialog_response(dialog, response, callback, config_string):
""" Callback for the "New Input Device" dialog. """
if response == Gtk.ResponseType.ACCEPT:
callback(config_string)
dialog.destroy()
def gtk_has_resize_grip_support():
""" Gtk from 3.14 removes resize grips. """
gtk_version = Version(Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION)
return gtk_version < Version(3, 14)
onboard-1.4.1/Onboard/__init__.py 0000644 0001750 0001750 00000001636 13051012134 017062 0 ustar frafu frafu 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright © 2011-2012 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 .
from __future__ import division, print_function, unicode_literals
# install translation function _() for all modules
from Onboard.utils import Translation
Translation.install("onboard")
onboard-1.4.1/Onboard/HardwareSensorTracker.py 0000644 0001750 0001750 00000023073 13051012134 021565 0 ustar frafu frafu 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright © 2016-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 .
import os
import socket
import select
import threading
from Onboard.utils import EventSource
from Onboard.GlobalKeyListener import GlobalKeyListener
import logging
_logger = logging.getLogger("HardwareSensorTracker")
from Onboard.Config import Config
config = Config()
class HardwareSensorTracker(EventSource):
""" Singleton class that keeps track of hardware sensors. """
_tablet_mode_event_names = ("tablet-mode-changed",)
_event_names = (("power-button-pressed",) +
_tablet_mode_event_names)
# Filenames and search patterns to determine convertible tablet-mode.
# Only some of the drivers that send SW_TABLET_MODE evdev events
# also provide sysfs attributes to read the current tablet-mode state.
_tablet_mode_state_files = (
# classmate-laptop.c
# nothing
# fujitsu-tablet.c
# nothing
# hp-wmi.c
("/sys/devices/platform/hp-wmi/tablet",
"1"),
# ideapad-laptop.c, only debugfs which requires root
# ("/sys/kernel/debug/ideapad/status",
# re.compile("Touchpad status:Off(0)")),
# thinkpad_acpi.c
("/sys/devices/platform/thinkpad_acpi/hotkey_tablet_mode",
"1"),
# xo15-ebook.c
# nothing
)
def __new__(cls, *args, **kwargs):
"""
Singleton magic.
"""
if not hasattr(cls, "self"):
cls.self = object.__new__(cls, *args, **kwargs)
cls.self.construct()
return cls.self
def __init__(self):
"""
Called multiple times, don't use this.
"""
pass
def construct(self):
"""
Singleton constructor, runs only once.
"""
EventSource.__init__(self, self._event_names)
self._acpid_listener = None
self._tablet_mode = None
self._key_listener = None
def cleanup(self):
EventSource.cleanup(self)
self._register_listeners(False)
def connect(self, event_name, callback):
EventSource.connect(self, event_name, callback)
self.update_sensor_sources()
def disconnect(self, event_name, callback):
had_listeners = self.has_listeners(self._event_names)
EventSource.disconnect(self, event_name, callback)
self.update_sensor_sources()
# help debugging disconnecting events on exit
if had_listeners and not self.has_listeners(self._event_names):
_logger.info("all listeners disconnected")
def update_sensor_sources(self):
register = self.has_listeners()
self._register_acpid_listeners(register)
register = self.has_listeners(self._tablet_mode_event_names)
self._register_hotkey_listeners(register)
def _register_listeners(self, register):
self._register_acpid_listeners(register)
self._register_hotkey_listeners(register)
def _register_acpid_listeners(self, register):
if bool(self._acpid_listener) != register:
if register:
self._acpid_listener = AcpidListener(self)
else:
self._acpid_listener.stop()
self._acpid_listener = None
def _register_hotkey_listeners(self, register):
enter_key = config.auto_show.tablet_mode_enter_key
leave_key = config.auto_show.tablet_mode_leave_key
if not enter_key and not leave_key:
register = False
if register:
if not self._key_listener:
self._key_listener = GlobalKeyListener()
self._key_listener.connect("key-press", self._on_key_press)
else:
if self._key_listener:
self._key_listener.disconnect("key-press", self._on_key_press)
self._key_listener = None
def set_tablet_mode(self, activ):
self._tablet_mode = activ
self.emit_async("tablet-mode-changed", activ)
def get_tablet_mode(self):
"""
Return value:
True = convertible is in tablet-mode
False = convertible is not in tablet-mode
None = mode unknown
"""
state = self._get_tablet_mode_state()
if state is None:
return self._tablet_mode
return state
def _get_tablet_mode_state(self):
"""
Read the state from known system files, if available.
Else return None.
"sysfs" files are read from kernel memory, shouldn't be
too expensive to do repeatedly.
"""
custom_state_file = config.auto_show.tablet_mode_state_file
custom_pattern = config.auto_show.tablet_mode_state_file_pattern
if custom_state_file:
candidates = ((custom_state_file, custom_pattern),)
else:
candidates = self._tablet_mode_state_files
for fn, pattern in candidates:
try:
with open(fn, "r", encoding="UTF-8") as f:
content = f.read(4096)
except IOError as ex:
_logger.debug("Opening '{}' failed: {}".format(fn, ex))
content = ""
if content:
if isinstance(pattern, str):
active = bool(pattern) and pattern in content
else:
active = bool(pattern.search(content))
_logger.info("read tablet_mode={} from '{}' with pattern '{}'"
.format(active, fn, pattern))
return active
return None
def _on_key_press(self, event):
""" Global hotkey press received """
enter_keycode = config.auto_show.tablet_mode_enter_key
leave_keycode = config.auto_show.tablet_mode_leave_key
if _logger.isEnabledFor(logging.INFO):
s = self._key_listener.get_key_event_string(event)
s += ", enter_keycode={}, leave_keycode={}".format(enter_keycode,
leave_keycode)
_logger.info("_on_key_press(): {}".format(s))
if enter_keycode and event.keycode == enter_keycode:
_logger.info("hotkey tablet_mode_enter_key {} received"
.format(enter_keycode))
self.set_tablet_mode(True)
if leave_keycode and event.keycode == leave_keycode:
_logger.info("hotkey tablet_mode_leave_key {} received"
.format(leave_keycode))
self.set_tablet_mode(False)
class AcpidListener:
""" Listen to events aggregated by acpid. """
def __init__(self, sensor_tracker):
super(AcpidListener, self).__init__()
self._sensor_tracker = sensor_tracker
self._exit_r = self._exit_w = None
self.start()
def start(self):
self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
fn = "/var/run/acpid.socket"
try:
self._socket.connect(fn)
except Exception as ex:
_logger.warning("Failed to connect to acpid, "
"SW_TABLET_MODE detection disabled. "
"('{}': {}) "
.format(fn, str(ex)))
return
self._socket.setblocking(False)
self._exit_r, self._exit_w = os.pipe()
self._thread = threading.Thread(name=self.__class__.__name__,
target=self._run)
self._thread.start()
def stop(self):
if self._exit_w:
os.write(self._exit_w, "x".encode())
self._thread.join(2)
_logger.info("AcpidListener: thread stopped, is_alive={}"
.format(self._thread.is_alive()))
def _run(self):
_logger.info("AcpidListener: thread start")
while True:
rl, wl, xl = select.select([self._exit_r, self._socket],
[], [self._socket])
if self._socket in rl:
data = self._socket.recv(4096)
elif self._exit_r in rl:
break
for event in data.decode("UTF-8").splitlines():
_logger.info("AcpidListener: ACPI event: '{}'"
.format(event))
if event == "button/power PBTN 00000080 00000000":
_logger.info("AcpidListener: power button")
self._sensor_tracker.emit_async("power-button-pressed")
elif event == "video/tabletmode TBLT 0000008A 00000001":
_logger.info("AcpidListener: tablet_mode True")
self._sensor_tracker.set_tablet_mode(True)
elif event == "video/tabletmode TBLT 0000008A 00000000":
_logger.info("AcpidListener: tablet_mode False")
self._sensor_tracker.set_tablet_mode(False)
self._socket.close()
self._socket.close()
os.close(self._exit_r)
os.close(self._exit_w)
_logger.info("AcpidListener: thread exit")
onboard-1.4.1/Onboard/Version.py 0000644 0001750 0001750 00000002303 13051012134 016740 0 ustar frafu frafu 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright © 2016 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 .
import gi
def require_gi_versions():
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
gi.require_version('GdkX11', '3.0')
gi.require_version('Pango', '1.0')
gi.require_version('PangoCairo', '1.0')
# Atspi is not required
try:
gi.require_version('Atspi', '2.0')
except ValueError:
pass
# AppIndicator3 is not required
try:
gi.require_version('AppIndicator3', '0.1')
except ValueError:
pass
onboard-1.4.1/Onboard/Exceptions.py 0000644 0001750 0001750 00000006042 13051012134 017440 0 ustar frafu frafu 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright © 2008-2010 Chris Jones
# Copyright © 2010 Francesco Fumanti
# Copyright © 2011-2014 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 .
from __future__ import division, print_function, unicode_literals
import sys
from Onboard.utils import unicode_str
class ChainableError(Exception):
"""
Base class for Onboard errors
We want Python to print the stacktrace of the first exception in the chain
so we store the last stacktrace if the previous exception in the chain
has not.
"""
_last_exception = None
def __init__(self, message, chained_exception = None):
self._message = message
self.chained_exception = chained_exception
if chained_exception:
if not (isinstance(chained_exception, ChainableError) \
and chained_exception.traceback):
# Store last traceback
self._last_exception = sys.exc_info()
def _get_traceback(self):
if self._last_exception:
return self._last_exception[2]
elif self.chained_exception \
and isinstance(self.chained_exception, ChainableError):
return self.chained_exception.traceback
else:
return None
traceback = property(_get_traceback)
def __str__(self):
message = unicode_str(self._message)
if self.chained_exception:
message += ", " + unicode_str(self.chained_exception)
return message
class SVGSyntaxError(ChainableError):
"""Error raised when Onboard can't comprehend SVG layout file."""
pass
class LayoutFileError(ChainableError):
"""Error raised when Onboard can't comprehend layout definition file."""
pass
class ThemeFileError(ChainableError):
"""Error raised when Onboard can't comprehend theme definition file."""
pass
class ColorSchemeFileError(ChainableError):
"""Error raised when Onboard can't comprehend color
scheme definition file."""
pass
class SchemaError(ChainableError):
"""Error raised when a gesettings schema does not exist """
pass
def chain_handler(type, value, traceback):
"""
Wrap the default handler so that we can get the traceback from chained
exceptions.
"""
if isinstance(value, ChainableError) and value.traceback:
traceback = value.traceback
sys.__excepthook__(type, value, traceback)
onboard-1.4.1/Onboard/AtspiStateTracker.py 0000644 0001750 0001750 00000111412 13051012134 020712 0 ustar frafu frafu 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright © 2012-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 specific keyboard class """
from __future__ import division, print_function, unicode_literals
import time
import logging
_logger = logging.getLogger(__name__)
from Onboard.Version import require_gi_versions
require_gi_versions()
try:
from gi.repository import Atspi
except ImportError as e:
_logger.warning("Atspi typelib missing, auto-show unavailable")
from Onboard.utils import Rect, EventSource, Process, unicode_str
from Onboard.Timer import Timer
# Config Singleton
from Onboard.Config import Config
config = Config()
class CachedAccessible:
def __init__(self, accessible):
self._accessible = accessible
self._state = {} # cache of various accessible properties
# Use "==" for object identity tests instead of "is".
def __eq__(self, other):
return other is not None and self._accessible is other._accessible
def __ne__(self, other):
return other is None or self._accessible is not other._accessible
def get_state(self):
""" All cached state of the accessible """
return self._state
def get_all_state(self):
"""
Return _state filled with all kinds of properties, for easy printint
as debug output in TextContext.
"""
self.get_role()
self.get_role_name()
self.get_name()
self.get_state_set()
self.get_id()
self.get_attributes()
self.get_interfaces()
self.get_description()
self.get_pid()
self.get_process_name()
self.get_toolkit_name()
self.get_toolkit_version()
self.get_editable_text_iface()
self.get_editable_text_iface()
self.get_app_name()
self.get_app_description()
self.get_extents()
self.get_frame()
self.get_frame_extents()
self.is_urlbar()
self.is_byobu()
return self._state
# ### Cached, exception-safe accessor functions ###
def get_role(self):
return self._get_value("role",
self._accessible.get_role)
def get_role_name(self):
return self._get_value("role-name",
self._accessible.get_role_name)
def get_name(self):
return self._get_value("name",
self._accessible.get_name)
def invalidate_state_set(self):
self.invalidate("state-set")
def get_state_set(self):
return self._get_value("state-set",
self._accessible.get_state_set)
def get_id(self):
return self._get_value("id",
self._accessible.get_id)
def get_attributes(self):
return self._get_value("attributes",
self._accessible.get_attributes, {})
def get_interfaces(self):
return self._get_value("interfaces",
self._accessible.get_interfaces, [])
def get_description(self):
return self._get_value("description",
self._accessible.get_description)
def get_pid(self):
return self._get_value("pid",
self._accessible.get_process_id)
def get_process_name(self):
pid = self.get_pid()
if pid != -1:
return self._get_value_noex(
"process-name",
lambda : Process.get_process_name(pid))
return None
def get_toolkit_name(self):
return self._get_value("toolkit-name",
self._accessible.get_toolkit_name)
def get_toolkit_version(self):
return self._get_value("toolkit-version",
self._accessible.get_toolkit_version)
def get_editable_text_iface(self):
return self._get_value("editable-text-iface",
self._accessible.get_editable_text_iface)
def get_app_name(self):
def func():
app = self._accessible.get_application()
return app.get_name()
return self._get_value("app-name", func, "")
def get_app_description(self):
def func():
app = self._accessible.get_application()
return app.get_description()
return self._get_value("app-description", func, "")
def invalidate_extents(self):
self.invalidate("extents")
def get_extents(self):
"""
Screen rect after scaling.
"""
scale = config.window_scaling_factor
if scale != 1.0:
# Only Gtk-3 widgets return scaled coordinates, all others,
# including Gtk-2 apps like firefox, clawsmail and Qt-apps,
# apparently don't.
if self.is_toolkit_gtk3():
scale = 1.0
else:
scale = 1.0 / config.window_scaling_factor
def func():
ext = self._accessible.get_extents(Atspi.CoordType.SCREEN)
return Rect(ext.x * scale, ext.y * scale,
ext.width * scale, ext.height * scale)
return self._get_value("extents", func, Rect())
def get_frame(self):
def func():
frame = self._get_accessible_frame(self._accessible)
if frame:
return CachedAccessible(frame)
return None
return self._get_value_noex("frame", func)
def get_frame_extents(self):
def func():
frame = self.get_frame()
if frame:
return frame.get_extents()
return Rect()
return self._get_value_noex("frame_extents", func)
@staticmethod
def _get_accessible_frame(accessible):
""" Accessible of the top level window to which accessible belongs. """
frame = None
_logger.atspi("_get_accessible_frame(): searching for top level:")
try:
parent = accessible
while True:
parent = parent.get_parent()
if not parent:
break
role = parent.get_role()
_logger.atspi("parent: {}".format(role))
if role == Atspi.Role.FRAME or \
role == Atspi.Role.DIALOG or \
role == Atspi.Role.WINDOW or \
role == Atspi.Role.NOTIFICATION:
frame = parent
break
# private exception gi._glib.GError when
# right clicking onboards unity2d launcher (Precise)
except Exception as ex:
_logger.atspi("Invalid accessible,"
" failed to get top level accessible: " +
unicode_str(ex))
return frame
def is_urlbar(self):
""" Is this a (most likely firefox') URL bar? """
def func():
attributes = self.get_attributes()
return bool(attributes and "urlbar" in attributes.get("class", ""))
return self._get_value_noex("is_urlbar", func)
def is_byobu(self):
""" Is this possibly byobu running in a terminal? """
def func():
description = self.get_description()
return bool(description and "byobu" in description.lower())
return self._get_value_noex("is_byobu", func)
def _get_value(self, name, func, default=None):
""" Return cached return value of func(). """
value = self._state.get(name)
if value is None:
try:
value = func()
except Exception as ex: # private exception gi._glib.GError
_logger.info("CachedAccessible._get_value({}): "
"invalid accessible, failed to read state: "
.format(name) + unicode_str(ex))
value = default
self._state[name] = value
return value
def _get_value_noex(self, name, func):
""" Return cached return value of func(). """
value = self._state.get(name)
if value is None:
value = func()
self._state[name] = value
return value
def invalidate(self, name):
"""
Force re-reading property from the accessible.
May cause a D-Bus round-trip on the next read-attempt.
"""
try:
del self._state[name]
except KeyError:
pass
# ### uncached, but still exception safe functions ###
def get_selection(self, selection_num=0):
selection = None
try:
sel = self._accessible.get_selection(selection_num)
# Gtk-2 applications return 0,0 when there is no selection.
# Gtk-3 applications return caret positions in that case.
# LibreOffice Writer in Vivid initially returns -1,-1 when there
# is no selection, later the caret position.
start = sel.start_offset
end = sel.end_offset
if start > 0 and \
end > 0 and \
start <= end:
selection = (sel.start_offset, sel.end_offset)
except Exception as ex: # Private exception gi._glib.GErro
_logger.info("CachedAccessible.get_selection(): " +
unicode_str(ex))
return selection
def set_caret_offset(self, offset):
try:
self._accessible.set_caret_offset(offset)
except Exception as ex: # Private exception gi._glib.GErro
_logger.info("CachedAccessible.set_caret_offset(): " +
unicode_str(ex))
def insert_text(self, position, text):
try:
return self._accessible.insert_text(position, text, -1)
except Exception as ex: # Private exception gi._glib.GErro
_logger.info("CachedAccessible.insert_text(): " +
unicode_str(ex))
return False
def delete_text(self, start_pos, end_pos):
try:
return self._accessible.delete_text(start_pos, end_pos)
except Exception as ex: # Private exception gi._glib.GErro
_logger.info("CachedAccessible.delete_text(): " +
unicode_str(ex))
return False
# ### uncached, raising exceptions ###
def get_caret_offset(self):
try:
offset = self._accessible.get_caret_offset()
except Exception as ex: # Private exception gi._glib.GErro
_logger.info("CachedAccessible.get_caret_offset(): " +
unicode_str(ex))
raise ex
return offset
def get_character_count(self):
try:
count = self._accessible.get_character_count()
except Exception as ex: # Private exception gi._glib.GErro
_logger.info("CachedAccessible.get_character_count(): " +
unicode_str(ex))
raise ex
return count
def get_text_at_offset(self, offset, boundary_type):
try:
text = self._accessible.get_text_at_offset(offset, boundary_type)
except Exception as ex: # Private exception gi._glib.GErro
_logger.info("CachedAccessible.get_text_at_offset(): " +
unicode_str(ex))
raise ex
return text
def get_text_before_offset(self, offset, boundary_type):
try:
text = self._accessible.get_text_before_offset(offset,
boundary_type)
except Exception as ex: # Private exception gi._glib.GErro
_logger.info("CachedAccessible.get_text_before_offset(): " +
unicode_str(ex))
raise ex
return text
def get_text(self, begin, end):
""" Text of the given accessible, no caching """
try:
text = Atspi.Text.get_text(self._accessible, begin, end)
# private exception gi._glib.GError: timeout from dbind
# with web search in firefox.
except Exception as ex:
_logger.atspi("CachedAccessible.get_text(): " +
unicode_str(ex))
raise ex
return text
# ### Higher level functions ###
def is_focused(self, invalidate=False):
if invalidate: # re-read properties?
self.invalidate_state_set()
state_set = self.get_state_set()
if state_set is not None:
return state_set.contains(Atspi.StateType.FOCUSED)
return False
def is_editable(self):
""" Is this an accessible onboard should be shown for? """
role = self.get_role()
state_set = self.get_state_set()
if state_set is not None:
if role in [Atspi.Role.TEXT,
Atspi.Role.TERMINAL,
Atspi.Role.DATE_EDITOR,
Atspi.Role.PASSWORD_TEXT,
Atspi.Role.EDITBAR,
Atspi.Role.ENTRY,
Atspi.Role.DOCUMENT_TEXT,
Atspi.Role.DOCUMENT_FRAME,
Atspi.Role.DOCUMENT_EMAIL,
Atspi.Role.SPIN_BUTTON,
Atspi.Role.COMBO_BOX,
Atspi.Role.DATE_EDITOR,
Atspi.Role.PARAGRAPH, # LibreOffice Writer
Atspi.Role.HEADER,
Atspi.Role.FOOTER,
]:
if role in [Atspi.Role.TERMINAL] or \
(state_set is not None and
state_set.contains(Atspi.StateType.EDITABLE)):
return True
return False
def is_not_focus_stealing(self):
"""
Is this accessible unlikely to steal the focus from
a previously focused editable accessible?
"""
role = self.get_role()
state_set = self.get_state_set()
if state_set is not None:
# Mainly firefox elements after the workaround
# for firefox 50.
if role in [Atspi.Role.DOCUMENT_FRAME,
Atspi.Role.LINK,
] \
and state_set is not None and \
not state_set.contains(Atspi.StateType.EDITABLE):
return True
return False
def is_single_line(self):
""" Is accessible a single line text entry? """
state_set = self.get_state_set()
return state_set and state_set.contains(Atspi.StateType.SINGLE_LINE)
def is_toolkit_gtk3(self):
""" Are the accessible attributes from a gtk3 widget? """
attributes = self.get_attributes()
return attributes and \
"toolkit" in attributes and attributes["toolkit"] == "gtk"
def get_character_extents(self, accessible, offset):
""" Screen rect of the character at offset """
try:
rect = self._get_character_extents(offset)
except Exception as ex: # private exception gi._glib.GError when
# right clicking onboards unity2d launcher (Precise)
_logger.atspi("Invalid accessible,"
" failed to get character extents: " +
unicode_str(ex))
rect = Rect()
return rect
def _get_character_extents(self, offset):
"""
Screen rect of the character at offset of the accessible, little
caching and exception handling.
"""
scale = config.window_scaling_factor
if scale != 1.0:
# Only Gtk-3 widgets return scaled coordinates, all others,
# including Gtk-2 apps like firefox, clawsmail and Qt-apps,
# apparently don't.
if self.is_toolkit_gtk3():
scale = 1.0
else:
scale = 1.0 / config.window_scaling_factor
ext = self._accessible.get_character_extents(offset,
Atspi.CoordType.SCREEN)
# x, y = ext.x + ext.width / 2, ext.y + ext.height / 2
# offset_control = self._accessible.get_offset_at_point(x, y,
# Atspi.CoordType.SCREEN)
# print(offset, offset_control)
return Rect(ext.x * scale, ext.y * scale,
ext.width * scale, ext.height * scale)
class AsyncEvent:
"""
Decouple AT-SPI events from D-Bus calls to reduce the risk for deadlocks.
"""
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
self._kwargs = kwargs
def __repr__(self):
return type(self).__name__ + "(" + \
", ".join(str(key) + "=" + repr(val)
for key, val in self._kwargs.items()) \
+ ")"
class AtspiStateTracker(EventSource):
"""
Keeps track of the currently active accessible by listening
to AT-SPI focus events.
"""
_focus_event_names = ("text-entry-activated",)
_text_event_names = ("text-changed", "text-caret-moved")
_key_stroke_event_names = ("key-pressed",)
_async_event_names = ("async-focus-changed",
"async-text-changed",
"async-text-caret-moved")
_event_names = (_async_event_names +
_focus_event_names +
_text_event_names +
_key_stroke_event_names)
_focus_listeners_registered = False
_keystroke_listeners_registered = False
_text_listeners_registered = False
_keystroke_listener = None
# asynchronously accessible members
_focused_accessible = None # last focused editable accessible
_focused_pid = None # pid of last focused editable accessible
_active_accessible = None # currently active editable accessible
_active_accessible_activation_time = 0.0 # time since focus received
_last_active_accessible = None
_poll_unity_timer = Timer()
def __new__(cls, *args, **kwargs):
"""
Singleton magic.
"""
if not hasattr(cls, "self"):
cls.self = object.__new__(cls, *args, **kwargs)
cls.self.construct()
return cls.self
def __init__(self):
"""
Called multiple times, don't use this.
"""
pass
def construct(self):
"""
Singleton constructor, runs only once.
"""
EventSource.__init__(self, self._event_names)
self._frozen = False
def cleanup(self):
EventSource.cleanup(self)
self._register_atspi_listeners(False)
def connect(self, event_name, callback):
EventSource.connect(self, event_name, callback)
self._update_listeners()
def disconnect(self, event_name, callback):
had_listeners = self.has_listeners(self._event_names)
EventSource.disconnect(self, event_name, callback)
self._update_listeners()
# help debugging disconnecting events on exit
if had_listeners and not self.has_listeners(self._event_names):
_logger.info("all listeners disconnected")
def _update_listeners(self):
register = self.has_listeners(self._focus_event_names)
self._register_atspi_focus_listeners(register)
register = self.has_listeners(self._text_event_names)
self._register_atspi_text_listeners(register)
register = self.has_listeners(self._key_stroke_event_names)
self._register_atspi_keystroke_listeners(register)
def _register_atspi_listeners(self, register):
self._register_atspi_focus_listeners(register)
self._register_atspi_text_listeners(register)
self._register_atspi_keystroke_listeners(register)
def _register_atspi_focus_listeners(self, register):
if "Atspi" not in globals():
return
if self._focus_listeners_registered != register:
if register:
self.atspi_connect("_listener_focus",
"focus",
self._on_atspi_global_focus)
self.atspi_connect("_listener_object_focus",
"object:state-changed:focused",
self._on_atspi_object_focus)
# private asynchronous events
for name in self._async_event_names:
handler = "_on_" + name.replace("-", "_")
EventSource.connect(self, name, getattr(self, handler))
else:
self._poll_unity_timer.stop()
self.atspi_disconnect("_listener_focus",
"focus")
self.atspi_disconnect("_listener_object_focus",
"object:state-changed:focused")
for name in self._async_event_names:
handler = "_on_" + name.replace("-", "_")
EventSource.disconnect(self, name, getattr(self, handler))
self._focus_listeners_registered = register
def _register_atspi_text_listeners(self, register):
if "Atspi" not in globals():
return
if self._text_listeners_registered != register:
if register:
self.atspi_connect("_listener_text_changed",
"object:text-changed:insert",
self._on_atspi_text_changed)
self.atspi_connect("_listener_text_changed",
"object:text-changed:delete",
self._on_atspi_text_changed)
self.atspi_connect("_listener_text_caret_moved",
"object:text-caret-moved",
self._on_atspi_text_caret_moved)
else:
self.atspi_disconnect("_listener_text_changed",
"object:text-changed:insert")
self.atspi_disconnect("_listener_text_changed",
"object:text-changed:delete")
self.atspi_disconnect("_listener_text_caret_moved",
"object:text-caret-moved")
self._text_listeners_registered = register
def _register_atspi_keystroke_listeners(self, register):
if "Atspi" not in globals():
return
if self._keystroke_listeners_registered != register:
modifier_masks = range(16)
if register:
if not self._keystroke_listener:
self._keystroke_listener = \
Atspi.DeviceListener.new(self._on_atspi_keystroke,
None)
for modifier_mask in modifier_masks:
Atspi.register_keystroke_listener(
self._keystroke_listener,
None, # key set, None=all
modifier_mask,
Atspi.KeyEventType.PRESSED,
Atspi.KeyListenerSyncType.SYNCHRONOUS)
else:
# Apparently any single deregister call will turn off
# all the other registered modifier_masks too. Since
# deregistering takes extremely long (~2.5s for 16 calls)
# seize the opportunity and just pick a single arbitrary
# mask (Quantal).
modifier_masks = [2]
for modifier_mask in modifier_masks:
Atspi.deregister_keystroke_listener(
self._keystroke_listener,
None, # key set, None=all
modifier_mask,
Atspi.KeyEventType.PRESSED)
self._keystroke_listeners_registered = register
def atspi_connect(self, attribute, event, callback):
"""
Start listening to an AT-SPI event.
Creates a new event listener for each event, since this seems
to be the only way to allow reliable deregistering of events.
"""
if hasattr(self, attribute):
listener = getattr(self, attribute)
else:
listener = None
if listener is None:
listener = Atspi.EventListener.new(callback, None)
setattr(self, attribute, listener)
listener.register(event)
def atspi_disconnect(self, attribute, event):
"""
Stop listening to AT-SPI event.
"""
listener = getattr(self, attribute)
listener.deregister(event)
def freeze(self):
"""
Freeze AT-SPI message processing, e.g. while displaying
a dialog or popoup menu.
"""
self._register_atspi_listeners(False)
self._frozen = True
def thaw(self):
"""
Resume AT-SPI message processing.
"""
self._update_listeners()
self._frozen = False
def emit_async(self, event_name, *args, **kwargs):
if not self._frozen:
EventSource.emit_async(self, event_name, *args, **kwargs)
def _get_cached_accessible(self, accessible):
return CachedAccessible(accessible) \
if accessible else None
# ######### synchronous handlers ######### #
def _on_atspi_global_focus(self, event, user_data):
self._on_atspi_focus(event, True)
def _on_atspi_object_focus(self, event, user_data):
self._on_atspi_focus(event)
def _on_atspi_focus(self, event, focus_received=False):
focused = (bool(focus_received) or
bool(event.detail1)) # received focus?
ae = AsyncEvent(accessible=self._get_cached_accessible(event.source),
focused=focused)
self.emit_async("async-focus-changed", ae)
def _on_atspi_text_changed(self, event, user_data):
# print("_on_atspi_text_changed", event.detail1, event.detail2,
# event.source, event.type, event.type.endswith("delete"))
ae = AsyncEvent(accessible=self._get_cached_accessible(event.source),
type=event.type,
pos=event.detail1,
length=event.detail2)
self.emit_async("async-text-changed", ae)
return False
def _on_atspi_text_caret_moved(self, event, user_data):
# print("_on_atspi_text_caret_moved", event.detail1, event.detail2,
# event.source, event.type, event.source.get_name(),
# event.source.get_role())
ae = AsyncEvent(accessible=self._get_cached_accessible(event.source),
caret=event.detail1)
self.emit_async("async-text-caret-moved", ae)
return False
def _on_atspi_keystroke(self, event, user_data):
if event.type == Atspi.EventType.KEY_PRESSED_EVENT:
_logger.atspi("key-stroke {} {} {} {}"
.format(event.modifiers,
event.hw_code, event.id, event.is_text))
# keysym = event.id # What is this? Not an XK_ keysym apparently.
ae = AsyncEvent(hw_code=event.hw_code,
modifiers=event.modifiers)
self.emit_async("key-pressed", ae)
return False # don't consume event
# ######### asynchronous handlers ######### #
def _on_async_focus_changed(self, event):
accessible = event.accessible
focused = event.focused
# Don't access the accessible while frozen. This leads to deadlocks
# while displaying Onboard's own dialogs/popup menu's.
if self._frozen:
return
self._log_accessible(accessible, focused)
if not accessible:
return
app_name = accessible.get_app_name().lower()
if app_name == "unity":
self._handle_focus_changed_unity(event)
else:
self._handle_focus_changed_apps(event)
def _handle_focus_changed_apps(self, event):
""" Focus change in regular applications """
accessible = event.accessible
focused = event.focused
# Since Trusty, focus events no longer come reliably in a
# predictable order. -> Store the last editable accessible
# so we can pick it over later focused non-editable ones.
# Helps to keep the keyboard open in presence of popup selections
# e.g. in GNOME's file dialog and in Unity Dash.
if self._focused_accessible == accessible:
if not focused:
self._focused_accessible = None
else:
pid = accessible.get_pid()
if focused:
self._poll_unity_timer.stop()
if accessible.is_editable():
self._focused_accessible = accessible
self._focused_pid = pid
# Static accessible, i.e. something that cannot
# accidentally steal the focus from an editable
# accessible. e.g. firefox ATSPI_ROLE_DOCUMENT_FRAME?
elif accessible.is_not_focus_stealing():
self._focused_accessible = None
self._focused_pid = None
else:
# Wily: attempt to hide when unity dash closes
# (there's no focus lost event).
# Also check duration since last activation to
# skip out of order focus events (firefox
# ATSPI_ROLE_DOCUMENT_FRAME) for a short while
# after opening dash.
now = time.time()
if focused and \
now - self._active_accessible_activation_time > .5:
if self._focused_pid != pid:
self._focused_accessible = None
_logger.atspi("Dropping accessible due to "
"pid change: {} != {} "
.format(self._focused_pid, pid))
# Has the previously focused accessible lost the focus?
active_accessible = self._focused_accessible
if active_accessible and \
not active_accessible.is_focused(True):
# Zesty: Firefox 50+ loses focus of the URL entry after
# typing just a few letters and focuses a completion
# menu item instead. Let's pretend the accessible is
# still focused in that case.
is_firefox_completion = \
self._focused_accessible.is_urlbar() and \
accessible.get_role() == Atspi.Role.MENU_ITEM
if not is_firefox_completion:
active_accessible = None
self._set_active_accessible(active_accessible)
def _handle_focus_changed_unity(self, event):
""" Focus change in Unity Dash """
accessible = event.accessible
focused = event.focused
# Wily: prevent random icons, buttons and toolbars
# in unity dash from hiding Onboard. Somehow hovering
# over those buttons silently drops the focus from the
# text entry. Let's pretend the buttons don't exist
# and keep the previously saved text entry active.
# Zesty: Don't fight lost focus events anymore, only
# react to focus events when the text entry gains focus.
if focused and \
accessible.is_editable():
self._focused_accessible = accessible
self._set_active_accessible(accessible)
# For hiding we poll Dash's toplevel accessible
def _poll_unity_dash():
frame = accessible.get_frame()
state_set = frame.get_state_set()
_logger.debug(
"polling unity dash state_set: {}"
.format(AtspiStateType.to_strings(state_set)))
if not state_set or \
not state_set.contains(Atspi.StateType.ACTIVE):
self._focused_accessible = None
self._set_active_accessible(None)
return False
return True
# Only ever start polling if Dash is "ACTIVE".
# The state_set might change in the future and the
# keyboard better fail to auto-hide than to never show.
frame = accessible.get_frame()
state_set = frame.get_state_set()
_logger.debug(
"dash focused, state_set: {}"
.format(AtspiStateType.to_strings(state_set)))
if state_set and \
state_set.contains(Atspi.StateType.ACTIVE):
self._poll_unity_timer.start(0.5, _poll_unity_dash)
def _set_active_accessible(self, accessible):
if self._active_accessible != accessible:
self._active_accessible = accessible
if self._active_accessible or \
self._last_active_accessible:
# notify listeners
self.emit("text-entry-activated", self._active_accessible)
self._last_active_accessible = self._active_accessible
self._active_accessible_activation_time = time.time()
def _on_async_text_changed(self, event):
if event.accessible == self._active_accessible:
type = event.type
insert = type.endswith(("insert", "insert:system"))
delete = type.endswith(("delete", "delete:system"))
# print(event.accessible.get_id(), type, insert)
if insert or delete:
event.insert = insert
self.emit("text-changed", event)
else:
_logger.warning("_on_async_text_changed: "
"unknown event type '{}'"
.format(event.type))
def _on_async_text_caret_moved(self, event):
if event.accessible == self._active_accessible:
self.emit("text-caret-moved", event)
def _log_accessible(self, accessible, focused):
if _logger.isEnabledFor(_logger.LEVEL_ATSPI):
msg = "AT-SPI focus event: focused={}, ".format(focused)
msg += "accessible={}, ".format(accessible)
if accessible:
name = accessible.get_name()
role = accessible.get_role()
role_name = accessible.get_role_name()
state_set = accessible.get_state_set()
states = state_set.states
editable = state_set.contains(Atspi.StateType.EDITABLE) \
if state_set else None
extents = accessible.get_extents()
msg += "name={name}, role={role}({role_name}), " \
"editable={editable}, states={states}, " \
"extents={extents}]" \
.format(accessible=accessible, name=repr(name),
role=role.value_name if role else role,
role_name=repr(role_name),
editable=editable,
states=states,
extents=extents
)
_logger.atspi(msg)
class AtspiStateType:
states = ['ACTIVE',
'ANIMATED',
'ARMED',
'BUSY',
'CHECKED',
'COLLAPSED',
'DEFUNCT',
'EDITABLE',
'ENABLED',
'EXPANDABLE',
'EXPANDED',
'FOCUSABLE',
'FOCUSED',
'HAS_TOOLTIP',
'HORIZONTAL',
'ICONIFIED',
'INDETERMINATE',
'INVALID',
'INVALID_ENTRY',
'IS_DEFAULT',
'LAST_DEFINED',
'MANAGES_DESCENDANTS',
'MODAL',
'MULTISELECTABLE',
'MULTI_LINE',
'OPAQUE',
'PRESSED',
'REQUIRED',
'RESIZABLE',
'SELECTABLE',
'SELECTABLE_TEXT',
'SELECTED',
'SENSITIVE',
'SHOWING',
'SINGLE_LINE',
'STALE',
'SUPPORTS_AUTOCOMPLETION',
'TRANSIENT',
'TRUNCATED',
'VERTICAL',
'VISIBLE',
'VISITED',
]
@staticmethod
def to_strings(state_set):
result = []
if state_set is not None:
for s in AtspiStateType.states:
if state_set.contains(getattr(Atspi.StateType, s)):
result.append(s)
return result
onboard-1.4.1/Onboard/XInput.py 0000644 0001750 0001750 00000046527 13051012134 016562 0 ustar frafu frafu 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright © 2012-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 .
from __future__ import division, print_function, unicode_literals
import sys
import copy
from Onboard.Version import require_gi_versions
require_gi_versions()
from gi.repository import Gdk
from Onboard.utils import EventSource, unicode_str
from Onboard.definitions import UINPUT_DEVICE_NAME
import Onboard.osk as osk
import logging
_logger = logging.getLogger(__name__)
class XIEventType:
""" enum of XInput events """
DeviceChanged = 1
KeyPress = 2
KeyRelease = 3
ButtonPress = 4
ButtonRelease = 5
Motion = 6
Enter = 7
Leave = 8
FocusIn = 9
FocusOut = 10
HierarchyChanged = 11
PropertyEvent = 12
RawKeyPress = 13
RawKeyRelease = 14
RawButtonPress = 15
RawButtonRelease = 16
RawMotion = 17
TouchBegin = 18
TouchUpdate = 19
TouchEnd = 20
TouchOwnership = 21
RawTouchBegin = 22
RawTouchUpdate = 23
RawTouchEnd = 24
# extra non-XI events
DeviceAdded = 1100
DeviceRemoved = 1101
SlaveAttached = 1102
SlaveDetached = 1103
HierarchyEvents = (DeviceAdded, DeviceRemoved, SlaveAttached, SlaveDetached)
class XIEventMask:
""" enum of XInput event masks """
DeviceChangedMask = 1 << XIEventType.DeviceChanged
KeyPressMask = 1 << XIEventType.KeyPress
KeyReleaseMask = 1 << XIEventType.KeyRelease
ButtonPressMask = 1 << XIEventType.ButtonPress
ButtonReleaseMask = 1 << XIEventType.ButtonRelease
MotionMask = 1 << XIEventType.Motion
EnterMask = 1 << XIEventType.Enter
LeaveMask = 1 << XIEventType.Leave
FocusInMask = 1 << XIEventType.FocusIn
FocusOutMask = 1 << XIEventType.FocusOut
HierarchyChangedMask = 1 << XIEventType.HierarchyChanged
PropertyEventMask = 1 << XIEventType.PropertyEvent
RawKeyPressMask = 1 << XIEventType.RawKeyPress
RawKeyReleaseMask = 1 << XIEventType.RawKeyRelease
RawButtonPressMask = 1 << XIEventType.RawButtonPress
RawButtonReleaseMask = 1 << XIEventType.RawButtonRelease
RawMotionMask = 1 << XIEventType.RawMotion
TouchBeginMask = 1 << XIEventType.TouchBegin
TouchUpdateMask = 1 << XIEventType.TouchUpdate
TouchEndMask = 1 << XIEventType.TouchEnd
TouchOwnershipMask = 1 << XIEventType.TouchOwnership
RawTouchBeginMask = 1 << XIEventType.RawTouchBegin
RawTouchUpdateMask = 1 << XIEventType.RawTouchUpdate
RawTouchEndMask = 1 << XIEventType.RawTouchEnd
TouchMask = TouchBeginMask | \
TouchUpdateMask | \
TouchEndMask
RawTouchMask = RawTouchBeginMask | \
RawTouchUpdateMask | \
RawTouchEndMask
class XIDeviceType:
""" enum of XInput device types """
MasterPointer = 1
MasterKeyboard = 2
SlavePointer = 3
SlaveKeyboard = 4
FloatingSlave = 5
class XITouchMode:
DirectTouch = 1
DependentTouch = 2
class XIDeviceManager(EventSource):
"""
XInput device manager singleton.
"""
blacklist = ("Virtual core XTEST keyboard",
UINPUT_DEVICE_NAME,
"Power Button")
last_device_blacklist = ("Virtual core XTEST pointer")
def __new__(cls, *args, **kwargs):
"""
Singleton magic.
"""
if not hasattr(cls, "self"):
cls.self = object.__new__(cls, *args, **kwargs)
cls.self.construct()
return cls.self
def __init__(self):
"""
Called multiple times, do not use. In particular don't
call base class constructors here.
"""
pass
def construct(self):
"""
Singleton constructor, runs only once.
"""
EventSource.__init__(self, ["device-event", "device-grab",
"devices-updated"])
self._devices = {}
self._osk_devices = None
try:
self._osk_devices = osk.Devices(event_handler = \
self._on_device_event)
except Exception as ex:
_logger.warning("Failed to create osk.Devices: " + \
unicode_str(ex))
self._last_motion_device_id = None
self._last_click_device_id = None
self._last_device_blacklist_ids = []
self._grabbed_devices_ids = set()
if self.is_valid():
self.update_devices()
def is_valid(self):
return not self._osk_devices is None
def lookup_device_id(self, device_id):
return self._devices.get(device_id)
def lookup_config_string(self, device_config_string):
for device in self.get_pointer_devices():
if device.get_config_string() == device_config_string:
return device
def get_client_pointer(self):
""" Return client pointer device """
device_id = self._osk_devices.get_client_pointer()
return self.lookup_device_id(device_id)
def get_client_keyboard(self):
""" Return client keyboard device """
client_pointer = self.get_client_pointer()
device_id = client_pointer.attachment
return self.lookup_device_id(device_id)
def get_devices(self):
return self._devices.values()
def get_pointer_devices(self):
return [device for device in self._devices.values() \
if device.is_pointer()]
def get_client_pointer_slaves(self):
"""
All slaves of the client pointer, with and without device grabs.
"""
client_pointer = self.get_client_pointer()
return [device for device in self.get_pointer_devices() \
if not device.is_master() and \
device.attachment == client_pointer.id]
def get_client_pointer_attached_slaves(self):
"""
Slaves that are currently attached to the client pointer.
"""
devices = self.get_client_pointer_slaves()
devices = [d for d in devices if not d.is_floating() and \
not self.is_grabbed(d)]
return devices
def get_keyboard_devices(self):
return [device for device in self._devices.values() \
if device.is_keyboard()]
def get_client_keyboard_slaves(self):
"""
All slaves of the keyboard paired with the client pointer,
with and without device grabs.
"""
client_keyboard = self.get_client_keyboard()
return [device for device in self.get_keyboard_devices() \
if not device.is_master() and \
device.attachment == client_keyboard.id]
def get_client_keyboard_attached_slaves(self):
"""
Slaves that are currently attached to the keyboard paired
with the client pointer.
"""
devices = self.get_client_keyboard_slaves()
devices = [d for d in devices if not d.is_floating() and \
not self.is_grabbed(d)]
return devices
def get_master_pointer_devices(self):
return [device for device in self.get_pointer_devices() \
if device.is_master()]
def update_devices(self):
devices = {}
self._last_device_blacklist_ids = []
for info in self._osk_devices.list():
device = XIDevice()
device._device_manager = self
(
device.name,
device.id,
device.use,
device.attachment,
device.enabled,
device.vendor,
device.product,
device.touch_mode,
) = info
device.source = XIDevice.classify_source(device.name, device.use,
device.touch_mode)
if sys.version_info.major == 2:
device.name = unicode_str(device.name)
if not device.name in self.blacklist:
devices[device.id] = device
if device.name in self.last_device_blacklist:
self._last_device_blacklist_ids.append(device.id)
self._devices = devices
self._last_click_device_id = None
self._last_motion_device_id = None
# notify listeners about the previous devices becoming invalid
self.emit("devices-updated")
def select_events(self, window, device, mask):
if window is None: # use root window?
xid = 0
else:
xid = window.get_xid()
if not xid:
return False
self._osk_devices.select_events(xid, device.id, mask)
return True
def unselect_events(self, window, device):
if window is None: # use root window?
xid = 0
else:
xid = window.get_xid()
if not xid:
return False
self._osk_devices.unselect_events(xid, device.id)
return True
def attach_device(self, device, master_id):
self.attach_device_id(device.id, master_id)
def detach_device(self, device):
self.detach_device_id(device.id)
def attach_device_id(self, device_id, master_id):
self._osk_devices.attach(device_id, master_id)
def detach_device_id(self, device_id):
self._osk_devices.detach(device_id)
def grab_device(self, device):
self.grab_device_id(device.id) # raises osk.error
self.emit("device-grab", device, True)
def ungrab_device(self, device):
self.ungrab_device_id(device.id) # raises osk.error
self.emit("device-grab", device, False)
def is_grabbed(self, device):
return device.id in self._grabbed_devices_ids
def grab_device_id(self, device_id):
self._osk_devices.grab_device(device_id, 0)
assert(not device_id in self._grabbed_devices_ids)
self._grabbed_devices_ids.add(device_id)
def ungrab_device_id(self, device_id):
self._grabbed_devices_ids.discard(device_id)
self._osk_devices.ungrab_device(device_id)
def get_last_click_device(self):
id = self._last_click_device_id
if id is None:
return None
return self.lookup_device_id(id)
def get_last_motion_device(self):
id = self._last_motion_device_id
if id is None:
return None
return self.lookup_device_id(id)
def _on_device_event(self, event):
"""
Handler for XI2 events.
"""
event_type = event.xi_type
device_id = event.device_id
source_id = event.source_id
# update our device objects on changes to the device hierarchy
if event_type in XIEventType.HierarchyEvents or \
event_type == XIEventType.DeviceChanged:
self.update_devices()
# simulate gtk source device
if source_id:
source_device = self.lookup_device_id(source_id)
if not source_device:
return
else:
source_device = None
event.set_source_device(source_device)
## debug, simulate touch-screen
if 0 and \
device_id == 11:
if not self._disguise_as_touch_event(event, False):
return
# remember recently used device ids for CSFloatingSlave
if not source_id in self._last_device_blacklist_ids:
if event_type == XIEventType.Motion:
self._last_motion_device_id = source_id
elif event_type == XIEventType.ButtonPress or \
event_type == XIEventType.TouchBegin:
self._last_click_device_id = source_id
# forward the event to all listeners
for callback in self._callbacks["device-event"]:
# Copy event to isolate callbacks from each other (LP: 1421840)
ev = copy.copy(event)
callback(ev)
def _disguise_as_touch_event(self, event, wacom_mode=False):
"""
Disguise a pointer event as touch event.
For debugging purposes only.
Set wacom_mode=True to simulate pointer events coming
from a "touch screen" device that only sends pointer events.
This is the default case for wacom touch screens due to
gestures being enabled, see LP #1297692.
"""
device = self.lookup_device_id(event.source_id)
device.name = "Touch-Screen"
device.source = Gdk.InputSource.TOUCHSCREEN
device.touch_mode = XITouchMode.DirectTouch
if event.xi_type == XIEventType.Motion and \
not event.state & (Gdk.ModifierType.BUTTON1_MASK | \
Gdk.ModifierType.BUTTON2_MASK | \
Gdk.ModifierType.BUTTON3_MASK):
return False # discard event
if not wacom_mode:
if event.xi_type == XIEventType.ButtonPress:
event.xi_type = XIEventType.TouchBegin
event.type = Gdk.EventType.TOUCH_BEGIN
if event.xi_type == XIEventType.ButtonRelease:
event.xi_type = XIEventType.TouchEnd
event.type = Gdk.EventType.TOUCH_END
if event.xi_type == XIEventType.Motion:
event.xi_type = XIEventType.TouchUpdate
event.type = Gdk.EventType.TOUCH_UPDATE
event.sequence = 10 # single touch only
event.button = 1
return True # allow event
class XIDevice(object):
"""
XInput device wrapper.
"""
name = None
id = None
use = None
attachment = None # master for slaves, paired master for masters
enabled = None
vendor = None
product = None
source = None
touch_mode = None
_device_manager = None
def __repr__(self):
return "{}(id={}, attachment={}, name={}, source={} )" \
.format(type(self).__name__,
repr(self.id),
repr(self.attachment),
repr(self.name),
repr(self.source),
)
def __str__(self):
return "{}(id={} attachment={} use={} touch_mode={} source={} name={} " \
"vendor=0x{:04x} product=0x{:04x} enabled={})" \
.format(type(self).__name__,
self.id,
self.attachment,
self.use,
self.touch_mode,
self.get_source().value_name,
self.name,
self.vendor,
self.product,
self.enabled,
)
def get_source(self):
"""
Return Gdk.InputSource for compatibility with Gtk event handling.
"""
return self.source
@staticmethod
def classify_source(name, use, touch_mode):
"""
Determine the source type (Gdk.InputSource) of the device.
Logic taken from GDK, gdk/x11/gdkdevicemanager-xi2.c
"""
if use == XIDeviceType.MasterKeyboard or \
use == XIDeviceType.SlaveKeyboard:
input_source = Gdk.InputSource.KEYBOARD
elif use == XIDeviceType.SlavePointer and \
touch_mode:
if touch_mode == XITouchMode.DirectTouch:
input_source = Gdk.InputSource.TOUCHSCREEN
else:
input_source = Gdk.InputSource.TOUCHPAD
else:
name = unicode_str(name.lower())
if "eraser" in name:
input_source = Gdk.InputSource.ERASER
elif "cursor" in name:
input_source = Gdk.InputSource.CURSOR
elif "wacom" in name or \
"pen" in name: # uh oh, false positives?
input_source = Gdk.InputSource.PEN
else:
input_source = Gdk.InputSource.MOUSE
return input_source
def is_touch_screen(self):
"""
Touch screen device?
"""
return self.source == Gdk.InputSource.TOUCHSCREEN
# methods inherited from Gerd's scanner device.
def is_master(self):
"""
Is this a master device?
"""
return self.use == XIDeviceType.MasterPointer or \
self.use == XIDeviceType.MasterKeyboard
def is_pointer(self):
"""
Is this device a pointer?
"""
return self.use == XIDeviceType.MasterPointer or \
self.use == XIDeviceType.SlavePointer
def is_keyboard(self):
"""
Is this device a keyboard?
"""
return self.use == XIDeviceType.MasterKeyboard or \
self.use == XIDeviceType.SlaveKeyboard
def is_floating(self):
"""
Is this device detached?
"""
return self.use == XIDeviceType.FloatingSlave
def get_config_string(self):
"""
Get a configuration string for the device.
Format: VID:PID:USE
"""
return "{:04X}:{:04X}:{!s}".format(self.vendor,
self.product,
self.use)
class XIDeviceEventLogger:
"""
Facilities for logging device events.
Has little overhead when logging is disabled.
"""
def __init__(self):
if not _logger.isEnabledFor(logging.DEBUG):
self.log_event = self._log_event_stub
def _log_device_event(self, event):
if not event.xi_type in [ XIEventType.TouchUpdate,
XIEventType.Motion]:
self.log_event("Device event: dev_id={} src_id={} xi_type={} "
"xid_event={}({}) x={} y={} x_root={} y_root={} "
"button={} state={} sequence={}"
"".format(event.device_id,
event.source_id,
event.xi_type,
event.xid_event,
self.get_xid(),
event.x, event.y,
event.x_root, event.y_root,
event.button, event.state,
event.sequence,
)
)
device = event.get_source_device()
self.log_event("Source device: " + str(device))
@staticmethod
def log_event(msg, *args):
_logger.event(msg.format(*args))
@staticmethod
def _log_event_stub(msg, *args):
pass
onboard-1.4.1/Onboard/TextContext.py 0000644 0001750 0001750 00000056254 13051012134 017622 0 ustar frafu frafu 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright © 2012-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 .
from __future__ import division, print_function, unicode_literals
import unicodedata
import time
import logging
_logger = logging.getLogger(__name__)
from Onboard.Version import require_gi_versions
require_gi_versions()
try:
from gi.repository import Atspi
except ImportError as e:
pass
from Onboard.AtspiStateTracker import AtspiStateTracker, AtspiStateType
from Onboard.TextDomain import TextDomains
from Onboard.TextChanges import TextChanges, TextSpan
from Onboard.utils import KeyCode, unicode_str
from Onboard.Timer import Timer
from Onboard import KeyCommon
### Config Singleton ###
from Onboard.Config import Config
config = Config()
class TextContext:
"""
Keep track of the current text context and intecept typed key events.
"""
def cleanup(self):
pass
def reset(self):
pass
def can_insert_text(self):
return NotImplementedError()
def insert_text(self, offset, text):
return NotImplementedError()
def insert_text_at_caret(self, text):
return NotImplementedError()
def delete_text(self, offset, length=1):
return NotImplementedError()
def delete_text_before_caret(self, length=1):
return NotImplementedError()
def get_context(self):
raise NotImplementedError()
def get_line(self):
raise NotImplementedError()
def get_line_caret_pos(self):
raise NotImplementedError()
def get_changes(self):
raise NotImplementedError()
def clear_changes(self):
raise NotImplementedError()
class AtspiTextContext(TextContext):
"""
Keep track of the current text context with AT-SPI
"""
_state_tracker = AtspiStateTracker()
def __init__(self, wp):
self._wp = wp
self._accessible = None
self._can_insert_text = False
self._text_domains = TextDomains()
self._text_domain = self._text_domains.get_nop_domain()
self._changes = TextChanges()
self._entering_text = False
self._text_changed = False
self._context = ""
self._line = ""
self._line_caret = 0
self._selection_span = TextSpan()
self._begin_of_text = False # context starts at begin of text?
self._begin_of_text_offset = None # offset of text begin
self._pending_separator_span = None
self._last_text_change_time = 0
self._last_caret_move_time = 0
self._last_caret_move_position = 0
self._last_context = None
self._last_line = None
self._update_context_timer = Timer()
self._update_context_delay_normal = 0.01
self._update_context_delay = self._update_context_delay_normal
def cleanup(self):
self._register_atspi_listeners(False)
def enable(self, enable):
self._register_atspi_listeners(enable)
def get_text_domain(self):
return self._text_domain
def set_pending_separator(self, separator_span=None):
""" Remember this separator span for later insertion. """
if self._pending_separator_span is not separator_span:
self._pending_separator_span = separator_span
def get_pending_separator(self):
""" Return current pending separator span or None """
return self._pending_separator_span
def get_context(self):
"""
Returns the predictions context, i.e. some range of
text before the caret position.
"""
if self._accessible is None:
return ""
# Don't update suggestions in scrolling terminals
if self._entering_text or \
not self._text_changed or \
self.can_suggest_before_typing():
return self._context
return ""
def get_bot_context(self):
"""
Returns the predictions context with
begin of text marker (at text begin).
"""
context = ""
if self._accessible:
context = self.get_context()
# prepend domain specific begin-of-text marker
if self._begin_of_text:
marker = self.get_text_begin_marker()
if marker:
context = marker + " " + context
return context
def get_pending_bot_context(self):
"""
Context including bot marker and pending separator.
"""
context = self.get_bot_context()
if self._pending_separator_span is not None:
context += self._pending_separator_span.get_span_text()
return context
def get_line(self):
return self._line \
if self._accessible else ""
def get_line_caret_pos(self):
return self._line_caret \
if self._accessible else 0
def get_line_past_caret(self):
return self._line[self._line_caret:] \
if self._accessible else ""
def get_selection_span(self):
return self._selection_span \
if self._accessible else None
def get_span_at_caret(self):
if not self._accessible:
return None
span = self._selection_span.copy()
span.length = 0
return span
def get_caret(self):
return self._selection_span.begin() \
if self._accessible else 0
def get_character_extents(self, offset):
accessible = self._accessible
if accessible:
return accessible.get_character_extents(offset)
else:
return None
def get_text_begin_marker(self):
domain = self.get_text_domain()
if domain:
return domain.get_text_begin_marker()
return ""
def can_record_insertion(self, accessible, pos, length):
domain = self.get_text_domain()
if domain:
return domain.can_record_insertion(accessible, pos, length)
return True
def can_suggest_before_typing(self):
domain = self.get_text_domain()
if domain:
return domain.can_suggest_before_typing()
return True
def can_auto_punctuate(self):
domain = self.get_text_domain()
if domain:
return domain.can_auto_punctuate(self._begin_of_text)
return False
def get_begin_of_text_offset(self):
return self._begin_of_text_offset \
if self._accessible else None
def get_changes(self):
return self._changes
def has_changes(self):
""" Are there any changes to learn? """
return not self._changes.is_empty()
def clear_changes(self):
self._changes.clear()
def can_insert_text(self):
"""
Can delete or insert text into the accessible?
"""
# support for inserting is spotty: not in firefox, terminal
return bool(self._accessible) and self._can_insert_text
def delete_text(self, offset, length=1):
""" Delete directly, without going through faking key presses. """
self._accessible.delete_text(offset, offset + length)
def delete_text_before_caret(self, length=1):
""" Delete directly, without going through faking key presses. """
try:
caret_offset = self._accessible.get_caret_offset()
except Exception as ex: # Private exception gi._glib.GError when
_logger.info("TextContext.delete_text_before_caret(): " +
unicode_str(ex))
return
self.delete_text(caret_offset - length, length)
def insert_text(self, offset, text):
"""
Insert directly, without going through faking key presses.
"""
self._accessible.insert_text(offset, text)
# Move the caret after insertion if the accessible itself
# hasn't done so already. This assumes the insertion begins at
# the current caret position, which always happens to be the case
# currently.
# Only the nautilus rename text entry appears to need this.
offset_before = offset
try:
offset_after = self._accessible.get_caret_offset()
except Exception as ex: # Private exception gi._glib.GError when
_logger.info("TextContext.insert_text(): " +
unicode_str(ex))
return
if text and offset_before == offset_after:
self._accessible.set_caret_offset(offset_before + len(text))
def insert_text_at_caret(self, text):
"""
Insert directly, without going through faking key presses.
Fails for terminal and firefox, unfortunately.
"""
try:
caret_offset = self._accessible.get_caret_offset()
except Exception as ex: # Private exception gi._glib.GError when
_logger.info("TextContext.insert_text_at_caret(): " +
unicode_str(ex))
return
self.insert_text(caret_offset, text)
def _register_atspi_listeners(self, register=True):
st = self._state_tracker
if register:
st.connect("text-entry-activated", self._on_text_entry_activated)
st.connect("text-changed", self._on_text_changed)
st.connect("text-caret-moved", self._on_text_caret_moved)
# st.connect("key-pressed", self._on_atspi_key_pressed)
else:
st.disconnect("text-entry-activated", self._on_text_entry_activated)
st.disconnect("text-changed", self._on_text_changed)
st.disconnect("text-caret-moved", self._on_text_caret_moved)
# st.disconnect("key-pressed", self._on_atspi_key_pressed)
def get_accessible_capabilities(self, accessible):
can_insert_text = False
if accessible:
# Can insert text via Atspi?
# Advantages:
# - faster, no individual key presses
# - trouble-free insertion of all unicode characters
if "EditableText" in accessible.get_interfaces():
# Support for atspi text insertion is spotty.
# Firefox, LibreOffice Writer, gnome-terminal don't support it,
# even if they claim to implement the EditableText interface.
# Allow direct text insertion for gtk widgets
if accessible.is_toolkit_gtk3():
can_insert_text = True
return can_insert_text
def _on_text_entry_activated(self, accessible):
# old text_domain still valid here
self._wp.on_text_entry_deactivated()
# keep track of the active accessible asynchronously
self._accessible = accessible
self._entering_text = False
self._text_changed = False
# make sure state is filled with essential entries
if accessible:
accessible.get_role()
accessible.get_attributes()
accessible.get_interfaces()
accessible.is_urlbar()
state = accessible.get_state()
else:
state = {}
# select text domain matching this accessible
self._text_domain = self._text_domains.find_match(**state)
self._text_domain.init_domain()
# determine capabilities of this accessible
self._can_insert_text = \
self.get_accessible_capabilities(accessible)
# log accessible info
if _logger.isEnabledFor(_logger.LEVEL_ATSPI):
log = _logger.atspi
log("-" * 70)
log("Accessible focused: ")
indent = " " * 4
if accessible:
state = accessible.get_all_state()
for key, value in sorted(state.items()):
msg = str(key) + "="
if key == "state-set":
msg += repr(AtspiStateType.to_strings(value))
elif hasattr(value, "value_name"): # e.g. role
msg += value.value_name
else:
msg += repr(value)
log(indent + msg)
log(indent + "text_domain: {}"
.format(self._text_domain and
type(self._text_domain).__name__))
log(indent + "can_insert_text: {}"
.format(self._can_insert_text))
else:
log(indent + "None")
self._update_context()
self._wp.on_text_entry_activated()
def _on_text_changed(self, event):
insertion_span = self._record_text_change(event.pos,
event.length,
event.insert)
# synchronously notify of text insertion
if insertion_span:
try:
caret_offset = self._accessible.get_caret_offset()
except Exception as ex: # Private exception gi._glib.GError when
_logger.info("TextContext._on_text_changed(): " +
unicode_str(ex))
else:
self._wp.on_text_inserted(insertion_span, caret_offset)
self._last_text_change_time = time.time()
self._update_context()
def _on_text_caret_moved(self, event):
self._last_caret_move_time = time.time()
self._last_caret_move_position = event.caret
self._update_context()
self._wp.on_text_caret_moved()
def _on_atspi_key_pressed(self, event):
""" disabled, Francesco didn't receive any AT-SPI key-strokes. """
# keycode = event.hw_code # uh oh, only keycodes...
# # hopefully "c" doesn't move around a lot.
# modifiers = event.modifiers
# self._handle_key_press(keycode, modifiers)
def on_onboard_typing(self, key, mod_mask):
if key.is_text_changing():
keycode = 0
if key.is_return():
keycode = KeyCode.KP_Enter
else:
label = key.get_label()
if label == "C" or label == "c":
keycode = KeyCode.C
self._handle_key_press(keycode, mod_mask)
def _handle_key_press(self, keycode, modifiers):
if self._accessible:
domain = self.get_text_domain()
if domain:
self._entering_text, end_of_editing = \
domain.handle_key_press(keycode, modifiers)
if end_of_editing is True:
self._wp.commit_changes()
elif end_of_editing is False:
self._wp.discard_changes()
def _record_text_change(self, pos, length, insert):
accessible = self._accessible
insertion_span = None
char_count = None
if accessible:
try:
char_count = accessible.get_character_count()
except: # gi._glib.GError: The application no longer exists
# when closing a tab in gnome-terminal.
char_count = None
if char_count is not None:
# record the change
spans_to_update = []
if insert:
if self._entering_text and \
self.can_record_insertion(accessible, pos, length):
if self._wp.is_typing() or length < 30:
# Remember all of the insertion, might have been
# a pressed snippet or wordlist button.
include_length = -1
else:
# Remember only the first few characters.
# Large inserts can be paste, reload or scroll
# operations. Only learn the first word of these.
include_length = 2
# simple span for current insertion
begin = max(pos - 100, 0)
end = min(pos + length + 100, char_count)
try:
text = accessible.get_text(begin, end)
except Exception as ex:
_logger.info("TextContext._record_text_change() 1: " +
unicode_str(ex))
else:
insertion_span = TextSpan(pos, length, text, begin)
else:
# Remember nothing, just update existing spans.
include_length = None
spans_to_update = self._changes.insert(pos, length,
include_length)
else:
spans_to_update = self._changes.delete(pos, length,
self._entering_text)
# update text of all modified spans
for span in spans_to_update:
# Get some more text around the span to hopefully
# include whole words at beginning and end.
begin = max(span.begin() - 100, 0)
end = min(span.end() + 100, char_count)
try:
span.text = accessible.get_text(begin, end)
except Exception as ex:
_logger.info("TextContext._record_text_change() 2: " +
unicode_str(ex))
span.text = ""
span.text_pos = begin
self._text_changed = True
return insertion_span
def set_update_context_delay(self, delay):
self._update_context_delay = delay
def reset_update_context_delay(self):
self._update_context_delay = self._update_context_delay_normal
def _update_context(self):
self._update_context_timer.start(self._update_context_delay,
self.on_text_context_changed)
def on_text_context_changed(self):
# Clear pending separator when the user clicked to move
# the cursor away from the separator position.
if self._pending_separator_span:
# Lone caret movement, no recent text change?
if self._last_caret_move_time - self._last_text_change_time > 1.0:
# Away from the separator?
if self._last_caret_move_position != \
self._pending_separator_span.begin():
self.set_pending_separator(None)
result = self._text_domain.read_context(self._wp, self._accessible)
if result is not None:
(self._context,
self._line,
self._line_caret,
self._selection_span,
self._begin_of_text,
self._begin_of_text_offset) = result
# make sure to include bot-markers and pending separator
context = self.get_pending_bot_context()
change_detected = (self._last_context != context or
self._last_line != self._line)
if change_detected:
self._last_context = context
self._last_line = self._line
self._wp.on_text_context_changed(change_detected)
return False
class InputLine(TextContext):
"""
Track key presses ourselves.
Advantage: Doesn't require AT-SPI
Problems: Misses key repeats,
Doesn't know about keymap translations before events are
delivered to their destination, i.e records wrong key
strokes when changing keymaps.
"""
def __init__(self):
self.reset()
def reset(self):
self.line = ""
self.caret = 0
self.valid = True
self.word_infos = {}
def is_valid(self):
return self.valid
def is_empty(self):
return len(self.line) == 0
def insert(self, s):
self.line = self.line[:self.caret] + s + self.line[self.caret:]
self.move_caret(len(s))
def delete_left(self, n=1): # backspace
self.line = self.line[:self.caret - n] + self.line[self.caret:]
self.move_caret(-n)
def delete_right(self, n=1): # delete
self.line = self.line[:self.caret] + self.line[self.caret + n:]
def move_caret(self, n):
self.caret += n
# moving into unknown territory -> suggest reset
if self.caret < 0:
self.caret = 0
self.valid = False
if self.caret > len(self.line):
self.caret = len(self.line)
self.valid = False
def get_context(self):
return self.line[:self.caret]
def get_line(self):
return self.line
def get_line_caret_pos(self):
return self.caret
@staticmethod
def is_printable(char):
"""
True for printable keys including whitespace as defined for isprint().
"""
if char == "\t":
return True
return not unicodedata.category(char) in ('Cc', 'Cf', 'Cs', 'Co',
'Cn', 'Zl', 'Zp')
def track_sent_key(self, key, mods):
"""
Sync input_line with single key presses.
WORD_ACTION and MACRO_ACTION do this in press_key_string.
"""
end_editing = False
if config.wp.stealth_mode:
return True
id = key.id.upper()
char = key.get_label()
if char is None or len(char) > 1:
char = ""
if key.action_type == KeyCommon.WORD_ACTION:
pass # don't reset input on word insertion
elif key.action_type == KeyCommon.MODIFIER_ACTION:
pass # simply pressing a modifier shouldn't stop the word
elif key.action_type == KeyCommon.BUTTON_ACTION:
pass
elif key.action_type == KeyCommon.KEYSYM_ACTION:
if id == 'ESC':
self.reset()
end_editing = True
elif key.action_type == KeyCommon.KEYPRESS_NAME_ACTION:
if id == 'DELE':
self.delete_right()
elif id == 'LEFT':
self.move_caret(-1)
elif id == 'RGHT':
self.move_caret(1)
else:
end_editing = True
elif key.action_type == KeyCommon.KEYCODE_ACTION:
if id == 'RTRN':
char = "\n"
elif id == 'SPCE':
char = " "
elif id == 'TAB':
char = "\t"
if id == 'BKSP':
self.delete_left()
elif self.is_printable(char):
if mods[4]: # ctrl+key press?
end_editing = True
else:
self.insert(char)
else:
end_editing = True
else:
end_editing = True
if not self.is_valid(): # caret moved outside known range?
end_editing = True
# print end_editing,"'%s' " % self.line, self.caret
return end_editing
onboard-1.4.1/Onboard/LayoutView.py 0000644 0001750 0001750 00000064653 13051012134 017443 0 ustar frafu frafu 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright © 2012-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 .
""" Keyboard layout view """
from __future__ import division, print_function, unicode_literals
import time
from math import pi
import cairo
from Onboard.Version import require_gi_versions
require_gi_versions()
from gi.repository import Gtk, Gdk, GdkPixbuf
from Onboard.utils import Rect, \
roundrect_arc, roundrect_curve, \
gradient_line, brighten, \
unicode_str
from Onboard.WindowUtils import get_monitor_dimensions
from Onboard.KeyGtk import Key
from Onboard.KeyCommon import LOD
from Onboard.definitions import UIMask
### Logging ###
import logging
_logger = logging.getLogger("LayoutView")
###############
### Config Singleton ###
from Onboard.Config import Config
config = Config()
########################
class LayoutView:
"""
Viewer for a tree of layout items.
"""
def __init__(self, keyboard):
self.keyboard = keyboard
self.supports_alpha = False
self._lod = LOD.FULL
self._font_sizes_valid = False
self._shadow_quality_valid = False
self._last_canvas_shadow_rect = Rect()
self._starting_up = True
self._keys_pre_rendered = False
self.keyboard.register_view(self)
def cleanup(self):
self.keyboard.deregister_view(self)
# free xserver memory
self.invalidate_keys()
self.invalidate_shadows()
def handle_realize_event(self):
self.update_touch_input_mode()
self.update_input_event_source()
def on_layout_loaded(self):
""" Layout has been loaded. """
self.invalidate_shadow_quality()
def get_layout(self):
return self.keyboard.layout
def get_color_scheme(self):
return self.keyboard.color_scheme
def invalidate_for_resize(self, lod=LOD.FULL):
self.invalidate_keys()
if self._lod == LOD.FULL:
self.invalidate_shadows()
self.invalidate_font_sizes()
# self.invalidate_label_extents()
self.keyboard.invalidate_for_resize()
def invalidate_font_sizes(self):
"""
Update font_sizes at the next possible chance.
"""
self._font_sizes_valid = False
def invalidate_keys(self):
"""
Clear cached key surfaces, e.g. after resizing,
change of theme settings.
"""
layout = self.get_layout()
if layout:
for item in layout.iter_keys():
item.invalidate_key()
def invalidate_images(self):
"""
Clear cached images, e.g. after changing window_scaling_factor.
"""
layout = self.get_layout()
if layout:
for item in layout.iter_keys():
item.invalidate_image()
def invalidate_shadows(self):
"""
Clear cached shadow surfaces, e.g. after resizing,
change of theme settings.
"""
layout = self.get_layout()
if layout:
for item in layout.iter_keys():
item.invalidate_shadow()
def invalidate_shadow_quality(self):
self._shadow_quality_valid = False
def invalidate_label_extents(self):
"""
Clear cached resolution independent label extents, e.g.
after changes to the systems font dpi setting (gtk-xft-dpi).
"""
layout = self.get_layout()
if layout:
for item in layout.iter_keys():
item.invalidate_label_extents()
def reset_lod(self):
""" Reset to full level of detail """
if self._lod != LOD.FULL:
self._lod = LOD.FULL
self.invalidate_for_resize()
self.keyboard.invalidate_context_ui()
self.keyboard.invalidate_canvas()
self.keyboard.commit_ui_updates()
def is_visible(self):
return None
def set_visible(self, visible):
pass
def toggle_visible(self):
pass
def raise_to_top(self):
pass
def redraw(self, items=None, invalidate=True):
"""
Queue redrawing for individual keys or the whole keyboard.
"""
if items is None:
self.queue_draw()
elif len(items) == 0:
pass
else:
area = None
for item in items:
rect = item.get_canvas_border_rect()
area = area.union(rect) if area else rect
# assume keys need to be refreshed when actively redrawn
# e.g. for pressed state changes, dwell progress updates...
if invalidate and \
item.is_key():
item.invalidate_key()
# account for stroke width, anti-aliasing
if self.get_layout():
extra_size = items[0].get_extra_render_size()
area = area.inflate(*extra_size)
self.queue_draw_area(*area)
def redraw_labels(self, invalidate=True):
self.redraw(self.update_labels(), invalidate)
def update_transparency(self):
pass
def update_input_event_source(self):
self.register_input_events(True, config.is_event_source_gtk())
def update_touch_input_mode(self):
self.set_touch_input_mode(config.keyboard.touch_input)
def can_delay_sequence_begin(self, sequence):
"""
Veto gesture delay for move buttons. Have the keyboard start
moving right away and not lag behind the pointer.
"""
layout = self.get_layout()
if layout:
for item in layout.find_ids(["move"]):
if item.is_path_visible() and \
item.is_point_within(sequence.point):
return False
return True
def show_touch_handles(self, show, auto_hide):
pass
def apply_ui_updates(self, mask):
if mask & UIMask.SIZE:
self.invalidate_for_resize()
def update_layout(self):
pass
def process_updates(self):
""" Draw now, synchronously. """
window = self.get_window()
if window:
window.process_updates(True)
def render(self, context):
""" Pre-render key surfaces for instant initial drawing. """
# lazily update font sizes and labels
if not self._font_sizes_valid:
self.update_labels()
layout = self.get_layout()
if not layout:
return
self._auto_select_shadow_quality(context)
# run through all visible layout items
for item in layout.iter_visible_items():
if item.is_key():
item.draw_shadow_cached(context)
item.draw_cached(context)
self._keys_pre_rendered = True
def _can_draw_cached(self, lod):
"""
Draw cached key surfaces?
On first startup draw cached only if keys were pre-rendered, i.e. the
time to render keys was hidden before the window was shown.
We can't easily pre-render keys in xembed mode because the window size
is unknown in advance. Draw there once uncached instead (faster).
"""
return (lod == LOD.FULL) and \
(not self._starting_up or self._keys_pre_rendered)
def draw(self, widget, context):
if not Gtk.cairo_should_draw_window(context, widget.get_window()):
return
lod = self._lod
draw_cached = self._can_draw_cached(lod)
# lazily update font sizes and labels
if not self._font_sizes_valid:
self.update_labels(lod)
draw_rect = self.get_damage_rect(context)
# draw background
decorated = self._draw_background(context, lod)
layout = self.get_layout()
if not layout:
return
# draw layer 0 and None-layer background
layer_ids = layout.get_layer_ids()
if config.window.transparent_background:
alpha = 0.0
elif decorated:
alpha = self.get_background_rgba()[3]
else:
alpha = 1.0
self._draw_layer_key_background(context, alpha,
None, None, lod)
if layer_ids:
self._draw_layer_key_background(context, alpha,
None, layer_ids[0], lod)
# run through all visible layout items
for item in layout.iter_visible_items():
if item.layer_id:
self._draw_layer_background(context, item, layer_ids, decorated)
# draw key
if item.is_key() and \
draw_rect.intersects(item.get_canvas_border_rect()):
if draw_cached:
item.draw_cached(context)
else:
item.draw(context, lod)
self._starting_up = False
return decorated
def _draw_background(self, context, lod):
""" Draw keyboard background """
transparent_bg = False
plain_bg = False
if config.is_keep_xembed_frame_aspect_ratio_enabled():
if self.supports_alpha:
self._clear_xembed_background(context)
transparent_bg = True
else:
plain_bg = True
elif config.xid_mode:
# xembed mode
# Disable transparency in lightdm and g-s-s for now.
# There are too many issues and there is no real
# visual improvement.
plain_bg = True
elif config.has_window_decoration():
# decorated window
if self.supports_alpha and \
config.window.transparent_background:
self._clear_background(context)
else:
plain_bg = True
else:
# undecorated window
if self.supports_alpha:
self._clear_background(context)
if not config.window.transparent_background:
transparent_bg = True
else:
plain_bg = True
if plain_bg:
self._draw_plain_background(context)
if transparent_bg:
self._draw_transparent_background(context, lod)
return transparent_bg
def _clear_background(self, context):
"""
Clear the whole gtk background.
Makes the whole strut transparent in xembed mode.
"""
context.save()
context.set_operator(cairo.OPERATOR_CLEAR)
context.paint()
context.restore()
def _clear_xembed_background(self, context):
""" fill with plain layer 0 color; no alpha support required """
rect = Rect(0, 0, self.get_allocated_width(),
self.get_allocated_height())
# draw background image
if config.get_xembed_background_image_enabled():
pixbuf = self._get_xembed_background_image()
if pixbuf:
src_size = (pixbuf.get_width(), pixbuf.get_height())
x, y = 0, rect.bottom() - src_size[1]
Gdk.cairo_set_source_pixbuf(context, pixbuf, x, y)
context.paint()
# draw solid colored bar on top (with transparency, usually)
rgba = config.get_xembed_background_rgba()
if rgba is None:
rgba = self.get_background_rgba()
rgba[3] = 0.5
context.set_source_rgba(*rgba)
context.rectangle(*rect)
context.fill()
def _get_xembed_background_image(self):
""" load the desktop background image in Unity """
try:
pixbuf = self._xid_background_image
except AttributeError:
size, size_mm = get_monitor_dimensions(self)
filename = config.get_desktop_background_filename()
if not filename or \
size[0] <= 0 or size[1] <= 0:
pixbuf = None
else:
try:
# load image
pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
# Scale image to mimic the behavior of gnome-screen-saver.
# Take the largest, aspect correct, centered rectangle
# that fits on the monitor.
rm = Rect(0, 0, size[0], size[1])
rp = Rect(0, 0, pixbuf.get_width(), pixbuf.get_height())
ra = rm.inscribe_with_aspect(rp)
pixbuf = pixbuf.new_subpixbuf(*ra)
pixbuf = pixbuf.scale_simple(size[0], size[1],
GdkPixbuf.InterpType.BILINEAR)
except Exception as ex: # private exception gi._glib.GError when
# librsvg2-common wasn't installed
_logger.error("_get_xembed_background_image(): " + \
unicode_str(ex))
pixbuf = None
self._xid_background_image = pixbuf
return pixbuf
def _draw_transparent_background(self, context, lod):
""" fill with the transparent background color """
corner_radius = config.CORNER_RADIUS
rect = self.get_keyboard_frame_rect()
fill = self.get_background_rgba()
if self.can_draw_sidebars():
self._draw_side_bars(context)
fill_gradient = config.theme_settings.background_gradient
if lod == LOD.MINIMAL or \
fill_gradient == 0:
context.set_source_rgba(*fill)
else:
fill_gradient /= 100.0
direction = config.theme_settings.key_gradient_direction
alpha = -pi/2.0 + pi * direction / 180.0
gline = gradient_line(rect, alpha)
pat = cairo.LinearGradient (*gline)
rgba = brighten(+fill_gradient*.5, *fill)
pat.add_color_stop_rgba(0, *rgba)
rgba = brighten(-fill_gradient*.5, *fill)
pat.add_color_stop_rgba(1, *rgba)
context.set_source (pat)
if config.xid_mode:
frame = False
else:
frame = self.can_draw_frame()
if frame:
roundrect_arc(context, rect, corner_radius)
else:
context.rectangle(*rect)
context.fill()
if frame:
self.draw_window_frame(context, lod)
self.draw_keyboard_frame(context, lod)
def _draw_side_bars(self, context):
"""
Transparent bars left and right of the aspect corrected
keyboard frame.
"""
rgba = self.get_background_rgba()
rgba[3] = 0.5
rwin = Rect(0, 0,
self.get_allocated_width(),
self.get_allocated_height())
rframe = self.get_keyboard_frame_rect()
if rwin.w > rframe.w:
r = rframe.copy()
context.set_source_rgba(*rgba)
context.set_line_width(0)
r.x = rwin.left()
r.w = rframe.left() - rwin.left()
context.rectangle(*r)
context.fill()
r.x = rframe.right()
r.w = rwin.right() - rframe.right()
context.rectangle(*r)
context.fill()
def can_draw_frame(self):
""" overloaded in KeyboardWidget """
return True
def can_draw_sidebars(self):
""" overloaded in KeyboardWidget """
return False
def draw_window_frame(self, context, lod):
pass
def draw_keyboard_frame(self, context, lod):
""" draw frame around the (potentially aspect corrected) keyboard """
corner_radius = config.CORNER_RADIUS
rect = self.get_keyboard_frame_rect()
fill = self.get_background_rgba()
# inner decoration line
line_rect = rect.deflate(1)
roundrect_arc(context, line_rect, corner_radius)
context.stroke()
def _draw_plain_background(self, context, layer_index = 0):
""" fill with plain layer 0 color; no alpha support required """
rgba = self._get_layer_fill_rgba(layer_index)
context.set_source_rgba(*rgba)
context.paint()
def _draw_layer_background(self, context, item, layer_ids, decorated):
# layer background
layer_index = layer_ids.index(item.layer_id)
parent = item.parent
if parent and \
layer_index != 0:
rect = parent.get_canvas_rect()
context.rectangle(*rect.inflate(1))
color_scheme = self.get_color_scheme()
if color_scheme:
rgba = color_scheme.get_layer_fill_rgba(layer_index)
else:
rgba = [0.5, 0.5, 0.5, 0.9]
context.set_source_rgba(*rgba)
context.fill()
# per-layer key background
self._draw_layer_key_background(context, 1.0, item, item.layer_id)
def _draw_layer_key_background(self, context, alpha = 1.0, item = None,
layer_id = None, lod = LOD.FULL):
self._draw_dish_key_background(context, alpha, item, layer_id)
self._draw_shadows(context, layer_id, lod)
def _draw_dish_key_background(self, context, alpha = 1.0, item = None,
layer_id = None):
"""
Black background following the contours of key clusters
to simulate the opening in the keyboard plane.
"""
if config.theme_settings.key_style == "dish":
layout = self.get_layout()
context.push_group()
context.set_source_rgba(0, 0, 0, 1)
enlargement = layout.context.scale_log_to_canvas((0.8, 0.8))
corner_radius = layout.context.scale_log_to_canvas_x(2.4)
if item is None:
item = layout
for key in item.iter_layer_keys(layer_id):
rect = key.get_canvas_fullsize_rect()
rect = rect.inflate(*enlargement)
roundrect_curve(context, rect, corner_radius)
context.fill()
context.pop_group_to_source()
context.paint_with_alpha(alpha);
def _draw_shadows(self, context, layer_id, lod):
"""
Draw drop shadows for all keys.
"""
# Shadows are drawn at odd positions when resizing while
# docked and extended with side bars visible.
# -> Turn them off while resizing. Improves rendering speed a bit too.
if lod < LOD.FULL:
return
if not config.theme_settings.key_shadow_strength:
return
self._auto_select_shadow_quality(context)
context.save()
self.set_shadow_scale(context, lod)
draw_rect = self.get_damage_rect(context)
layout = self.get_layout()
for item in layout.iter_layer_keys(layer_id):
if draw_rect.intersects(item.get_canvas_border_rect()):
item.draw_shadow_cached(context)
context.restore()
def _auto_select_shadow_quality(self, context):
""" auto-select shadow quality """
if not self._shadow_quality_valid:
quality = self._probe_shadow_performance(context)
Key.set_shadow_quality(quality)
self._shadow_quality_valid = True
def _probe_shadow_performance(self, context):
"""
Determine shadow quality based on the estimated render time of
the first layer's shadows.
"""
probe_begin = time.time()
quality = None
layout = self.get_layout()
max_total_time = 0.03 # upper limit refreshing all key's shadows [s]
max_probe_keys = 10
keys = None
for layer_id in layout.get_layer_ids():
layer_keys = list(layout.iter_layer_keys(layer_id))
num_first_layer_keys = len(layer_keys)
keys = layer_keys[:max_probe_keys]
break
if keys:
for quality, (steps, alpha) in enumerate(Key._shadow_presets):
begin = time.time()
for key in keys:
key.create_shadow_surface(context, steps, 0.1)
elapsed = time.time() - begin
estimate = elapsed / len(keys) * num_first_layer_keys
_logger.debug("Probing shadow performance: "
"estimated full refresh time {:6.1f}ms "
"at quality {}, {} steps." \
.format(estimate * 1000,
quality, steps))
if estimate > max_total_time:
break
_logger.info("Probing shadow performance took {:.1f}ms. "
"Selecting quality {}." \
.format((time.time() - probe_begin) * 1000,
quality))
return quality
def set_shadow_scale(self, context, lod):
"""
Shadows aren't normally refreshed while resizing.
-> scale the cached ones to fit the new canvas size.
Occasionally refresh them anyway if scaling becomes noticeable.
"""
r = self.get_keyboard_frame_rect()
if lod < LOD.FULL:
rl = self._last_canvas_shadow_rect
scale_x = r.w / rl.w
scale_y = r.h / rl.h
# scale in a reasonable range? -> draw stretched shadows
smin = 0.8
smax = 1.2
if smax > scale_x > smin and \
smax > scale_y > smin:
context.scale(scale_x, scale_y)
else:
# else scale is too far out -> refresh shadows
self.invalidate_shadows()
self._last_canvas_shadow_rect = r
else:
self._last_canvas_shadow_rect = r
def _get_layer_fill_rgba(self, layer_index):
color_scheme = self.get_color_scheme()
if color_scheme:
return color_scheme.get_layer_fill_rgba(layer_index)
else:
return [0.5, 0.5, 0.5, 1.0]
def get_background_rgba(self):
""" layer 0 color * background_transparency """
layer0_rgba = self._get_layer_fill_rgba(0)
background_alpha = config.window.get_background_opacity()
background_alpha *= layer0_rgba[3]
return layer0_rgba[:3] + [background_alpha]
def get_popup_window_rgba(self, element = "border"):
color_scheme = self.get_color_scheme()
if color_scheme:
rgba = color_scheme.get_window_rgba("key-popup", element)
else:
rgba = [0.8, 0.8, 0.8, 1.0]
background_alpha = config.window.get_background_opacity()
background_alpha *= rgba[3]
return rgba[:3] + [background_alpha]
def get_damage_rect(self, context):
clip_rect = Rect.from_extents(*context.clip_extents())
# Draw a little more than just the clip_rect.
# Prevents glitches around pressed keys in at least classic theme.
layout = self.get_layout()
if layout:
extra_size = layout.context.scale_log_to_canvas((2.0, 2.0))
else:
extra_size = 0, 0
return clip_rect.inflate(*extra_size)
def get_keyboard_frame_rect(self):
"""
Rectangle of the potentially aspect-corrected
frame around the layout.
"""
layout = self.get_layout()
if layout:
rect = layout.get_canvas_border_rect()
rect = rect.inflate(self.get_frame_width())
else:
rect = Rect(0, 0, self.get_allocated_width(),
self.get_allocated_height())
return rect.int()
def is_docking_expanded(self):
return self.window.docking_enabled and self.window.docking_expanded
def update_labels(self, lod = LOD.FULL):
"""
Iterate through all key groups and set each key's
label font size to the maximum possible for that group.
"""
changed_keys = set()
layout = self.get_layout()
mod_mask = self.keyboard.get_mod_mask()
if layout:
if lod == LOD.FULL: # no label changes necessary while dragging
for key in layout.iter_keys():
old_label = key.get_label()
key.configure_label(mod_mask)
if key.get_label() != old_label:
changed_keys.add(key)
for keys in layout.get_key_groups().values():
max_size = 0
for key in keys:
best_size = key.get_best_font_size(mod_mask)
if best_size:
if key.ignore_group:
if key.font_size != best_size:
key.font_size = best_size
changed_keys.add(key)
else:
if not max_size or best_size < max_size:
max_size = best_size
for key in keys:
if key.font_size != max_size and \
not key.ignore_group:
key.font_size = max_size
changed_keys.add(key)
self._font_sizes_valid = True
return tuple(changed_keys)
def get_key_at_location(self, point):
layout = self.get_layout()
keyboard = self.keyboard
if layout and keyboard: # may be gone on exit
return layout.get_key_at(point, keyboard.active_layer)
return None
def get_xid(self):
# Zesty, X, Gtk 3.22: XInput select_events() on self leads to
# LP: #1636252. On the first call to get_xid() of a child widget,
# Gtk creates a new native X Window with broken transparency.
# The toplevel window ought to always have a native X window, so
# we'll pick that one instead and skip on-the fly creation.
# TouchInput isn't used for anything other than full client areas
# yet, so in principle this shouldn't be a problem.
toplevel = self.get_toplevel()
if toplevel:
topwin = toplevel.get_window()
if topwin:
return topwin.get_xid()
return 0
onboard-1.4.1/Onboard/pypredict/ 0000755 0001750 0001750 00000000000 13051420243 016752 5 ustar frafu frafu 0000000 0000000 onboard-1.4.1/Onboard/pypredict/__init__.py 0000644 0001750 0001750 00000001644 13051012134 021064 0 ustar frafu frafu 0000000 0000000 # Copyright © 2009, 2012 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 .
import sys
from os.path import dirname, abspath
# allow absolute imports from inside the package
package_root = dirname(dirname(abspath(__file__)))
sys.path.insert(0, package_root)
from pypredict.lm_wrapper import *
onboard-1.4.1/Onboard/pypredict/lm/ 0000755 0001750 0001750 00000000000 13051420243 017362 5 ustar frafu frafu 0000000 0000000 onboard-1.4.1/Onboard/pypredict/lm/accent_transform.h 0000644 0001750 0001750 00000111364 13051012134 023065 0 ustar frafu frafu 0000000 0000000
/*
* Copyright © 2012 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 .
*/
//
// Generated for Onboard by gen_accent_transform
//
wint_t _accent_transform[][2] = {
{ 0x00c0, 0x0041 }, // À -> A
{ 0x00c1, 0x0041 }, // Á -> A
{ 0x00c2, 0x0041 }, // Â -> A
{ 0x00c3, 0x0041 }, // Ã -> A
{ 0x00c4, 0x0041 }, // Ä -> A
{ 0x00c5, 0x0041 }, // Å -> A
{ 0x00c7, 0x0043 }, // Ç -> C
{ 0x00c8, 0x0045 }, // È -> E
{ 0x00c9, 0x0045 }, // É -> E
{ 0x00ca, 0x0045 }, // Ê -> E
{ 0x00cb, 0x0045 }, // Ë -> E
{ 0x00cc, 0x0049 }, // Ì -> I
{ 0x00cd, 0x0049 }, // Í -> I
{ 0x00ce, 0x0049 }, // Î -> I
{ 0x00cf, 0x0049 }, // Ï -> I
{ 0x00d1, 0x004e }, // Ñ -> N
{ 0x00d2, 0x004f }, // Ò -> O
{ 0x00d3, 0x004f }, // Ó -> O
{ 0x00d4, 0x004f }, // Ô -> O
{ 0x00d5, 0x004f }, // Õ -> O
{ 0x00d6, 0x004f }, // Ö -> O
{ 0x00d9, 0x0055 }, // Ù -> U
{ 0x00da, 0x0055 }, // Ú -> U
{ 0x00db, 0x0055 }, // Û -> U
{ 0x00dc, 0x0055 }, // Ü -> U
{ 0x00dd, 0x0059 }, // Ý -> Y
{ 0x00e0, 0x0061 }, // à -> a
{ 0x00e1, 0x0061 }, // á -> a
{ 0x00e2, 0x0061 }, // â -> a
{ 0x00e3, 0x0061 }, // ã -> a
{ 0x00e4, 0x0061 }, // ä -> a
{ 0x00e5, 0x0061 }, // å -> a
{ 0x00e7, 0x0063 }, // ç -> c
{ 0x00e8, 0x0065 }, // è -> e
{ 0x00e9, 0x0065 }, // é -> e
{ 0x00ea, 0x0065 }, // ê -> e
{ 0x00eb, 0x0065 }, // ë -> e
{ 0x00ec, 0x0069 }, // ì -> i
{ 0x00ed, 0x0069 }, // í -> i
{ 0x00ee, 0x0069 }, // î -> i
{ 0x00ef, 0x0069 }, // ï -> i
{ 0x00f1, 0x006e }, // ñ -> n
{ 0x00f2, 0x006f }, // ò -> o
{ 0x00f3, 0x006f }, // ó -> o
{ 0x00f4, 0x006f }, // ô -> o
{ 0x00f5, 0x006f }, // õ -> o
{ 0x00f6, 0x006f }, // ö -> o
{ 0x00f9, 0x0075 }, // ù -> u
{ 0x00fa, 0x0075 }, // ú -> u
{ 0x00fb, 0x0075 }, // û -> u
{ 0x00fc, 0x0075 }, // ü -> u
{ 0x00fd, 0x0079 }, // ý -> y
{ 0x00ff, 0x0079 }, // ÿ -> y
{ 0x0100, 0x0041 }, // Ā -> A
{ 0x0101, 0x0061 }, // ā -> a
{ 0x0102, 0x0041 }, // Ă -> A
{ 0x0103, 0x0061 }, // ă -> a
{ 0x0104, 0x0041 }, // Ą -> A
{ 0x0105, 0x0061 }, // ą -> a
{ 0x0106, 0x0043 }, // Ć -> C
{ 0x0107, 0x0063 }, // ć -> c
{ 0x0108, 0x0043 }, // Ĉ -> C
{ 0x0109, 0x0063 }, // ĉ -> c
{ 0x010a, 0x0043 }, // Ċ -> C
{ 0x010b, 0x0063 }, // ċ -> c
{ 0x010c, 0x0043 }, // Č -> C
{ 0x010d, 0x0063 }, // č -> c
{ 0x010e, 0x0044 }, // Ď -> D
{ 0x010f, 0x0064 }, // ď -> d
{ 0x0112, 0x0045 }, // Ē -> E
{ 0x0113, 0x0065 }, // ē -> e
{ 0x0114, 0x0045 }, // Ĕ -> E
{ 0x0115, 0x0065 }, // ĕ -> e
{ 0x0116, 0x0045 }, // Ė -> E
{ 0x0117, 0x0065 }, // ė -> e
{ 0x0118, 0x0045 }, // Ę -> E
{ 0x0119, 0x0065 }, // ę -> e
{ 0x011a, 0x0045 }, // Ě -> E
{ 0x011b, 0x0065 }, // ě -> e
{ 0x011c, 0x0047 }, // Ĝ -> G
{ 0x011d, 0x0067 }, // ĝ -> g
{ 0x011e, 0x0047 }, // Ğ -> G
{ 0x011f, 0x0067 }, // ğ -> g
{ 0x0120, 0x0047 }, // Ġ -> G
{ 0x0121, 0x0067 }, // ġ -> g
{ 0x0122, 0x0047 }, // Ģ -> G
{ 0x0123, 0x0067 }, // ģ -> g
{ 0x0124, 0x0048 }, // Ĥ -> H
{ 0x0125, 0x0068 }, // ĥ -> h
{ 0x0128, 0x0049 }, // Ĩ -> I
{ 0x0129, 0x0069 }, // ĩ -> i
{ 0x012a, 0x0049 }, // Ī -> I
{ 0x012b, 0x0069 }, // ī -> i
{ 0x012c, 0x0049 }, // Ĭ -> I
{ 0x012d, 0x0069 }, // ĭ -> i
{ 0x012e, 0x0049 }, // Į -> I
{ 0x012f, 0x0069 }, // į -> i
{ 0x0130, 0x0049 }, // İ -> I
{ 0x0134, 0x004a }, // Ĵ -> J
{ 0x0135, 0x006a }, // ĵ -> j
{ 0x0136, 0x004b }, // Ķ -> K
{ 0x0137, 0x006b }, // ķ -> k
{ 0x0139, 0x004c }, // Ĺ -> L
{ 0x013a, 0x006c }, // ĺ -> l
{ 0x013b, 0x004c }, // Ļ -> L
{ 0x013c, 0x006c }, // ļ -> l
{ 0x013d, 0x004c }, // Ľ -> L
{ 0x013e, 0x006c }, // ľ -> l
{ 0x0143, 0x004e }, // Ń -> N
{ 0x0144, 0x006e }, // ń -> n
{ 0x0145, 0x004e }, // Ņ -> N
{ 0x0146, 0x006e }, // ņ -> n
{ 0x0147, 0x004e }, // Ň -> N
{ 0x0148, 0x006e }, // ň -> n
{ 0x014c, 0x004f }, // Ō -> O
{ 0x014d, 0x006f }, // ō -> o
{ 0x014e, 0x004f }, // Ŏ -> O
{ 0x014f, 0x006f }, // ŏ -> o
{ 0x0150, 0x004f }, // Ő -> O
{ 0x0151, 0x006f }, // ő -> o
{ 0x0154, 0x0052 }, // Ŕ -> R
{ 0x0155, 0x0072 }, // ŕ -> r
{ 0x0156, 0x0052 }, // Ŗ -> R
{ 0x0157, 0x0072 }, // ŗ -> r
{ 0x0158, 0x0052 }, // Ř -> R
{ 0x0159, 0x0072 }, // ř -> r
{ 0x015a, 0x0053 }, // Ś -> S
{ 0x015b, 0x0073 }, // ś -> s
{ 0x015c, 0x0053 }, // Ŝ -> S
{ 0x015d, 0x0073 }, // ŝ -> s
{ 0x015e, 0x0053 }, // Ş -> S
{ 0x015f, 0x0073 }, // ş -> s
{ 0x0160, 0x0053 }, // Š -> S
{ 0x0161, 0x0073 }, // š -> s
{ 0x0162, 0x0054 }, // Ţ -> T
{ 0x0163, 0x0074 }, // ţ -> t
{ 0x0164, 0x0054 }, // Ť -> T
{ 0x0165, 0x0074 }, // ť -> t
{ 0x0168, 0x0055 }, // Ũ -> U
{ 0x0169, 0x0075 }, // ũ -> u
{ 0x016a, 0x0055 }, // Ū -> U
{ 0x016b, 0x0075 }, // ū -> u
{ 0x016c, 0x0055 }, // Ŭ -> U
{ 0x016d, 0x0075 }, // ŭ -> u
{ 0x016e, 0x0055 }, // Ů -> U
{ 0x016f, 0x0075 }, // ů -> u
{ 0x0170, 0x0055 }, // Ű -> U
{ 0x0171, 0x0075 }, // ű -> u
{ 0x0172, 0x0055 }, // Ų -> U
{ 0x0173, 0x0075 }, // ų -> u
{ 0x0174, 0x0057 }, // Ŵ -> W
{ 0x0175, 0x0077 }, // ŵ -> w
{ 0x0176, 0x0059 }, // Ŷ -> Y
{ 0x0177, 0x0079 }, // ŷ -> y
{ 0x0178, 0x0059 }, // Ÿ -> Y
{ 0x0179, 0x005a }, // Ź -> Z
{ 0x017a, 0x007a }, // ź -> z
{ 0x017b, 0x005a }, // Ż -> Z
{ 0x017c, 0x007a }, // ż -> z
{ 0x017d, 0x005a }, // Ž -> Z
{ 0x017e, 0x007a }, // ž -> z
{ 0x01a0, 0x004f }, // Ơ -> O
{ 0x01a1, 0x006f }, // ơ -> o
{ 0x01af, 0x0055 }, // Ư -> U
{ 0x01b0, 0x0075 }, // ư -> u
{ 0x01cd, 0x0041 }, // Ǎ -> A
{ 0x01ce, 0x0061 }, // ǎ -> a
{ 0x01cf, 0x0049 }, // Ǐ -> I
{ 0x01d0, 0x0069 }, // ǐ -> i
{ 0x01d1, 0x004f }, // Ǒ -> O
{ 0x01d2, 0x006f }, // ǒ -> o
{ 0x01d3, 0x0055 }, // Ǔ -> U
{ 0x01d4, 0x0075 }, // ǔ -> u
{ 0x01d5, 0x0055 }, // Ǖ -> U
{ 0x01d6, 0x0075 }, // ǖ -> u
{ 0x01d7, 0x0055 }, // Ǘ -> U
{ 0x01d8, 0x0075 }, // ǘ -> u
{ 0x01d9, 0x0055 }, // Ǚ -> U
{ 0x01da, 0x0075 }, // ǚ -> u
{ 0x01db, 0x0055 }, // Ǜ -> U
{ 0x01dc, 0x0075 }, // ǜ -> u
{ 0x01de, 0x0041 }, // Ǟ -> A
{ 0x01df, 0x0061 }, // ǟ -> a
{ 0x01e0, 0x0041 }, // Ǡ -> A
{ 0x01e1, 0x0061 }, // ǡ -> a
{ 0x01e2, 0x00c6 }, // Ǣ -> Æ
{ 0x01e3, 0x00e6 }, // ǣ -> æ
{ 0x01e6, 0x0047 }, // Ǧ -> G
{ 0x01e7, 0x0067 }, // ǧ -> g
{ 0x01e8, 0x004b }, // Ǩ -> K
{ 0x01e9, 0x006b }, // ǩ -> k
{ 0x01ea, 0x004f }, // Ǫ -> O
{ 0x01eb, 0x006f }, // ǫ -> o
{ 0x01ec, 0x004f }, // Ǭ -> O
{ 0x01ed, 0x006f }, // ǭ -> o
{ 0x01ee, 0x01b7 }, // Ǯ -> Ʒ
{ 0x01ef, 0x0292 }, // ǯ -> ʒ
{ 0x01f0, 0x006a }, // ǰ -> j
{ 0x01f4, 0x0047 }, // Ǵ -> G
{ 0x01f5, 0x0067 }, // ǵ -> g
{ 0x01f8, 0x004e }, // Ǹ -> N
{ 0x01f9, 0x006e }, // ǹ -> n
{ 0x01fa, 0x0041 }, // Ǻ -> A
{ 0x01fb, 0x0061 }, // ǻ -> a
{ 0x01fc, 0x00c6 }, // Ǽ -> Æ
{ 0x01fd, 0x00e6 }, // ǽ -> æ
{ 0x01fe, 0x00d8 }, // Ǿ -> Ø
{ 0x01ff, 0x00f8 }, // ǿ -> ø
{ 0x0200, 0x0041 }, // Ȁ -> A
{ 0x0201, 0x0061 }, // ȁ -> a
{ 0x0202, 0x0041 }, // Ȃ -> A
{ 0x0203, 0x0061 }, // ȃ -> a
{ 0x0204, 0x0045 }, // Ȅ -> E
{ 0x0205, 0x0065 }, // ȅ -> e
{ 0x0206, 0x0045 }, // Ȇ -> E
{ 0x0207, 0x0065 }, // ȇ -> e
{ 0x0208, 0x0049 }, // Ȉ -> I
{ 0x0209, 0x0069 }, // ȉ -> i
{ 0x020a, 0x0049 }, // Ȋ -> I
{ 0x020b, 0x0069 }, // ȋ -> i
{ 0x020c, 0x004f }, // Ȍ -> O
{ 0x020d, 0x006f }, // ȍ -> o
{ 0x020e, 0x004f }, // Ȏ -> O
{ 0x020f, 0x006f }, // ȏ -> o
{ 0x0210, 0x0052 }, // Ȑ -> R
{ 0x0211, 0x0072 }, // ȑ -> r
{ 0x0212, 0x0052 }, // Ȓ -> R
{ 0x0213, 0x0072 }, // ȓ -> r
{ 0x0214, 0x0055 }, // Ȕ -> U
{ 0x0215, 0x0075 }, // ȕ -> u
{ 0x0216, 0x0055 }, // Ȗ -> U
{ 0x0217, 0x0075 }, // ȗ -> u
{ 0x0218, 0x0053 }, // Ș -> S
{ 0x0219, 0x0073 }, // ș -> s
{ 0x021a, 0x0054 }, // Ț -> T
{ 0x021b, 0x0074 }, // ț -> t
{ 0x021e, 0x0048 }, // Ȟ -> H
{ 0x021f, 0x0068 }, // ȟ -> h
{ 0x0226, 0x0041 }, // Ȧ -> A
{ 0x0227, 0x0061 }, // ȧ -> a
{ 0x0228, 0x0045 }, // Ȩ -> E
{ 0x0229, 0x0065 }, // ȩ -> e
{ 0x022a, 0x004f }, // Ȫ -> O
{ 0x022b, 0x006f }, // ȫ -> o
{ 0x022c, 0x004f }, // Ȭ -> O
{ 0x022d, 0x006f }, // ȭ -> o
{ 0x022e, 0x004f }, // Ȯ -> O
{ 0x022f, 0x006f }, // ȯ -> o
{ 0x0230, 0x004f }, // Ȱ -> O
{ 0x0231, 0x006f }, // ȱ -> o
{ 0x0232, 0x0059 }, // Ȳ -> Y
{ 0x0233, 0x0079 }, // ȳ -> y
{ 0x0374, 0x02b9 }, // ʹ -> ʹ
{ 0x037e, 0x003b }, // ; -> ;
{ 0x0385, 0x00a8 }, // ΅ -> ¨
{ 0x0386, 0x0391 }, // Ά -> Α
{ 0x0387, 0x00b7 }, // · -> ·
{ 0x0388, 0x0395 }, // Έ -> Ε
{ 0x0389, 0x0397 }, // Ή -> Η
{ 0x038a, 0x0399 }, // Ί -> Ι
{ 0x038c, 0x039f }, // Ό -> Ο
{ 0x038e, 0x03a5 }, // Ύ -> Υ
{ 0x038f, 0x03a9 }, // Ώ -> Ω
{ 0x0390, 0x03b9 }, // ΐ -> ι
{ 0x03aa, 0x0399 }, // Ϊ -> Ι
{ 0x03ab, 0x03a5 }, // Ϋ -> Υ
{ 0x03ac, 0x03b1 }, // ά -> α
{ 0x03ad, 0x03b5 }, // έ -> ε
{ 0x03ae, 0x03b7 }, // ή -> η
{ 0x03af, 0x03b9 }, // ί -> ι
{ 0x03b0, 0x03c5 }, // ΰ -> υ
{ 0x03ca, 0x03b9 }, // ϊ -> ι
{ 0x03cb, 0x03c5 }, // ϋ -> υ
{ 0x03cc, 0x03bf }, // ό -> ο
{ 0x03cd, 0x03c5 }, // ύ -> υ
{ 0x03ce, 0x03c9 }, // ώ -> ω
{ 0x03d3, 0x03d2 }, // ϓ -> ϒ
{ 0x03d4, 0x03d2 }, // ϔ -> ϒ
{ 0x0400, 0x0415 }, // Ѐ -> Е
{ 0x0401, 0x0415 }, // Ё -> Е
{ 0x0403, 0x0413 }, // Ѓ -> Г
{ 0x0407, 0x0406 }, // Ї -> І
{ 0x040c, 0x041a }, // Ќ -> К
{ 0x040d, 0x0418 }, // Ѝ -> И
{ 0x040e, 0x0423 }, // Ў -> У
{ 0x0419, 0x0418 }, // Й -> И
{ 0x0439, 0x0438 }, // й -> и
{ 0x0450, 0x0435 }, // ѐ -> е
{ 0x0451, 0x0435 }, // ё -> е
{ 0x0453, 0x0433 }, // ѓ -> г
{ 0x0457, 0x0456 }, // ї -> і
{ 0x045c, 0x043a }, // ќ -> к
{ 0x045d, 0x0438 }, // ѝ -> и
{ 0x045e, 0x0443 }, // ў -> у
{ 0x0476, 0x0474 }, // Ѷ -> Ѵ
{ 0x0477, 0x0475 }, // ѷ -> ѵ
{ 0x04c1, 0x0416 }, // Ӂ -> Ж
{ 0x04c2, 0x0436 }, // ӂ -> ж
{ 0x04d0, 0x0410 }, // Ӑ -> А
{ 0x04d1, 0x0430 }, // ӑ -> а
{ 0x04d2, 0x0410 }, // Ӓ -> А
{ 0x04d3, 0x0430 }, // ӓ -> а
{ 0x04d6, 0x0415 }, // Ӗ -> Е
{ 0x04d7, 0x0435 }, // ӗ -> е
{ 0x04da, 0x04d8 }, // Ӛ -> Ә
{ 0x04db, 0x04d9 }, // ӛ -> ә
{ 0x04dc, 0x0416 }, // Ӝ -> Ж
{ 0x04dd, 0x0436 }, // ӝ -> ж
{ 0x04de, 0x0417 }, // Ӟ -> З
{ 0x04df, 0x0437 }, // ӟ -> з
{ 0x04e2, 0x0418 }, // Ӣ -> И
{ 0x04e3, 0x0438 }, // ӣ -> и
{ 0x04e4, 0x0418 }, // Ӥ -> И
{ 0x04e5, 0x0438 }, // ӥ -> и
{ 0x04e6, 0x041e }, // Ӧ -> О
{ 0x04e7, 0x043e }, // ӧ -> о
{ 0x04ea, 0x04e8 }, // Ӫ -> Ө
{ 0x04eb, 0x04e9 }, // ӫ -> ө
{ 0x04ec, 0x042d }, // Ӭ -> Э
{ 0x04ed, 0x044d }, // ӭ -> э
{ 0x04ee, 0x0423 }, // Ӯ -> У
{ 0x04ef, 0x0443 }, // ӯ -> у
{ 0x04f0, 0x0423 }, // Ӱ -> У
{ 0x04f1, 0x0443 }, // ӱ -> у
{ 0x04f2, 0x0423 }, // Ӳ -> У
{ 0x04f3, 0x0443 }, // ӳ -> у
{ 0x04f4, 0x0427 }, // Ӵ -> Ч
{ 0x04f5, 0x0447 }, // ӵ -> ч
{ 0x04f8, 0x042b }, // Ӹ -> Ы
{ 0x04f9, 0x044b }, // ӹ -> ы
{ 0x0622, 0x0627 }, // آ -> ا
{ 0x0623, 0x0627 }, // أ -> ا
{ 0x0624, 0x0648 }, // ؤ -> و
{ 0x0625, 0x0627 }, // إ -> ا
{ 0x0626, 0x064a }, // ئ -> ي
{ 0x06c0, 0x06d5 }, // ۀ -> ە
{ 0x06c2, 0x06c1 }, // ۂ -> ہ
{ 0x06d3, 0x06d2 }, // ۓ -> ے
{ 0x0929, 0x0928 }, // ऩ -> न
{ 0x0931, 0x0930 }, // ऱ -> र
{ 0x0934, 0x0933 }, // ऴ -> ळ
{ 0x0958, 0x0915 }, // क़ -> क
{ 0x0959, 0x0916 }, // ख़ -> ख
{ 0x095a, 0x0917 }, // ग़ -> ग
{ 0x095b, 0x091c }, // ज़ -> ज
{ 0x095c, 0x0921 }, // ड़ -> ड
{ 0x095d, 0x0922 }, // ढ़ -> ढ
{ 0x095e, 0x092b }, // फ़ -> फ
{ 0x095f, 0x092f }, // य़ -> य
{ 0x09dc, 0x09a1 }, // ড় -> ড
{ 0x09dd, 0x09a2 }, // ঢ় -> ঢ
{ 0x09df, 0x09af }, // য় -> য
{ 0x0a33, 0x0a32 }, // ਲ਼ -> ਲ
{ 0x0a36, 0x0a38 }, // ਸ਼ -> ਸ
{ 0x0a59, 0x0a16 }, // ਖ਼ -> ਖ
{ 0x0a5a, 0x0a17 }, // ਗ਼ -> ਗ
{ 0x0a5b, 0x0a1c }, // ਜ਼ -> ਜ
{ 0x0a5e, 0x0a2b }, // ਫ਼ -> ਫ
{ 0x0b48, 0x0b47 }, // ୈ -> େ
{ 0x0b5c, 0x0b21 }, // ଡ଼ -> ଡ
{ 0x0b5d, 0x0b22 }, // ଢ଼ -> ଢ
{ 0x0cc0, 0x0cd5 }, // ೀ -> ೕ
{ 0x0cc7, 0x0cd5 }, // ೇ -> ೕ
{ 0x0cc8, 0x0cd6 }, // ೈ -> ೖ
{ 0x0cca, 0x0cc2 }, // ೊ -> ೂ
{ 0x0dda, 0x0dd9 }, // ේ -> ෙ
{ 0x0ddd, 0x0ddc }, // ෝ -> ො
{ 0x0f43, 0x0f42 }, // གྷ -> ག
{ 0x0f4d, 0x0f4c }, // ཌྷ -> ཌ
{ 0x0f52, 0x0f51 }, // དྷ -> ད
{ 0x0f57, 0x0f56 }, // བྷ -> བ
{ 0x0f5c, 0x0f5b }, // ཛྷ -> ཛ
{ 0x0f69, 0x0f40 }, // ཀྵ -> ཀ
{ 0x1026, 0x1025 }, // ဦ -> ဥ
{ 0x1b3b, 0x1b35 }, // ᬻ -> ᬵ
{ 0x1b3d, 0x1b35 }, // ᬽ -> ᬵ
{ 0x1b43, 0x1b35 }, // ᭃ -> ᬵ
{ 0x1e00, 0x0041 }, // Ḁ -> A
{ 0x1e01, 0x0061 }, // ḁ -> a
{ 0x1e02, 0x0042 }, // Ḃ -> B
{ 0x1e03, 0x0062 }, // ḃ -> b
{ 0x1e04, 0x0042 }, // Ḅ -> B
{ 0x1e05, 0x0062 }, // ḅ -> b
{ 0x1e06, 0x0042 }, // Ḇ -> B
{ 0x1e07, 0x0062 }, // ḇ -> b
{ 0x1e08, 0x0043 }, // Ḉ -> C
{ 0x1e09, 0x0063 }, // ḉ -> c
{ 0x1e0a, 0x0044 }, // Ḋ -> D
{ 0x1e0b, 0x0064 }, // ḋ -> d
{ 0x1e0c, 0x0044 }, // Ḍ -> D
{ 0x1e0d, 0x0064 }, // ḍ -> d
{ 0x1e0e, 0x0044 }, // Ḏ -> D
{ 0x1e0f, 0x0064 }, // ḏ -> d
{ 0x1e10, 0x0044 }, // Ḑ -> D
{ 0x1e11, 0x0064 }, // ḑ -> d
{ 0x1e12, 0x0044 }, // Ḓ -> D
{ 0x1e13, 0x0064 }, // ḓ -> d
{ 0x1e14, 0x0045 }, // Ḕ -> E
{ 0x1e15, 0x0065 }, // ḕ -> e
{ 0x1e16, 0x0045 }, // Ḗ -> E
{ 0x1e17, 0x0065 }, // ḗ -> e
{ 0x1e18, 0x0045 }, // Ḙ -> E
{ 0x1e19, 0x0065 }, // ḙ -> e
{ 0x1e1a, 0x0045 }, // Ḛ -> E
{ 0x1e1b, 0x0065 }, // ḛ -> e
{ 0x1e1c, 0x0045 }, // Ḝ -> E
{ 0x1e1d, 0x0065 }, // ḝ -> e
{ 0x1e1e, 0x0046 }, // Ḟ -> F
{ 0x1e1f, 0x0066 }, // ḟ -> f
{ 0x1e20, 0x0047 }, // Ḡ -> G
{ 0x1e21, 0x0067 }, // ḡ -> g
{ 0x1e22, 0x0048 }, // Ḣ -> H
{ 0x1e23, 0x0068 }, // ḣ -> h
{ 0x1e24, 0x0048 }, // Ḥ -> H
{ 0x1e25, 0x0068 }, // ḥ -> h
{ 0x1e26, 0x0048 }, // Ḧ -> H
{ 0x1e27, 0x0068 }, // ḧ -> h
{ 0x1e28, 0x0048 }, // Ḩ -> H
{ 0x1e29, 0x0068 }, // ḩ -> h
{ 0x1e2a, 0x0048 }, // Ḫ -> H
{ 0x1e2b, 0x0068 }, // ḫ -> h
{ 0x1e2c, 0x0049 }, // Ḭ -> I
{ 0x1e2d, 0x0069 }, // ḭ -> i
{ 0x1e2e, 0x0049 }, // Ḯ -> I
{ 0x1e2f, 0x0069 }, // ḯ -> i
{ 0x1e30, 0x004b }, // Ḱ -> K
{ 0x1e31, 0x006b }, // ḱ -> k
{ 0x1e32, 0x004b }, // Ḳ -> K
{ 0x1e33, 0x006b }, // ḳ -> k
{ 0x1e34, 0x004b }, // Ḵ -> K
{ 0x1e35, 0x006b }, // ḵ -> k
{ 0x1e36, 0x004c }, // Ḷ -> L
{ 0x1e37, 0x006c }, // ḷ -> l
{ 0x1e38, 0x004c }, // Ḹ -> L
{ 0x1e39, 0x006c }, // ḹ -> l
{ 0x1e3a, 0x004c }, // Ḻ -> L
{ 0x1e3b, 0x006c }, // ḻ -> l
{ 0x1e3c, 0x004c }, // Ḽ -> L
{ 0x1e3d, 0x006c }, // ḽ -> l
{ 0x1e3e, 0x004d }, // Ḿ -> M
{ 0x1e3f, 0x006d }, // ḿ -> m
{ 0x1e40, 0x004d }, // Ṁ -> M
{ 0x1e41, 0x006d }, // ṁ -> m
{ 0x1e42, 0x004d }, // Ṃ -> M
{ 0x1e43, 0x006d }, // ṃ -> m
{ 0x1e44, 0x004e }, // Ṅ -> N
{ 0x1e45, 0x006e }, // ṅ -> n
{ 0x1e46, 0x004e }, // Ṇ -> N
{ 0x1e47, 0x006e }, // ṇ -> n
{ 0x1e48, 0x004e }, // Ṉ -> N
{ 0x1e49, 0x006e }, // ṉ -> n
{ 0x1e4a, 0x004e }, // Ṋ -> N
{ 0x1e4b, 0x006e }, // ṋ -> n
{ 0x1e4c, 0x004f }, // Ṍ -> O
{ 0x1e4d, 0x006f }, // ṍ -> o
{ 0x1e4e, 0x004f }, // Ṏ -> O
{ 0x1e4f, 0x006f }, // ṏ -> o
{ 0x1e50, 0x004f }, // Ṑ -> O
{ 0x1e51, 0x006f }, // ṑ -> o
{ 0x1e52, 0x004f }, // Ṓ -> O
{ 0x1e53, 0x006f }, // ṓ -> o
{ 0x1e54, 0x0050 }, // Ṕ -> P
{ 0x1e55, 0x0070 }, // ṕ -> p
{ 0x1e56, 0x0050 }, // Ṗ -> P
{ 0x1e57, 0x0070 }, // ṗ -> p
{ 0x1e58, 0x0052 }, // Ṙ -> R
{ 0x1e59, 0x0072 }, // ṙ -> r
{ 0x1e5a, 0x0052 }, // Ṛ -> R
{ 0x1e5b, 0x0072 }, // ṛ -> r
{ 0x1e5c, 0x0052 }, // Ṝ -> R
{ 0x1e5d, 0x0072 }, // ṝ -> r
{ 0x1e5e, 0x0052 }, // Ṟ -> R
{ 0x1e5f, 0x0072 }, // ṟ -> r
{ 0x1e60, 0x0053 }, // Ṡ -> S
{ 0x1e61, 0x0073 }, // ṡ -> s
{ 0x1e62, 0x0053 }, // Ṣ -> S
{ 0x1e63, 0x0073 }, // ṣ -> s
{ 0x1e64, 0x0053 }, // Ṥ -> S
{ 0x1e65, 0x0073 }, // ṥ -> s
{ 0x1e66, 0x0053 }, // Ṧ -> S
{ 0x1e67, 0x0073 }, // ṧ -> s
{ 0x1e68, 0x0053 }, // Ṩ -> S
{ 0x1e69, 0x0073 }, // ṩ -> s
{ 0x1e6a, 0x0054 }, // Ṫ -> T
{ 0x1e6b, 0x0074 }, // ṫ -> t
{ 0x1e6c, 0x0054 }, // Ṭ -> T
{ 0x1e6d, 0x0074 }, // ṭ -> t
{ 0x1e6e, 0x0054 }, // Ṯ -> T
{ 0x1e6f, 0x0074 }, // ṯ -> t
{ 0x1e70, 0x0054 }, // Ṱ -> T
{ 0x1e71, 0x0074 }, // ṱ -> t
{ 0x1e72, 0x0055 }, // Ṳ -> U
{ 0x1e73, 0x0075 }, // ṳ -> u
{ 0x1e74, 0x0055 }, // Ṵ -> U
{ 0x1e75, 0x0075 }, // ṵ -> u
{ 0x1e76, 0x0055 }, // Ṷ -> U
{ 0x1e77, 0x0075 }, // ṷ -> u
{ 0x1e78, 0x0055 }, // Ṹ -> U
{ 0x1e79, 0x0075 }, // ṹ -> u
{ 0x1e7a, 0x0055 }, // Ṻ -> U
{ 0x1e7b, 0x0075 }, // ṻ -> u
{ 0x1e7c, 0x0056 }, // Ṽ -> V
{ 0x1e7d, 0x0076 }, // ṽ -> v
{ 0x1e7e, 0x0056 }, // Ṿ -> V
{ 0x1e7f, 0x0076 }, // ṿ -> v
{ 0x1e80, 0x0057 }, // Ẁ -> W
{ 0x1e81, 0x0077 }, // ẁ -> w
{ 0x1e82, 0x0057 }, // Ẃ -> W
{ 0x1e83, 0x0077 }, // ẃ -> w
{ 0x1e84, 0x0057 }, // Ẅ -> W
{ 0x1e85, 0x0077 }, // ẅ -> w
{ 0x1e86, 0x0057 }, // Ẇ -> W
{ 0x1e87, 0x0077 }, // ẇ -> w
{ 0x1e88, 0x0057 }, // Ẉ -> W
{ 0x1e89, 0x0077 }, // ẉ -> w
{ 0x1e8a, 0x0058 }, // Ẋ -> X
{ 0x1e8b, 0x0078 }, // ẋ -> x
{ 0x1e8c, 0x0058 }, // Ẍ -> X
{ 0x1e8d, 0x0078 }, // ẍ -> x
{ 0x1e8e, 0x0059 }, // Ẏ -> Y
{ 0x1e8f, 0x0079 }, // ẏ -> y
{ 0x1e90, 0x005a }, // Ẑ -> Z
{ 0x1e91, 0x007a }, // ẑ -> z
{ 0x1e92, 0x005a }, // Ẓ -> Z
{ 0x1e93, 0x007a }, // ẓ -> z
{ 0x1e94, 0x005a }, // Ẕ -> Z
{ 0x1e95, 0x007a }, // ẕ -> z
{ 0x1e96, 0x0068 }, // ẖ -> h
{ 0x1e97, 0x0074 }, // ẗ -> t
{ 0x1e98, 0x0077 }, // ẘ -> w
{ 0x1e99, 0x0079 }, // ẙ -> y
{ 0x1e9b, 0x017f }, // ẛ -> ſ
{ 0x1ea0, 0x0041 }, // Ạ -> A
{ 0x1ea1, 0x0061 }, // ạ -> a
{ 0x1ea2, 0x0041 }, // Ả -> A
{ 0x1ea3, 0x0061 }, // ả -> a
{ 0x1ea4, 0x0041 }, // Ấ -> A
{ 0x1ea5, 0x0061 }, // ấ -> a
{ 0x1ea6, 0x0041 }, // Ầ -> A
{ 0x1ea7, 0x0061 }, // ầ -> a
{ 0x1ea8, 0x0041 }, // Ẩ -> A
{ 0x1ea9, 0x0061 }, // ẩ -> a
{ 0x1eaa, 0x0041 }, // Ẫ -> A
{ 0x1eab, 0x0061 }, // ẫ -> a
{ 0x1eac, 0x0041 }, // Ậ -> A
{ 0x1ead, 0x0061 }, // ậ -> a
{ 0x1eae, 0x0041 }, // Ắ -> A
{ 0x1eaf, 0x0061 }, // ắ -> a
{ 0x1eb0, 0x0041 }, // Ằ -> A
{ 0x1eb1, 0x0061 }, // ằ -> a
{ 0x1eb2, 0x0041 }, // Ẳ -> A
{ 0x1eb3, 0x0061 }, // ẳ -> a
{ 0x1eb4, 0x0041 }, // Ẵ -> A
{ 0x1eb5, 0x0061 }, // ẵ -> a
{ 0x1eb6, 0x0041 }, // Ặ -> A
{ 0x1eb7, 0x0061 }, // ặ -> a
{ 0x1eb8, 0x0045 }, // Ẹ -> E
{ 0x1eb9, 0x0065 }, // ẹ -> e
{ 0x1eba, 0x0045 }, // Ẻ -> E
{ 0x1ebb, 0x0065 }, // ẻ -> e
{ 0x1ebc, 0x0045 }, // Ẽ -> E
{ 0x1ebd, 0x0065 }, // ẽ -> e
{ 0x1ebe, 0x0045 }, // Ế -> E
{ 0x1ebf, 0x0065 }, // ế -> e
{ 0x1ec0, 0x0045 }, // Ề -> E
{ 0x1ec1, 0x0065 }, // ề -> e
{ 0x1ec2, 0x0045 }, // Ể -> E
{ 0x1ec3, 0x0065 }, // ể -> e
{ 0x1ec4, 0x0045 }, // Ễ -> E
{ 0x1ec5, 0x0065 }, // ễ -> e
{ 0x1ec6, 0x0045 }, // Ệ -> E
{ 0x1ec7, 0x0065 }, // ệ -> e
{ 0x1ec8, 0x0049 }, // Ỉ -> I
{ 0x1ec9, 0x0069 }, // ỉ -> i
{ 0x1eca, 0x0049 }, // Ị -> I
{ 0x1ecb, 0x0069 }, // ị -> i
{ 0x1ecc, 0x004f }, // Ọ -> O
{ 0x1ecd, 0x006f }, // ọ -> o
{ 0x1ece, 0x004f }, // Ỏ -> O
{ 0x1ecf, 0x006f }, // ỏ -> o
{ 0x1ed0, 0x004f }, // Ố -> O
{ 0x1ed1, 0x006f }, // ố -> o
{ 0x1ed2, 0x004f }, // Ồ -> O
{ 0x1ed3, 0x006f }, // ồ -> o
{ 0x1ed4, 0x004f }, // Ổ -> O
{ 0x1ed5, 0x006f }, // ổ -> o
{ 0x1ed6, 0x004f }, // Ỗ -> O
{ 0x1ed7, 0x006f }, // ỗ -> o
{ 0x1ed8, 0x004f }, // Ộ -> O
{ 0x1ed9, 0x006f }, // ộ -> o
{ 0x1eda, 0x004f }, // Ớ -> O
{ 0x1edb, 0x006f }, // ớ -> o
{ 0x1edc, 0x004f }, // Ờ -> O
{ 0x1edd, 0x006f }, // ờ -> o
{ 0x1ede, 0x004f }, // Ở -> O
{ 0x1edf, 0x006f }, // ở -> o
{ 0x1ee0, 0x004f }, // Ỡ -> O
{ 0x1ee1, 0x006f }, // ỡ -> o
{ 0x1ee2, 0x004f }, // Ợ -> O
{ 0x1ee3, 0x006f }, // ợ -> o
{ 0x1ee4, 0x0055 }, // Ụ -> U
{ 0x1ee5, 0x0075 }, // ụ -> u
{ 0x1ee6, 0x0055 }, // Ủ -> U
{ 0x1ee7, 0x0075 }, // ủ -> u
{ 0x1ee8, 0x0055 }, // Ứ -> U
{ 0x1ee9, 0x0075 }, // ứ -> u
{ 0x1eea, 0x0055 }, // Ừ -> U
{ 0x1eeb, 0x0075 }, // ừ -> u
{ 0x1eec, 0x0055 }, // Ử -> U
{ 0x1eed, 0x0075 }, // ử -> u
{ 0x1eee, 0x0055 }, // Ữ -> U
{ 0x1eef, 0x0075 }, // ữ -> u
{ 0x1ef0, 0x0055 }, // Ự -> U
{ 0x1ef1, 0x0075 }, // ự -> u
{ 0x1ef2, 0x0059 }, // Ỳ -> Y
{ 0x1ef3, 0x0079 }, // ỳ -> y
{ 0x1ef4, 0x0059 }, // Ỵ -> Y
{ 0x1ef5, 0x0079 }, // ỵ -> y
{ 0x1ef6, 0x0059 }, // Ỷ -> Y
{ 0x1ef7, 0x0079 }, // ỷ -> y
{ 0x1ef8, 0x0059 }, // Ỹ -> Y
{ 0x1ef9, 0x0079 }, // ỹ -> y
{ 0x1f00, 0x03b1 }, // ἀ -> α
{ 0x1f01, 0x03b1 }, // ἁ -> α
{ 0x1f02, 0x03b1 }, // ἂ -> α
{ 0x1f03, 0x03b1 }, // ἃ -> α
{ 0x1f04, 0x03b1 }, // ἄ -> α
{ 0x1f05, 0x03b1 }, // ἅ -> α
{ 0x1f06, 0x03b1 }, // ἆ -> α
{ 0x1f07, 0x03b1 }, // ἇ -> α
{ 0x1f08, 0x0391 }, // Ἀ -> Α
{ 0x1f09, 0x0391 }, // Ἁ -> Α
{ 0x1f0a, 0x0391 }, // Ἂ -> Α
{ 0x1f0b, 0x0391 }, // Ἃ -> Α
{ 0x1f0c, 0x0391 }, // Ἄ -> Α
{ 0x1f0d, 0x0391 }, // Ἅ -> Α
{ 0x1f0e, 0x0391 }, // Ἆ -> Α
{ 0x1f0f, 0x0391 }, // Ἇ -> Α
{ 0x1f10, 0x03b5 }, // ἐ -> ε
{ 0x1f11, 0x03b5 }, // ἑ -> ε
{ 0x1f12, 0x03b5 }, // ἒ -> ε
{ 0x1f13, 0x03b5 }, // ἓ -> ε
{ 0x1f14, 0x03b5 }, // ἔ -> ε
{ 0x1f15, 0x03b5 }, // ἕ -> ε
{ 0x1f18, 0x0395 }, // Ἐ -> Ε
{ 0x1f19, 0x0395 }, // Ἑ -> Ε
{ 0x1f1a, 0x0395 }, // Ἒ -> Ε
{ 0x1f1b, 0x0395 }, // Ἓ -> Ε
{ 0x1f1c, 0x0395 }, // Ἔ -> Ε
{ 0x1f1d, 0x0395 }, // Ἕ -> Ε
{ 0x1f20, 0x03b7 }, // ἠ -> η
{ 0x1f21, 0x03b7 }, // ἡ -> η
{ 0x1f22, 0x03b7 }, // ἢ -> η
{ 0x1f23, 0x03b7 }, // ἣ -> η
{ 0x1f24, 0x03b7 }, // ἤ -> η
{ 0x1f25, 0x03b7 }, // ἥ -> η
{ 0x1f26, 0x03b7 }, // ἦ -> η
{ 0x1f27, 0x03b7 }, // ἧ -> η
{ 0x1f28, 0x0397 }, // Ἠ -> Η
{ 0x1f29, 0x0397 }, // Ἡ -> Η
{ 0x1f2a, 0x0397 }, // Ἢ -> Η
{ 0x1f2b, 0x0397 }, // Ἣ -> Η
{ 0x1f2c, 0x0397 }, // Ἤ -> Η
{ 0x1f2d, 0x0397 }, // Ἥ -> Η
{ 0x1f2e, 0x0397 }, // Ἦ -> Η
{ 0x1f2f, 0x0397 }, // Ἧ -> Η
{ 0x1f30, 0x03b9 }, // ἰ -> ι
{ 0x1f31, 0x03b9 }, // ἱ -> ι
{ 0x1f32, 0x03b9 }, // ἲ -> ι
{ 0x1f33, 0x03b9 }, // ἳ -> ι
{ 0x1f34, 0x03b9 }, // ἴ -> ι
{ 0x1f35, 0x03b9 }, // ἵ -> ι
{ 0x1f36, 0x03b9 }, // ἶ -> ι
{ 0x1f37, 0x03b9 }, // ἷ -> ι
{ 0x1f38, 0x0399 }, // Ἰ -> Ι
{ 0x1f39, 0x0399 }, // Ἱ -> Ι
{ 0x1f3a, 0x0399 }, // Ἲ -> Ι
{ 0x1f3b, 0x0399 }, // Ἳ -> Ι
{ 0x1f3c, 0x0399 }, // Ἴ -> Ι
{ 0x1f3d, 0x0399 }, // Ἵ -> Ι
{ 0x1f3e, 0x0399 }, // Ἶ -> Ι
{ 0x1f3f, 0x0399 }, // Ἷ -> Ι
{ 0x1f40, 0x03bf }, // ὀ -> ο
{ 0x1f41, 0x03bf }, // ὁ -> ο
{ 0x1f42, 0x03bf }, // ὂ -> ο
{ 0x1f43, 0x03bf }, // ὃ -> ο
{ 0x1f44, 0x03bf }, // ὄ -> ο
{ 0x1f45, 0x03bf }, // ὅ -> ο
{ 0x1f48, 0x039f }, // Ὀ -> Ο
{ 0x1f49, 0x039f }, // Ὁ -> Ο
{ 0x1f4a, 0x039f }, // Ὂ -> Ο
{ 0x1f4b, 0x039f }, // Ὃ -> Ο
{ 0x1f4c, 0x039f }, // Ὄ -> Ο
{ 0x1f4d, 0x039f }, // Ὅ -> Ο
{ 0x1f50, 0x03c5 }, // ὐ -> υ
{ 0x1f51, 0x03c5 }, // ὑ -> υ
{ 0x1f52, 0x03c5 }, // ὒ -> υ
{ 0x1f53, 0x03c5 }, // ὓ -> υ
{ 0x1f54, 0x03c5 }, // ὔ -> υ
{ 0x1f55, 0x03c5 }, // ὕ -> υ
{ 0x1f56, 0x03c5 }, // ὖ -> υ
{ 0x1f57, 0x03c5 }, // ὗ -> υ
{ 0x1f59, 0x03a5 }, // Ὑ -> Υ
{ 0x1f5b, 0x03a5 }, // Ὓ -> Υ
{ 0x1f5d, 0x03a5 }, // Ὕ -> Υ
{ 0x1f5f, 0x03a5 }, // Ὗ -> Υ
{ 0x1f60, 0x03c9 }, // ὠ -> ω
{ 0x1f61, 0x03c9 }, // ὡ -> ω
{ 0x1f62, 0x03c9 }, // ὢ -> ω
{ 0x1f63, 0x03c9 }, // ὣ -> ω
{ 0x1f64, 0x03c9 }, // ὤ -> ω
{ 0x1f65, 0x03c9 }, // ὥ -> ω
{ 0x1f66, 0x03c9 }, // ὦ -> ω
{ 0x1f67, 0x03c9 }, // ὧ -> ω
{ 0x1f68, 0x03a9 }, // Ὠ -> Ω
{ 0x1f69, 0x03a9 }, // Ὡ -> Ω
{ 0x1f6a, 0x03a9 }, // Ὢ -> Ω
{ 0x1f6b, 0x03a9 }, // Ὣ -> Ω
{ 0x1f6c, 0x03a9 }, // Ὤ -> Ω
{ 0x1f6d, 0x03a9 }, // Ὥ -> Ω
{ 0x1f6e, 0x03a9 }, // Ὦ -> Ω
{ 0x1f6f, 0x03a9 }, // Ὧ -> Ω
{ 0x1f70, 0x03b1 }, // ὰ -> α
{ 0x1f71, 0x03b1 }, // ά -> α
{ 0x1f72, 0x03b5 }, // ὲ -> ε
{ 0x1f73, 0x03b5 }, // έ -> ε
{ 0x1f74, 0x03b7 }, // ὴ -> η
{ 0x1f75, 0x03b7 }, // ή -> η
{ 0x1f76, 0x03b9 }, // ὶ -> ι
{ 0x1f77, 0x03b9 }, // ί -> ι
{ 0x1f78, 0x03bf }, // ὸ -> ο
{ 0x1f79, 0x03bf }, // ό -> ο
{ 0x1f7a, 0x03c5 }, // ὺ -> υ
{ 0x1f7b, 0x03c5 }, // ύ -> υ
{ 0x1f7c, 0x03c9 }, // ὼ -> ω
{ 0x1f7d, 0x03c9 }, // ώ -> ω
{ 0x1f80, 0x03b1 }, // ᾀ -> α
{ 0x1f81, 0x03b1 }, // ᾁ -> α
{ 0x1f82, 0x03b1 }, // ᾂ -> α
{ 0x1f83, 0x03b1 }, // ᾃ -> α
{ 0x1f84, 0x03b1 }, // ᾄ -> α
{ 0x1f85, 0x03b1 }, // ᾅ -> α
{ 0x1f86, 0x03b1 }, // ᾆ -> α
{ 0x1f87, 0x03b1 }, // ᾇ -> α
{ 0x1f88, 0x0391 }, // ᾈ -> Α
{ 0x1f89, 0x0391 }, // ᾉ -> Α
{ 0x1f8a, 0x0391 }, // ᾊ -> Α
{ 0x1f8b, 0x0391 }, // ᾋ -> Α
{ 0x1f8c, 0x0391 }, // ᾌ -> Α
{ 0x1f8d, 0x0391 }, // ᾍ -> Α
{ 0x1f8e, 0x0391 }, // ᾎ -> Α
{ 0x1f8f, 0x0391 }, // ᾏ -> Α
{ 0x1f90, 0x03b7 }, // ᾐ -> η
{ 0x1f91, 0x03b7 }, // ᾑ -> η
{ 0x1f92, 0x03b7 }, // ᾒ -> η
{ 0x1f93, 0x03b7 }, // ᾓ -> η
{ 0x1f94, 0x03b7 }, // ᾔ -> η
{ 0x1f95, 0x03b7 }, // ᾕ -> η
{ 0x1f96, 0x03b7 }, // ᾖ -> η
{ 0x1f97, 0x03b7 }, // ᾗ -> η
{ 0x1f98, 0x0397 }, // ᾘ -> Η
{ 0x1f99, 0x0397 }, // ᾙ -> Η
{ 0x1f9a, 0x0397 }, // ᾚ -> Η
{ 0x1f9b, 0x0397 }, // ᾛ -> Η
{ 0x1f9c, 0x0397 }, // ᾜ -> Η
{ 0x1f9d, 0x0397 }, // ᾝ -> Η
{ 0x1f9e, 0x0397 }, // ᾞ -> Η
{ 0x1f9f, 0x0397 }, // ᾟ -> Η
{ 0x1fa0, 0x03c9 }, // ᾠ -> ω
{ 0x1fa1, 0x03c9 }, // ᾡ -> ω
{ 0x1fa2, 0x03c9 }, // ᾢ -> ω
{ 0x1fa3, 0x03c9 }, // ᾣ -> ω
{ 0x1fa4, 0x03c9 }, // ᾤ -> ω
{ 0x1fa5, 0x03c9 }, // ᾥ -> ω
{ 0x1fa6, 0x03c9 }, // ᾦ -> ω
{ 0x1fa7, 0x03c9 }, // ᾧ -> ω
{ 0x1fa8, 0x03a9 }, // ᾨ -> Ω
{ 0x1fa9, 0x03a9 }, // ᾩ -> Ω
{ 0x1faa, 0x03a9 }, // ᾪ -> Ω
{ 0x1fab, 0x03a9 }, // ᾫ -> Ω
{ 0x1fac, 0x03a9 }, // ᾬ -> Ω
{ 0x1fad, 0x03a9 }, // ᾭ -> Ω
{ 0x1fae, 0x03a9 }, // ᾮ -> Ω
{ 0x1faf, 0x03a9 }, // ᾯ -> Ω
{ 0x1fb0, 0x03b1 }, // ᾰ -> α
{ 0x1fb1, 0x03b1 }, // ᾱ -> α
{ 0x1fb2, 0x03b1 }, // ᾲ -> α
{ 0x1fb3, 0x03b1 }, // ᾳ -> α
{ 0x1fb4, 0x03b1 }, // ᾴ -> α
{ 0x1fb6, 0x03b1 }, // ᾶ -> α
{ 0x1fb7, 0x03b1 }, // ᾷ -> α
{ 0x1fb8, 0x0391 }, // Ᾰ -> Α
{ 0x1fb9, 0x0391 }, // Ᾱ -> Α
{ 0x1fba, 0x0391 }, // Ὰ -> Α
{ 0x1fbb, 0x0391 }, // Ά -> Α
{ 0x1fbc, 0x0391 }, // ᾼ -> Α
{ 0x1fbe, 0x03b9 }, // ι -> ι
{ 0x1fc1, 0x00a8 }, // ῁ -> ¨
{ 0x1fc2, 0x03b7 }, // ῂ -> η
{ 0x1fc3, 0x03b7 }, // ῃ -> η
{ 0x1fc4, 0x03b7 }, // ῄ -> η
{ 0x1fc6, 0x03b7 }, // ῆ -> η
{ 0x1fc7, 0x03b7 }, // ῇ -> η
{ 0x1fc8, 0x0395 }, // Ὲ -> Ε
{ 0x1fc9, 0x0395 }, // Έ -> Ε
{ 0x1fca, 0x0397 }, // Ὴ -> Η
{ 0x1fcb, 0x0397 }, // Ή -> Η
{ 0x1fcc, 0x0397 }, // ῌ -> Η
{ 0x1fcd, 0x1fbf }, // ῍ -> ᾿
{ 0x1fce, 0x1fbf }, // ῎ -> ᾿
{ 0x1fcf, 0x1fbf }, // ῏ -> ᾿
{ 0x1fd0, 0x03b9 }, // ῐ -> ι
{ 0x1fd1, 0x03b9 }, // ῑ -> ι
{ 0x1fd2, 0x03b9 }, // ῒ -> ι
{ 0x1fd3, 0x03b9 }, // ΐ -> ι
{ 0x1fd6, 0x03b9 }, // ῖ -> ι
{ 0x1fd7, 0x03b9 }, // ῗ -> ι
{ 0x1fd8, 0x0399 }, // Ῐ -> Ι
{ 0x1fd9, 0x0399 }, // Ῑ -> Ι
{ 0x1fda, 0x0399 }, // Ὶ -> Ι
{ 0x1fdb, 0x0399 }, // Ί -> Ι
{ 0x1fdd, 0x1ffe }, // ῝ -> ῾
{ 0x1fde, 0x1ffe }, // ῞ -> ῾
{ 0x1fdf, 0x1ffe }, // ῟ -> ῾
{ 0x1fe0, 0x03c5 }, // ῠ -> υ
{ 0x1fe1, 0x03c5 }, // ῡ -> υ
{ 0x1fe2, 0x03c5 }, // ῢ -> υ
{ 0x1fe3, 0x03c5 }, // ΰ -> υ
{ 0x1fe4, 0x03c1 }, // ῤ -> ρ
{ 0x1fe5, 0x03c1 }, // ῥ -> ρ
{ 0x1fe6, 0x03c5 }, // ῦ -> υ
{ 0x1fe7, 0x03c5 }, // ῧ -> υ
{ 0x1fe8, 0x03a5 }, // Ῠ -> Υ
{ 0x1fe9, 0x03a5 }, // Ῡ -> Υ
{ 0x1fea, 0x03a5 }, // Ὺ -> Υ
{ 0x1feb, 0x03a5 }, // Ύ -> Υ
{ 0x1fec, 0x03a1 }, // Ῥ -> Ρ
{ 0x1fed, 0x00a8 }, // ῭ -> ¨
{ 0x1fee, 0x00a8 }, // ΅ -> ¨
{ 0x1fef, 0x0060 }, // ` -> `
{ 0x1ff2, 0x03c9 }, // ῲ -> ω
{ 0x1ff3, 0x03c9 }, // ῳ -> ω
{ 0x1ff4, 0x03c9 }, // ῴ -> ω
{ 0x1ff6, 0x03c9 }, // ῶ -> ω
{ 0x1ff7, 0x03c9 }, // ῷ -> ω
{ 0x1ff8, 0x039f }, // Ὸ -> Ο
{ 0x1ff9, 0x039f }, // Ό -> Ο
{ 0x1ffa, 0x03a9 }, // Ὼ -> Ω
{ 0x1ffb, 0x03a9 }, // Ώ -> Ω
{ 0x1ffc, 0x03a9 }, // ῼ -> Ω
{ 0x1ffd, 0x00b4 }, // ´ -> ´
{ 0x2000, 0x2002 }, // ->
{ 0x2001, 0x2003 }, // ->
{ 0x2126, 0x03a9 }, // Ω -> Ω
{ 0x212a, 0x004b }, // K -> K
{ 0x212b, 0x0041 }, // Å -> A
{ 0x219a, 0x2190 }, // ↚ -> ←
{ 0x219b, 0x2192 }, // ↛ -> →
{ 0x21ae, 0x2194 }, // ↮ -> ↔
{ 0x21cd, 0x21d0 }, // ⇍ -> ⇐
{ 0x21ce, 0x21d4 }, // ⇎ -> ⇔
{ 0x21cf, 0x21d2 }, // ⇏ -> ⇒
{ 0x2204, 0x2203 }, // ∄ -> ∃
{ 0x2209, 0x2208 }, // ∉ -> ∈
{ 0x220c, 0x220b }, // ∌ -> ∋
{ 0x2224, 0x2223 }, // ∤ -> ∣
{ 0x2226, 0x2225 }, // ∦ -> ∥
{ 0x2241, 0x223c }, // ≁ -> ∼
{ 0x2244, 0x2243 }, // ≄ -> ≃
{ 0x2247, 0x2245 }, // ≇ -> ≅
{ 0x2249, 0x2248 }, // ≉ -> ≈
{ 0x2260, 0x003d }, // ≠ -> =
{ 0x2262, 0x2261 }, // ≢ -> ≡
{ 0x226d, 0x224d }, // ≭ -> ≍
{ 0x226e, 0x003c }, // ≮ -> <
{ 0x226f, 0x003e }, // ≯ -> >
{ 0x2270, 0x2264 }, // ≰ -> ≤
{ 0x2271, 0x2265 }, // ≱ -> ≥
{ 0x2274, 0x2272 }, // ≴ -> ≲
{ 0x2275, 0x2273 }, // ≵ -> ≳
{ 0x2278, 0x2276 }, // ≸ -> ≶
{ 0x2279, 0x2277 }, // ≹ -> ≷
{ 0x2280, 0x227a }, // ⊀ -> ≺
{ 0x2281, 0x227b }, // ⊁ -> ≻
{ 0x2284, 0x2282 }, // ⊄ -> ⊂
{ 0x2285, 0x2283 }, // ⊅ -> ⊃
{ 0x2288, 0x2286 }, // ⊈ -> ⊆
{ 0x2289, 0x2287 }, // ⊉ -> ⊇
{ 0x22ac, 0x22a2 }, // ⊬ -> ⊢
{ 0x22ad, 0x22a8 }, // ⊭ -> ⊨
{ 0x22ae, 0x22a9 }, // ⊮ -> ⊩
{ 0x22af, 0x22ab }, // ⊯ -> ⊫
{ 0x22e0, 0x227c }, // ⋠ -> ≼
{ 0x22e1, 0x227d }, // ⋡ -> ≽
{ 0x22e2, 0x2291 }, // ⋢ -> ⊑
{ 0x22e3, 0x2292 }, // ⋣ -> ⊒
{ 0x22ea, 0x22b2 }, // ⋪ -> ⊲
{ 0x22eb, 0x22b3 }, // ⋫ -> ⊳
{ 0x22ec, 0x22b4 }, // ⋬ -> ⊴
{ 0x22ed, 0x22b5 }, // ⋭ -> ⊵
{ 0x2329, 0x3008 }, // 〈 -> 〈
{ 0x232a, 0x3009 }, // 〉 -> 〉
{ 0x2adc, 0x2add }, // ⫝̸ -> ⫝
{ 0x304c, 0x304b }, // が -> か
{ 0x304e, 0x304d }, // ぎ -> き
{ 0x3050, 0x304f }, // ぐ -> く
{ 0x3052, 0x3051 }, // げ -> け
{ 0x3054, 0x3053 }, // ご -> こ
{ 0x3056, 0x3055 }, // ざ -> さ
{ 0x3058, 0x3057 }, // じ -> し
{ 0x305a, 0x3059 }, // ず -> す
{ 0x305c, 0x305b }, // ぜ -> せ
{ 0x305e, 0x305d }, // ぞ -> そ
{ 0x3060, 0x305f }, // だ -> た
{ 0x3062, 0x3061 }, // ぢ -> ち
{ 0x3065, 0x3064 }, // づ -> つ
{ 0x3067, 0x3066 }, // で -> て
{ 0x3069, 0x3068 }, // ど -> と
{ 0x3070, 0x306f }, // ば -> は
{ 0x3071, 0x306f }, // ぱ -> は
{ 0x3073, 0x3072 }, // び -> ひ
{ 0x3074, 0x3072 }, // ぴ -> ひ
{ 0x3076, 0x3075 }, // ぶ -> ふ
{ 0x3077, 0x3075 }, // ぷ -> ふ
{ 0x3079, 0x3078 }, // べ -> へ
{ 0x307a, 0x3078 }, // ぺ -> へ
{ 0x307c, 0x307b }, // ぼ -> ほ
{ 0x307d, 0x307b }, // ぽ -> ほ
{ 0x3094, 0x3046 }, // ゔ -> う
{ 0x309e, 0x309d }, // ゞ -> ゝ
{ 0x30ac, 0x30ab }, // ガ -> カ
{ 0x30ae, 0x30ad }, // ギ -> キ
{ 0x30b0, 0x30af }, // グ -> ク
{ 0x30b2, 0x30b1 }, // ゲ -> ケ
{ 0x30b4, 0x30b3 }, // ゴ -> コ
{ 0x30b6, 0x30b5 }, // ザ -> サ
{ 0x30b8, 0x30b7 }, // ジ -> シ
{ 0x30ba, 0x30b9 }, // ズ -> ス
{ 0x30bc, 0x30bb }, // ゼ -> セ
{ 0x30be, 0x30bd }, // ゾ -> ソ
{ 0x30c0, 0x30bf }, // ダ -> タ
{ 0x30c2, 0x30c1 }, // ヂ -> チ
{ 0x30c5, 0x30c4 }, // ヅ -> ツ
{ 0x30c7, 0x30c6 }, // デ -> テ
{ 0x30c9, 0x30c8 }, // ド -> ト
{ 0x30d0, 0x30cf }, // バ -> ハ
{ 0x30d1, 0x30cf }, // パ -> ハ
{ 0x30d3, 0x30d2 }, // ビ -> ヒ
{ 0x30d4, 0x30d2 }, // ピ -> ヒ
{ 0x30d6, 0x30d5 }, // ブ -> フ
{ 0x30d7, 0x30d5 }, // プ -> フ
{ 0x30d9, 0x30d8 }, // ベ -> ヘ
{ 0x30da, 0x30d8 }, // ペ -> ヘ
{ 0x30dc, 0x30db }, // ボ -> ホ
{ 0x30dd, 0x30db }, // ポ -> ホ
{ 0x30f4, 0x30a6 }, // ヴ -> ウ
{ 0x30f7, 0x30ef }, // ヷ -> ワ
{ 0x30f8, 0x30f0 }, // ヸ -> ヰ
{ 0x30f9, 0x30f1 }, // ヹ -> ヱ
{ 0x30fa, 0x30f2 }, // ヺ -> ヲ
{ 0x30fe, 0x30fd }, // ヾ -> ヽ
};
onboard-1.4.1/Onboard/pypredict/lm/lm_dynamic_impl.h 0000644 0001750 0001750 00000031614 13051012134 022671 0 ustar frafu frafu 0000000 0000000 /*
* Copyright © 2010, 2012-2014 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 .
*/
#include
#include
#include
#include
#include
#include
//------------------------------------------------------------------------
// NGramTrie - root node of the ngram trie
//------------------------------------------------------------------------
// Lookup node or create it if it doesn't exist
template
BaseNode* NGramTrie::
add_node(const WordId* wids, int n)
{
BaseNode* node = this;
BaseNode* parent = NULL;
TNODE* grand_parent = NULL;
int parent_index = 0;
int grand_parent_index = 0;
for (int i=0; i(parent);
parent = node;
grand_parent_index = parent_index;
node = get_child(parent, i, wid, parent_index);
if (!node)
{
if (i == order-1)
{
TBEFORELASTNODE* p = static_cast(parent);
// check the available space for LastNodes
int size = p->children.size();
int old_capacity = p->children.capacity();
if (size >= old_capacity) // no space for another TLASTNODE?
{
// grow the memory block of the parent node
int new_capacity = p->children.capacity(size + 1);
int old_bytes = sizeof(TBEFORELASTNODE) +
old_capacity*sizeof(TLASTNODE);
int new_bytes = sizeof(TBEFORELASTNODE) +
new_capacity*sizeof(TLASTNODE);
TBEFORELASTNODE* pnew = (TBEFORELASTNODE*) MemAlloc(new_bytes);
if (!pnew)
return NULL;
// copy the data over, no need for constructor calls
memcpy(pnew, p, old_bytes);
// replace grand_parent pointer
ASSERT(p == grand_parent->children[grand_parent_index]);
grand_parent->children[grand_parent_index] = pnew;
MemFree(p);
p = pnew;
}
// add the new child node
node = p->add_child(wid);
}
else
if (i == order-2)
{
int bytes = sizeof(TBEFORELASTNODE) +
inplace_vector::capacity(0)*sizeof(TLASTNODE);
TBEFORELASTNODE* nd = (TBEFORELASTNODE*)MemAlloc(bytes);
//node = new TBEFORELASTNODE(wid);
if (!nd)
return NULL;
node = new(nd) TBEFORELASTNODE(wid);
static_cast(parent)->add_child(node);
}
else
{
TNODE* nd = (TNODE*)MemAlloc(sizeof(TNODE));
if (!nd)
return NULL;
node = new(nd) TNODE(wid);
static_cast(parent)->add_child(node);
}
// Create only a single node per call. For a valid model we
// expect count_ngram() to be called extra for each node in every
// path, in particular for all unigrams. Use learn_tokens() to
// enforce this. Only then is the model ready for use with
// predict().
break;
}
}
return node;
}
template
void NGramTrie::
get_probs_witten_bell_i(const std::vector& history,
const std::vector& words,
std::vector& vp,
int num_word_types)
{
int i,j;
int n = history.size() + 1;
int size = words.size(); // number of candidate words
std::vector vc(size); // vector of counts, reused for order 1..n
// order 0
vp.resize(size);
fill(vp.begin(), vp.end(), 1.0/num_word_types); // uniform distribution
// order 1..n
for(j=0; j h(history.begin()+(n-j-1), history.end()); // tmp history
BaseNode* hnode = get_node(h);
if (hnode)
{
int N1prx = get_N1prx(hnode, j); // number of word types following the history
if (!N1prx) // break early, don't reset probabilities to 0
break; // for unknown histories
// total number of occurences of the history
int cs = sum_child_counts(hnode, j);
if (cs)
{
// get ngram counts
fill(vc.begin(), vc.end(), 0);
int num_children = get_num_children(hnode, j);
for(i=0; iword_id); // word_indices have to be sorted by index
if (index >= 0)
vc[index] = child->get_count();
}
double l1 = N1prx / (N1prx + float(cs)); // normalization factor
// 1 - lambda
for(i=0; i
void NGramTrie::
get_probs_abs_disc_i(const std::vector& history,
const std::vector& words,
std::vector& vp,
int num_word_types,
const std::vector& Ds)
{
int i,j;
int n = history.size() + 1;
int size = words.size(); // number of candidate words
std::vector vc(size); // vector of counts, reused for order 1..n
// order 0
vp.resize(size);
fill(vp.begin(), vp.end(), 1.0/num_word_types); // uniform distribution
// order 1..n
for(j=0; j h(history.begin()+(n-j-1), history.end()); // tmp history
BaseNode* hnode = get_node(h);
if (hnode)
{
int N1prx = get_N1prx(hnode, j); // number of word types following the history
if (!N1prx) // break early, don't reset probabilities to 0
break; // for unknown histories
// total number of occurences of the history
int cs = sum_child_counts(hnode, j);
if (cs)
{
// get ngram counts
fill(vc.begin(), vc.end(), 0);
int num_children = get_num_children(hnode, j);
for(i=0; iword_id); // word_indices have to be sorted by index
if (index >= 0)
vc[index] = child->get_count();
}
double D = Ds[j];
double l1 = D / float(cs) * N1prx; // normalization factor
// 1 - lambda
for(i=0; i
void _DynamicModel::set_order(int n)
{
if(n < 2) // use UnigramModel for order 1
n = 2;
n1s = std::vector(n, 0);
n2s = std::vector(n, 0);
Ds = std::vector(n, 0);
ngrams.set_order(n);
NGramModel::set_order(n); // calls clear()
}
template
void _DynamicModel::clear()
{
ngrams.clear();
DynamicModelBase::clear(); // clears dictionary
}
// Add increment to the count of the given ngram.
// Unknown words will be added to the dictionary and
// unknown ngrams will cause new trie nodes to be created as needed.
template
BaseNode* _DynamicModel::count_ngram(const wchar_t* const* ngram, int n,
int increment, bool allow_new_words)
{
std::vector wids(n);
if (dictionary.query_add_words(ngram, n, wids, allow_new_words))
return count_ngram(&wids[0], n, increment);
return NULL;
}
// Add increment to the count of the given ngram.
// Unknown words will be added to the dictionary first and
// unknown ngrams will cause new trie nodes to be created as needed.
template
BaseNode* _DynamicModel::count_ngram(const WordId* wids, int n,
int increment)
{
int i;
// get/add node for ngram
BaseNode* node = ngrams.add_node(wids, n);
if (!node)
return NULL;
// remove old state
if (node->count == 1)
n1s[n-1]--;
if (node->count == 2)
n2s[n-1]--;
int count = increment_node_count(node, wids, n, increment);
// add new state
if (node->count == 1)
n1s[n-1]++;
if (node->count == 2)
n2s[n-1]++;
// estimate discounting parameters for absolute discounting, kneser-ney
for (i = 0; i < order; i++)
{
double D;
int n1 = n1s[i];
int n2 = n2s[i];
if (n1 == 0 || n2 == 0)
D = 0.1; // training corpus too small, take a guess
else
// deleted estimation, Ney, Essen, and Kneser 1994
D = n1 / (n1 + 2.0*n2);
ASSERT(0 <= D and D <= 1.0);
//D = 0.1;
Ds[i] = D;
}
return count >= 0 ? node : NULL;
}
// Return the number of occurences of the given ngram
template
int _DynamicModel::get_ngram_count(const wchar_t* const* ngram, int n)
{
BaseNode* node = get_ngram_node(ngram, n);
return (node ? node->get_count() : 0);
}
// Calculate a vector of probabilities for the ngrams formed
// by history + word[i], for all i.
// input: constant history and a vector of candidate words
// output: vector of probabilities, one value per candidate word
template
void _DynamicModel::get_probs(const std::vector& history,
const std::vector& words,
std::vector& probabilities)
{
// pad/cut history so it's always of length order-1
int n = std::min((int)history.size(), order-1);
std::vector h(order-1, UNKNOWN_WORD_ID);
copy_backward(history.end()-n, history.end(), h.end());
#ifndef NDEBUG
for (int i=0; i
LMError _DynamicModel::
write_arpa_ngrams(FILE* f)
{
int i;
for (i=0; i wids;
for (typename TNGRAMS::iterator it = ngrams.begin(); *it; it++)
{
if (it.get_level() == i+1)
{
it.get_ngram(wids);
LMError error = write_arpa_ngram(f, *it, wids);
if (error)
return error;
}
}
}
return ERR_NONE;
}
onboard-1.4.1/Onboard/pypredict/lm/lm_unigram.cpp 0000644 0001750 0001750 00000003646 13051012134 022225 0 ustar frafu frafu 0000000 0000000 /*
* Copyright © 2013 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 .
*/
#include "lm_unigram.h"
#include
using namespace std;
//------------------------------------------------------------------------
// UnigramModel
//------------------------------------------------------------------------
// Calculate a vector of probabilities for the ngrams formed
// by history + word[i], for all i.
// Input: constant history and a vector of candidate words
// Output: vector of probabilities, one value per candidate word
void UnigramModel::get_probs(const std::vector& history,
const std::vector& words,
std::vector& probabilities)
{
std::vector& vp = probabilities;
int size = words.size(); // number of candidate words
int num_word_types = get_num_word_types();
int cs = accumulate(m_counts.begin(), m_counts.end(), 0); // total number of occurencess
if (cs)
{
vp.resize(size);
for(int i=0; i