linuxOS_AP05/debian/test/usr/lib/python3/dist-packages/Onboard/KeyboardPopups.py

719 lines
23 KiB
Python
Raw Permalink Normal View History

2025-09-26 01:40:02 +00:00
# -*- 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()