719 lines
23 KiB
Python
719 lines
23 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
||
|
|
|
||
|
|
# Copyright © 2013-2016 marmuta <marmvta@gmail.com>
|
||
|
|
#
|
||
|
|
# This file is part of Onboard.
|
||
|
|
#
|
||
|
|
# Onboard is free software; you can redistribute it and/or modify
|
||
|
|
# it under the terms of the GNU General Public License as published by
|
||
|
|
# the Free Software Foundation; either version 3 of the License, or
|
||
|
|
# (at your option) any later version.
|
||
|
|
#
|
||
|
|
# Onboard is distributed in the hope that it will be useful,
|
||
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
|
# GNU General Public License for more details.
|
||
|
|
#
|
||
|
|
# You should have received a copy of the GNU General Public License
|
||
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
|
||
|
|
""" GTK keyboard widget """
|
||
|
|
|
||
|
|
from __future__ import division, print_function, unicode_literals
|
||
|
|
|
||
|
|
import cairo
|
||
|
|
|
||
|
|
from Onboard.Version import require_gi_versions
|
||
|
|
require_gi_versions()
|
||
|
|
from gi.repository import Gdk, Gtk, Pango, PangoCairo
|
||
|
|
|
||
|
|
from Onboard.utils import Rect, roundrect_arc, Version
|
||
|
|
from Onboard.Timer import Timer
|
||
|
|
from Onboard.WindowUtils import limit_window_position, \
|
||
|
|
get_monitor_rects, \
|
||
|
|
canvas_to_root_window_rect, \
|
||
|
|
get_monitor_dimensions, \
|
||
|
|
WindowRectTracker, \
|
||
|
|
gtk_has_resize_grip_support
|
||
|
|
from Onboard.TouchInput import TouchInput
|
||
|
|
from Onboard import KeyCommon
|
||
|
|
from Onboard.Layout import LayoutRoot, LayoutPanel
|
||
|
|
from Onboard.LayoutView import LayoutView
|
||
|
|
from Onboard.KeyGtk import RectKey
|
||
|
|
from Onboard.KeyCommon import ImageSlot
|
||
|
|
|
||
|
|
import Onboard.osk as osk
|
||
|
|
|
||
|
|
### Logging ###
|
||
|
|
import logging
|
||
|
|
_logger = logging.getLogger(__name__)
|
||
|
|
###############
|
||
|
|
|
||
|
|
### 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 TouchFeedback:
|
||
|
|
""" Display magnified labels as touch feedback """
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self._key_feedback_popup_pool = []
|
||
|
|
self._visible_key_feedback_popups = {}
|
||
|
|
|
||
|
|
def show(self, key, view):
|
||
|
|
if not key in self._visible_key_feedback_popups: # not already shown?
|
||
|
|
r = key.get_canvas_border_rect()
|
||
|
|
root_rect = canvas_to_root_window_rect(view, r)
|
||
|
|
toplevel = view.get_toplevel()
|
||
|
|
|
||
|
|
popup = self._get_free_key_feedback_popup()
|
||
|
|
if popup is None:
|
||
|
|
popup = LabelPopup()
|
||
|
|
popup.set_transient_for(toplevel)
|
||
|
|
self._key_feedback_popup_pool.append(popup)
|
||
|
|
popup.realize()
|
||
|
|
|
||
|
|
# Set window size
|
||
|
|
w, h = self._get_popup_size(popup)
|
||
|
|
popup.set_default_size(w, h)
|
||
|
|
popup.resize(w, h)
|
||
|
|
|
||
|
|
popup.set_key(key)
|
||
|
|
popup.position_at(root_rect.x + root_rect.w * 0.5,
|
||
|
|
root_rect.y, 0.5, 1)
|
||
|
|
popup.supports_alpha = view.supports_alpha
|
||
|
|
if popup.supports_alpha:
|
||
|
|
popup.set_opacity(toplevel.get_opacity())
|
||
|
|
popup.show_all()
|
||
|
|
|
||
|
|
self._visible_key_feedback_popups[key] = popup
|
||
|
|
|
||
|
|
def hide(self, key = None):
|
||
|
|
keys = [key] if key else list(self._visible_key_feedback_popups.keys())
|
||
|
|
for _key in keys:
|
||
|
|
popup = self._visible_key_feedback_popups.get(_key)
|
||
|
|
if popup:
|
||
|
|
popup.hide()
|
||
|
|
popup.set_key(None)
|
||
|
|
del self._visible_key_feedback_popups[_key]
|
||
|
|
|
||
|
|
def raise_all(self):
|
||
|
|
for key, popup in self._visible_key_feedback_popups.items():
|
||
|
|
win = popup.get_window()
|
||
|
|
if win:
|
||
|
|
win.raise_()
|
||
|
|
|
||
|
|
def _get_free_key_feedback_popup(self):
|
||
|
|
""" Get a currently unused one from the pool of popups. """
|
||
|
|
for popup in self._key_feedback_popup_pool:
|
||
|
|
if not popup.get_key():
|
||
|
|
return popup
|
||
|
|
return None
|
||
|
|
|
||
|
|
def _get_popup_size(self, window):
|
||
|
|
DEFAULT_POPUP_SIZE_MM = 18.0
|
||
|
|
MAX_POPUP_SIZE_PX = 120.0 # fall-back if phys. monitor size unavail.
|
||
|
|
|
||
|
|
gdk_win = window.get_window()
|
||
|
|
|
||
|
|
w = config.keyboard.touch_feedback_size
|
||
|
|
if w == 0:
|
||
|
|
sz, sz_mm = get_monitor_dimensions(window)
|
||
|
|
if sz[0] > 0 and sz_mm[0] > 0:
|
||
|
|
default_size_mm = DEFAULT_POPUP_SIZE_MM
|
||
|
|
|
||
|
|
# scale for hires displays
|
||
|
|
if gdk_win:
|
||
|
|
try:
|
||
|
|
default_size_mm *= gdk_win.get_scale_factor() # from Gdk 3.10
|
||
|
|
except AttributeError:
|
||
|
|
pass
|
||
|
|
|
||
|
|
w = sz[0] * default_size_mm / sz_mm[0]
|
||
|
|
elif sz[0] > 0:
|
||
|
|
w = min(sz[0] / 12.0, MAX_POPUP_SIZE_PX)
|
||
|
|
else: # LP #1633284
|
||
|
|
w = MAX_POPUP_SIZE_PX
|
||
|
|
|
||
|
|
return w, w * (1.0 + LabelPopup.ARROW_HEIGHT)
|
||
|
|
|
||
|
|
|
||
|
|
class KeyboardPopup(WindowRectTracker, Gtk.Window):
|
||
|
|
""" Abstract base class for popups. """
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self._opacity = 1.0
|
||
|
|
|
||
|
|
WindowRectTracker.__init__(self)
|
||
|
|
|
||
|
|
args = {
|
||
|
|
"skip_taskbar_hint" : True,
|
||
|
|
"skip_pager_hint" : True,
|
||
|
|
"urgency_hint" : False,
|
||
|
|
"decorated" : False,
|
||
|
|
"accept_focus" : False,
|
||
|
|
"opacity" : 1.0,
|
||
|
|
"app_paintable" : True,
|
||
|
|
}
|
||
|
|
if gtk_has_resize_grip_support():
|
||
|
|
args["has_resize_grip"] = False
|
||
|
|
|
||
|
|
Gtk.Window.__init__(self, **args)
|
||
|
|
|
||
|
|
self.set_keep_above(True)
|
||
|
|
|
||
|
|
# In Precise, directly drawing on the top level window has no effect.
|
||
|
|
# The Cairo target surface is correctly rendered, but somehow it
|
||
|
|
# doesn't become visible. Compositing or not doesn't matter.
|
||
|
|
# It's most likely an old issue with Gtk/Gdk. Later releases like
|
||
|
|
# Trusty, Vivid are unaffected.
|
||
|
|
# -> Create a widget we can successfully draw on anywhere.
|
||
|
|
self.drawing_area = Gtk.DrawingArea()
|
||
|
|
self.add(self.drawing_area)
|
||
|
|
self.drawing_area.connect("draw", self.on_draw)
|
||
|
|
|
||
|
|
# use transparency if available
|
||
|
|
screen = Gdk.Screen.get_default()
|
||
|
|
visual = screen.get_rgba_visual()
|
||
|
|
self.supports_alpha = False
|
||
|
|
if visual:
|
||
|
|
self.set_visual(visual)
|
||
|
|
self.drawing_area.set_visual(visual)
|
||
|
|
|
||
|
|
# Somehow Gtk 3.4 still needs these now deprecated calls
|
||
|
|
# for LayoutPopups, even though IconPalette and LabelPopups
|
||
|
|
# don't. Otherwise there will be a white background.
|
||
|
|
gtk_version = Version(Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION)
|
||
|
|
if gtk_version < Version(3, 18): # Xenial doesn't need them
|
||
|
|
self.override_background_color(
|
||
|
|
Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 0))
|
||
|
|
self.drawing_area.override_background_color(
|
||
|
|
Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 0))
|
||
|
|
self.supports_alpha = True
|
||
|
|
|
||
|
|
def set_opacity(self, opacity):
|
||
|
|
""" Override deprecated Gtk function of the same name """
|
||
|
|
self._opacity = opacity
|
||
|
|
|
||
|
|
def get_opacity(self):
|
||
|
|
""" Override deprecated Gtk function of the same name """
|
||
|
|
return self._opacity
|
||
|
|
|
||
|
|
def on_draw(self, widget, context):
|
||
|
|
raise NotImplementedError()
|
||
|
|
|
||
|
|
def position_at(self, x, y, x_align, y_align):
|
||
|
|
"""
|
||
|
|
Align the window with the given point.
|
||
|
|
x, y in root window coordinates.
|
||
|
|
"""
|
||
|
|
rect = Rect.from_position_size(self.get_position(), self.get_size())
|
||
|
|
rect = rect.align_at_point(x, y, x_align, y_align)
|
||
|
|
rect = self.limit_to_workarea(rect)
|
||
|
|
x, y = rect.get_position()
|
||
|
|
|
||
|
|
self.move(x, y)
|
||
|
|
|
||
|
|
def limit_to_workarea(self, rect):
|
||
|
|
visible_rect = Rect(0, 0, rect.w, rect.h)
|
||
|
|
|
||
|
|
x, y = limit_window_position(rect.x, rect.y, visible_rect,
|
||
|
|
get_monitor_rects(self.get_screen()))
|
||
|
|
return Rect(x, y, rect.w, rect.h)
|
||
|
|
|
||
|
|
|
||
|
|
class LabelPopup(KeyboardPopup):
|
||
|
|
""" Ephemeral popup displaying a key label without user interaction. """
|
||
|
|
|
||
|
|
ARROW_HEIGHT = 0.13
|
||
|
|
ARROW_WIDTH = 0.3
|
||
|
|
LABEL_MARGIN = 0.1
|
||
|
|
|
||
|
|
_pango_layout = None
|
||
|
|
_osk_util = osk.Util()
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
KeyboardPopup.__init__(self)
|
||
|
|
self._key = None
|
||
|
|
self.connect("realize", self._on_realize_event)
|
||
|
|
|
||
|
|
def _on_realize_event(self, user_data):
|
||
|
|
self.set_override_redirect(True)
|
||
|
|
|
||
|
|
# set minimal input shape for the popup to become click-through
|
||
|
|
win = self.get_window()
|
||
|
|
self._osk_util.set_input_rect(win, 0, 0, 1, 1)
|
||
|
|
|
||
|
|
def on_draw(self, widget, context):
|
||
|
|
if not LabelPopup._pango_layout:
|
||
|
|
LabelPopup._pango_layout = Pango.Layout(context=Gdk.pango_context_get())
|
||
|
|
|
||
|
|
rect = Rect(0, 0, self.get_allocated_width(),
|
||
|
|
self.get_allocated_height())
|
||
|
|
content_rect = Rect(rect.x, rect.y, rect.w,
|
||
|
|
rect.h - rect.h * self.ARROW_HEIGHT)
|
||
|
|
arrow_rect = Rect(rect.x, content_rect.bottom(), rect.w,
|
||
|
|
rect.h * self.ARROW_HEIGHT) \
|
||
|
|
.deflate((rect.w - rect.w * self.ARROW_WIDTH) / 2.0, 0)
|
||
|
|
|
||
|
|
label_rect = content_rect.deflate(rect.w * self.LABEL_MARGIN)
|
||
|
|
|
||
|
|
# background
|
||
|
|
fill = self._key.get_fill_color()
|
||
|
|
context.save()
|
||
|
|
context.set_operator(cairo.OPERATOR_CLEAR)
|
||
|
|
context.paint()
|
||
|
|
context.restore()
|
||
|
|
|
||
|
|
context.push_group()
|
||
|
|
|
||
|
|
context.set_source_rgba(*fill)
|
||
|
|
roundrect_arc(context, content_rect, config.CORNER_RADIUS)
|
||
|
|
context.fill()
|
||
|
|
|
||
|
|
l, t, r, b = arrow_rect.to_extents()
|
||
|
|
t -= 1
|
||
|
|
context.move_to(l, t)
|
||
|
|
context.line_to(r, t)
|
||
|
|
context.line_to((l+r) / 2, b)
|
||
|
|
context.fill()
|
||
|
|
|
||
|
|
# draw label/image
|
||
|
|
label_color = self._key.get_label_color()
|
||
|
|
pixbuf = self._key.get_image(label_rect.w, label_rect.h)
|
||
|
|
if pixbuf:
|
||
|
|
pixbuf.draw(context, label_rect, label_color)
|
||
|
|
else:
|
||
|
|
label = self._key.get_label()
|
||
|
|
if label:
|
||
|
|
if label == " ":
|
||
|
|
label = "␣"
|
||
|
|
self._draw_text(context, label, label_rect, label_color)
|
||
|
|
|
||
|
|
context.pop_group_to_source()
|
||
|
|
context.paint_with_alpha(self._opacity)
|
||
|
|
|
||
|
|
def _draw_text(self, context, text, rect, rgba):
|
||
|
|
layout = self._pango_layout
|
||
|
|
layout.set_text(text, -1)
|
||
|
|
|
||
|
|
# find text extents
|
||
|
|
font_description = Pango.FontDescription( \
|
||
|
|
config.theme_settings.key_label_font)
|
||
|
|
base_extents = self._calc_base_layout_extents(layout, font_description)
|
||
|
|
|
||
|
|
# scale label to the available rect
|
||
|
|
font_size = self._calc_font_size(rect, base_extents)
|
||
|
|
font_description.set_size(max(1, font_size))
|
||
|
|
layout.set_font_description(font_description)
|
||
|
|
|
||
|
|
# center
|
||
|
|
w, h = layout.get_size()
|
||
|
|
w /= Pango.SCALE
|
||
|
|
h /= Pango.SCALE
|
||
|
|
offset = rect.align_rect(Rect(0, 0, w, h)).get_position()
|
||
|
|
|
||
|
|
# draw
|
||
|
|
context.move_to(*offset)
|
||
|
|
context.set_source_rgba(*rgba)
|
||
|
|
PangoCairo.show_layout(context, layout)
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _calc_font_size(rect, base_extents):
|
||
|
|
size_for_maximum_width = rect.w / base_extents[0]
|
||
|
|
size_for_maximum_height = rect.h / base_extents[1]
|
||
|
|
if size_for_maximum_width < size_for_maximum_height:
|
||
|
|
return int(size_for_maximum_width)
|
||
|
|
else:
|
||
|
|
return int(size_for_maximum_height)
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _calc_base_layout_extents(layout, font_description):
|
||
|
|
BASE_FONTDESCRIPTION_SIZE = 10000000
|
||
|
|
|
||
|
|
font_description.set_size(BASE_FONTDESCRIPTION_SIZE)
|
||
|
|
layout.set_font_description(font_description)
|
||
|
|
|
||
|
|
w, h = layout.get_size() # In Pango units
|
||
|
|
w = w or 1.0
|
||
|
|
h = h or 1.0
|
||
|
|
extents = (w / (Pango.SCALE * BASE_FONTDESCRIPTION_SIZE),
|
||
|
|
h / (Pango.SCALE * BASE_FONTDESCRIPTION_SIZE))
|
||
|
|
return extents
|
||
|
|
|
||
|
|
def get_key(self):
|
||
|
|
return self._key
|
||
|
|
|
||
|
|
def set_key(self, key):
|
||
|
|
self._key = key
|
||
|
|
|
||
|
|
|
||
|
|
class LayoutPopup(KeyboardPopup, LayoutView, TouchInput):
|
||
|
|
""" Popup showing a (sub-)layout tree. """
|
||
|
|
|
||
|
|
IDLE_CLOSE_DELAY = 0 # seconds of inactivity until window closes
|
||
|
|
|
||
|
|
def __init__(self, keyboard, notify_done_callback):
|
||
|
|
self._layout = None
|
||
|
|
self._notify_done_callback = notify_done_callback
|
||
|
|
self._drag_selected = False # grazed by the pointer?
|
||
|
|
|
||
|
|
KeyboardPopup.__init__(self)
|
||
|
|
LayoutView.__init__(self, keyboard)
|
||
|
|
TouchInput.__init__(self)
|
||
|
|
|
||
|
|
self.connect("destroy", self._on_destroy_event)
|
||
|
|
|
||
|
|
self._close_timer = Timer()
|
||
|
|
self.start_close_timer()
|
||
|
|
|
||
|
|
def cleanup(self):
|
||
|
|
self.stop_close_timer()
|
||
|
|
|
||
|
|
# fix label popup staying visible on double click
|
||
|
|
self.keyboard.hide_touch_feedback()
|
||
|
|
|
||
|
|
LayoutView.cleanup(self) # deregister from keyboard
|
||
|
|
|
||
|
|
def get_toplevel(self):
|
||
|
|
return self
|
||
|
|
|
||
|
|
def set_layout(self, layout, frame_width):
|
||
|
|
self._layout = layout
|
||
|
|
self._frame_width = frame_width
|
||
|
|
|
||
|
|
self.update_labels()
|
||
|
|
|
||
|
|
# set window size
|
||
|
|
layout_canvas_rect = layout.get_canvas_border_rect()
|
||
|
|
canvas_rect = layout_canvas_rect.inflate(frame_width)
|
||
|
|
w, h = canvas_rect.get_size()
|
||
|
|
self.set_default_size(w + 1, h + 1)
|
||
|
|
|
||
|
|
def get_layout(self):
|
||
|
|
return self._layout
|
||
|
|
|
||
|
|
def get_frame_width(self):
|
||
|
|
return self._frame_width
|
||
|
|
|
||
|
|
def got_motion(self):
|
||
|
|
""" Has the pointer ever entered the popup? """
|
||
|
|
return self._drag_selected
|
||
|
|
|
||
|
|
def handle_realize_event(self):
|
||
|
|
self.set_override_redirect(True)
|
||
|
|
super(LayoutPopup, self).handle_realize_event()
|
||
|
|
|
||
|
|
def _on_destroy_event(self, user_data):
|
||
|
|
self.cleanup()
|
||
|
|
|
||
|
|
def on_enter_notify(self, widget, event):
|
||
|
|
self.stop_close_timer()
|
||
|
|
|
||
|
|
def on_leave_notify(self, widget, event):
|
||
|
|
self.start_close_timer()
|
||
|
|
|
||
|
|
def on_input_sequence_begin(self, sequence):
|
||
|
|
self.stop_close_timer()
|
||
|
|
key = self.get_key_at_location(sequence.point)
|
||
|
|
if key:
|
||
|
|
sequence.active_key = key
|
||
|
|
self.keyboard.key_down(key, self, sequence)
|
||
|
|
|
||
|
|
def on_input_sequence_update(self, sequence):
|
||
|
|
if sequence.state & BUTTON123_MASK:
|
||
|
|
key = self.get_key_at_location(sequence.point)
|
||
|
|
|
||
|
|
# drag-select new active key
|
||
|
|
active_key = sequence.active_key
|
||
|
|
if not active_key is key and \
|
||
|
|
(active_key is None or not active_key.activated):
|
||
|
|
sequence.active_key = key
|
||
|
|
self.keyboard.key_up(active_key, self, sequence, False)
|
||
|
|
self.keyboard.key_down(key, self, sequence, False)
|
||
|
|
self._drag_selected = True
|
||
|
|
|
||
|
|
def on_input_sequence_end(self, sequence):
|
||
|
|
key = sequence.active_key
|
||
|
|
if key:
|
||
|
|
keyboard = self.keyboard
|
||
|
|
keyboard.key_up(key, self, sequence)
|
||
|
|
|
||
|
|
if key and \
|
||
|
|
not self._drag_selected:
|
||
|
|
Timer(config.UNPRESS_DELAY, self.close_window)
|
||
|
|
else:
|
||
|
|
self.close_window()
|
||
|
|
|
||
|
|
def on_draw(self, widget, context):
|
||
|
|
context.push_group()
|
||
|
|
|
||
|
|
LayoutView.draw(self, widget, context)
|
||
|
|
|
||
|
|
context.pop_group_to_source()
|
||
|
|
context.paint_with_alpha(self._opacity)
|
||
|
|
|
||
|
|
def draw_window_frame(self, context, lod):
|
||
|
|
corner_radius = config.CORNER_RADIUS
|
||
|
|
border_rgba = self.get_popup_window_rgba("border")
|
||
|
|
alpha = border_rgba[3]
|
||
|
|
|
||
|
|
colors = [
|
||
|
|
[[0.5, 0.5, 0.5, alpha], 0 , 1],
|
||
|
|
[border_rgba, 1.5, 2.0],
|
||
|
|
]
|
||
|
|
|
||
|
|
rect = Rect(0, 0, self.get_allocated_width(),
|
||
|
|
self.get_allocated_height())
|
||
|
|
|
||
|
|
for rgba, pos, width in colors:
|
||
|
|
r = rect.deflate(width)
|
||
|
|
roundrect_arc(context, r, corner_radius)
|
||
|
|
context.set_line_width(width)
|
||
|
|
context.set_source_rgba(*rgba)
|
||
|
|
context.stroke()
|
||
|
|
|
||
|
|
def close_window(self):
|
||
|
|
self._notify_done_callback()
|
||
|
|
|
||
|
|
def start_close_timer(self):
|
||
|
|
if self.IDLE_CLOSE_DELAY:
|
||
|
|
self._close_timer.start(self.IDLE_CLOSE_DELAY, self.close_window)
|
||
|
|
|
||
|
|
def stop_close_timer(self):
|
||
|
|
self._close_timer.stop()
|
||
|
|
|
||
|
|
|
||
|
|
class LayoutBuilder:
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def build(source_key, color_scheme, layout):
|
||
|
|
context = source_key.context
|
||
|
|
|
||
|
|
frame_width = LayoutBuilder._calc_frame_width(context)
|
||
|
|
|
||
|
|
layout = LayoutRoot(layout)
|
||
|
|
layout.update_log_rect()
|
||
|
|
log_rect = layout.get_border_rect()
|
||
|
|
canvas_rect = Rect(frame_width, frame_width,
|
||
|
|
log_rect.w * context.scale_log_to_canvas_x(1.0),
|
||
|
|
log_rect.h * context.scale_log_to_canvas_y(1.0))
|
||
|
|
layout.fit_inside_canvas(canvas_rect)
|
||
|
|
|
||
|
|
return layout, frame_width
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _calc_frame_width(context):
|
||
|
|
# calculate border around the layout
|
||
|
|
canvas_border = context.scale_log_to_canvas((1, 1))
|
||
|
|
return config.POPUP_FRAME_WIDTH + min(canvas_border)
|
||
|
|
|
||
|
|
|
||
|
|
class LayoutBuilderKeySequence(LayoutBuilder):
|
||
|
|
|
||
|
|
MAX_KEY_COLUMNS = 8 # max number of keys in one row
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def build(source_key, color_scheme, key_sequence):
|
||
|
|
# parse sequence into lines
|
||
|
|
lines, ncolumns = \
|
||
|
|
LayoutBuilderKeySequence ._layout_sequence(key_sequence)
|
||
|
|
return LayoutBuilderKeySequence._create_layout(source_key,
|
||
|
|
color_scheme,
|
||
|
|
lines, ncolumns)
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _create_layout(source_key, color_scheme, lines, ncolumns):
|
||
|
|
context = source_key.context
|
||
|
|
frame_width = LayoutBuilderKeySequence._calc_frame_width(context)
|
||
|
|
|
||
|
|
nrows = len(lines)
|
||
|
|
spacing = (1, 1)
|
||
|
|
|
||
|
|
# calc canvas size
|
||
|
|
rect = source_key.get_canvas_border_rect()
|
||
|
|
layout_canvas_rect = Rect(frame_width, frame_width,
|
||
|
|
rect.w * ncolumns + spacing[0] * (ncolumns - 1),
|
||
|
|
rect.h * nrows + spacing[1] * (nrows - 1))
|
||
|
|
|
||
|
|
# subdivide into logical rectangles for the keys
|
||
|
|
layout_rect = context.canvas_to_log_rect(layout_canvas_rect)
|
||
|
|
key_rects = layout_rect.subdivide(ncolumns, nrows, *spacing)
|
||
|
|
|
||
|
|
# create keys, slots for empty labels are skipped
|
||
|
|
keys = []
|
||
|
|
for i, line in enumerate(lines):
|
||
|
|
for j, item in enumerate(line):
|
||
|
|
slot = i * ncolumns + j
|
||
|
|
if item:
|
||
|
|
# control item?
|
||
|
|
key = item
|
||
|
|
key.set_border_rect(key_rects[slot])
|
||
|
|
key.group = "alternatives"
|
||
|
|
key.color_scheme = color_scheme
|
||
|
|
keys.append(key)
|
||
|
|
|
||
|
|
item = LayoutPanel()
|
||
|
|
item.border = 0
|
||
|
|
item.set_items(keys)
|
||
|
|
layout = LayoutRoot(item)
|
||
|
|
|
||
|
|
layout.fit_inside_canvas(layout_canvas_rect)
|
||
|
|
|
||
|
|
return layout, frame_width
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _layout_sequence(sequence):
|
||
|
|
"""
|
||
|
|
Split sequence into lines.
|
||
|
|
"""
|
||
|
|
max_columns = LayoutBuilderAlternatives.MAX_KEY_COLUMNS
|
||
|
|
min_columns = max_columns // 2
|
||
|
|
add_close = False
|
||
|
|
fill_gaps = True
|
||
|
|
|
||
|
|
# find the number of columns with the best packing,
|
||
|
|
# i.e. the least number of empty slots.
|
||
|
|
n = len(sequence)
|
||
|
|
if add_close:
|
||
|
|
n += 1 # +1 for close button
|
||
|
|
max_mod = 0
|
||
|
|
ncolumns = max_columns
|
||
|
|
for i in range(max_columns, min_columns, -1):
|
||
|
|
m = n % i
|
||
|
|
if m == 0:
|
||
|
|
max_mod = m
|
||
|
|
ncolumns = i
|
||
|
|
break
|
||
|
|
if max_mod < m:
|
||
|
|
max_mod = m
|
||
|
|
ncolumns = i
|
||
|
|
|
||
|
|
# limit to len for the single row case
|
||
|
|
ncolumns = min(n, ncolumns)
|
||
|
|
|
||
|
|
# cut the input into lines of the newly found optimal length
|
||
|
|
lines = []
|
||
|
|
line = []
|
||
|
|
column = 0
|
||
|
|
for item in sequence:
|
||
|
|
line.append(item)
|
||
|
|
column += 1
|
||
|
|
if column >= ncolumns:
|
||
|
|
lines.append(line)
|
||
|
|
line = []
|
||
|
|
column = 0
|
||
|
|
|
||
|
|
# append close button
|
||
|
|
if add_close:
|
||
|
|
n = len(line)
|
||
|
|
line.extend([None]*(ncolumns - (n+1)))
|
||
|
|
|
||
|
|
key = RectKey("_close_")
|
||
|
|
key.labels = {}
|
||
|
|
key.image_filenames = {ImageSlot.NORMAL : "close.svg"}
|
||
|
|
key.type = KeyCommon.BUTTON_TYPE
|
||
|
|
line.append(key)
|
||
|
|
|
||
|
|
# fill empty slots with dummy buttons
|
||
|
|
if fill_gaps:
|
||
|
|
n = len(line)
|
||
|
|
if n:
|
||
|
|
for i in range(ncolumns - n):
|
||
|
|
key = RectKey("_dummy_")
|
||
|
|
key.sensitive = False
|
||
|
|
line.append(key)
|
||
|
|
|
||
|
|
if line:
|
||
|
|
lines.append(line)
|
||
|
|
|
||
|
|
return lines, ncolumns
|
||
|
|
|
||
|
|
|
||
|
|
class LayoutBuilderAlternatives(LayoutBuilderKeySequence):
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def build(source_key, color_scheme, alternatives):
|
||
|
|
key_sequence = []
|
||
|
|
for i, label in enumerate(alternatives):
|
||
|
|
key = RectKey("_alternative" + str(i))
|
||
|
|
key.type = KeyCommon.CHAR_TYPE
|
||
|
|
key.labels = {0: label}
|
||
|
|
key.code = label[0]
|
||
|
|
key_sequence.append(key)
|
||
|
|
|
||
|
|
return LayoutBuilderKeySequence.build(source_key, color_scheme,
|
||
|
|
key_sequence)
|
||
|
|
|
||
|
|
|
||
|
|
class PendingSeparatorPopup(KeyboardPopup):
|
||
|
|
""" Ephemeral popup displaying the pending word separator. """
|
||
|
|
|
||
|
|
_osk_util = osk.Util()
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
KeyboardPopup.__init__(self)
|
||
|
|
self.connect("realize", self._on_realize_event)
|
||
|
|
self._visible = False
|
||
|
|
|
||
|
|
def show_at(self, view, character_rect):
|
||
|
|
toplevel = view.get_toplevel()
|
||
|
|
|
||
|
|
self.set_transient_for(toplevel)
|
||
|
|
self.realize()
|
||
|
|
|
||
|
|
x, y, w, h = character_rect
|
||
|
|
self.set_default_size(w, h)
|
||
|
|
self.resize(w, h)
|
||
|
|
self.move(x, y)
|
||
|
|
|
||
|
|
self.supports_alpha = view.supports_alpha
|
||
|
|
if self.supports_alpha:
|
||
|
|
self.set_opacity(toplevel.get_opacity())
|
||
|
|
self.show_all()
|
||
|
|
self._visible = True
|
||
|
|
|
||
|
|
def hide(self):
|
||
|
|
KeyboardPopup.hide(self)
|
||
|
|
self._visible = False
|
||
|
|
|
||
|
|
def is_visible(self):
|
||
|
|
return self._visible
|
||
|
|
|
||
|
|
def _on_realize_event(self, user_data):
|
||
|
|
self.set_override_redirect(True)
|
||
|
|
|
||
|
|
# set minimal input shape for the popup to become click-through
|
||
|
|
win = self.get_window()
|
||
|
|
self._osk_util.set_input_rect(win, 0, 0, 1, 1)
|
||
|
|
|
||
|
|
def on_draw(self, widget, cr):
|
||
|
|
rect = Rect(0, 0,
|
||
|
|
self.get_allocated_width(), self.get_allocated_height())
|
||
|
|
|
||
|
|
fill_rgba = (1.0, 0.5, 0.5, 0.2)
|
||
|
|
stroke_rgba = (1.0, 0.5, 0.5, 0.7)
|
||
|
|
|
||
|
|
cr.set_source_rgba(*fill_rgba)
|
||
|
|
cr.rectangle(*rect)
|
||
|
|
cr.fill()
|
||
|
|
|
||
|
|
r = rect
|
||
|
|
top = r.y + r.h * 0.75
|
||
|
|
bottom = r.bottom()
|
||
|
|
cr.set_source_rgba(*stroke_rgba)
|
||
|
|
cr.set_line_width(max(1, r.h / 6))
|
||
|
|
cr.move_to(r.left(), top)
|
||
|
|
cr.line_to(r.left(), bottom)
|
||
|
|
cr.line_to(r.right(), bottom)
|
||
|
|
cr.line_to(r.right(), top)
|
||
|
|
cr.stroke()
|
||
|
|
|