linuxOS_AP05/debian/test/usr/lib/python3/dist-packages/Onboard/LayoutView.py
2025-09-26 09:40:02 +08:00

797 lines
26 KiB
Python

# -*- coding: utf-8 -*-
# Copyright © 2012-2017 marmuta <marmvta@gmail.com>
#
# This file is part of Onboard.
#
# Onboard is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Onboard is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" 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