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

1257 lines
43 KiB
Python

# -*- coding: UTF-8 -*-
# Copyright © 2007 Martin Böhme <martin.bohm@kubuntu.org>
# Copyright © 2009 Chris Jones <tortoise@tortuga>
# Copyright © 2010 Francesco Fumanti <francesco.fumanti@gmx.net>
# Copyright © 2011 Alan Bell <alanbell@ubuntu.com>
# Copyright © 2012 Gerd Kohlberger <lowfi@chello.at>
# Copyright © 2009-2014, 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/>.
from __future__ import division, print_function, unicode_literals
from math import pi, sin, cos, sqrt
import cairo
from Onboard.Version import require_gi_versions
require_gi_versions()
from gi.repository import GLib, Gdk, Pango, PangoCairo, GdkPixbuf
from Onboard.KeyCommon import *
from Onboard.WindowUtils import DwellProgress
from Onboard.utils import brighten, unicode_str, \
gradient_line, drop_shadow, \
roundrect_curve, rounded_path, \
rounded_polygon_path_to_cairo_path
### Logging ###
import logging
_logger = logging.getLogger("KeyGTK")
###############
### Config Singleton ###
from Onboard.Config import Config
config = Config()
########################
PangoUnscale = 1.0 / Pango.SCALE
class Key(KeyCommon):
_pango_layouts = None
_label_extents = None # resolution independent size {mod_mask: (w, h)}
_popup_indicator = "" # font dependent popup indicator (ellipsis)
_shadow_steps = 0
_shadow_alpha = 0
_shadow_presets = ((1, 0.015), (4, 0.005)) # quality presets (steps, alpha)
def __init__(self):
KeyCommon.__init__(self)
self._label_extents = {}
def get_best_font_size(self):
"""
Get the maximum font possible that would not cause the label to
overflow the boundaries of the key.
"""
raise NotImplementedError()
@staticmethod
def reset_pango_layout():
Key._pango_layouts = None
@staticmethod
def get_pango_layout(text, font_size, slot = 0):
# work around memory leak (gnome #599730)
if Key._pango_layouts is None:
# use PangoCairo.create_layout once it works with gi (pango >= 1.29.1)
#Key._pango_layouts = PangoCairo.create_layout(context)
Key._pango_layouts = (
Pango.Layout(context = Gdk.pango_context_get()),
Pango.Layout(context = Gdk.pango_context_get()),
Pango.Layout(context = Gdk.pango_context_get()))
layout = Key._pango_layouts[slot]
Key.prepare_pango_layout(layout, text, font_size)
return layout
@staticmethod
def prepare_pango_layout(layout, text, font_size):
if text is None:
text = ""
layout.set_text(text, -1)
layout.set_width(-1) # no wrapping, ellipsization
font_description = Pango.FontDescription(config.theme_settings.key_label_font)
font_description.set_size(max(1, font_size))
layout.set_font_description(font_description)
@classmethod
def set_shadow_quality(_class, quality):
if quality is None:
quality = 1
_class._shadow_steps, _class._shadow_alpha = \
_class._shadow_presets[quality]
class RectKey(Key, RectKeyCommon, DwellProgress):
_image_pixbuf = None
_requested_image_size = None
_shadow_surface = None
def __init__(self, id = "", border_rect = None):
Key.__init__(self)
RectKeyCommon.__init__(self, id, border_rect)
self._key_surfaces = {}
def is_key(self):
""" Is this a key item? """
return True
def invalidate_caches(self):
"""
Clear buffered patterns, e.g. after resizing, change of settings...
"""
self.invalidate_key()
self.invalidate_shadow()
def invalidate_key(self):
self._key_surfaces = {}
def invalidate_image(self):
"""
Images only have to be expicitely cleared when the
window_scaling_factor changes.
"""
self._image_pixbuf = {}
self._requested_image_size = {}
def invalidate_shadow(self):
self._shadow_surface = None
def set_border_rect(self, rect):
"""
The expand-corrections button moves around a lot.
Be sure to keep its image surfaces updated.
"""
if rect != self.get_border_rect():
super(RectKey, self).set_border_rect(rect)
self.invalidate_caches()
def draw_cached(self, cr):
key = (self.label, self.font_size >> 8)
entry = self._key_surfaces.get(key)
if entry is None:
if self.font_size:
entry = self._create_key_surface(cr)
self._key_surfaces[key] = entry
if entry:
surface, rect = entry
cr.set_source_surface(surface, rect.x, rect.y)
cr.paint()
def _create_key_surface(self, base_context):
rect = self.get_canvas_rect()
clip_rect = rect.inflate(*self.get_extra_render_size()).int()
# create caching surface
target = base_context.get_target()
surface = target.create_similar(cairo.CONTENT_COLOR_ALPHA,
clip_rect.w, clip_rect.h)
cr = cairo.Context(surface)
cr.save()
cr.translate(-clip_rect.x, -clip_rect.y)
self.draw(cr)
cr.restore()
Gdk.flush() # else artefacts in labels and images
# on Nexus 7, Raring
return surface, clip_rect
def draw(self, cr, lod = LOD.FULL):
self.draw_geometry(cr, lod)
self.draw_image(cr, lod)
self.draw_label(cr, lod)
def draw_geometry(self, cr, lod):
if not self.show_face and not self.show_border:
return
if lod == LOD.FULL and self.show_border:
scale = self.get_stroke_width()
if scale:
root = self.get_layout_root()
t = root.context.scale_log_to_canvas((1.0, 1.0))
line_width = (t[0] + t[1]) / 2.4
line_width = min(line_width, 3.0) * scale
line_width = max(line_width, 1.0)
else:
line_width = 0
else:
line_width = 0
fill = self.get_fill_color()
key_style = self.get_style()
if key_style == "flat":
self.draw_flat_key(cr, fill, line_width)
elif key_style == "gradient":
self.draw_gradient_key(cr, fill, line_width, lod)
elif key_style == "dish":
self.draw_dish_key(cr, fill, line_width, lod)
def draw_flat_key(self, cr, fill, line_width):
self._build_canvas_path(cr)
if self.show_face:
cr.set_source_rgba(*fill)
if line_width:
cr.fill_preserve()
else:
cr.fill()
if line_width:
cr.set_source_rgba(*self.get_stroke_color())
cr.set_line_width(line_width)
cr.stroke()
def draw_gradient_key(self, cr, fill, line_width, lod):
# simple gradients for fill and stroke
fill_gradient = config.theme_settings.key_fill_gradient / 100.0
stroke_gradient = self.get_stroke_gradient()
alpha = self.get_gradient_angle()
rect = self.get_canvas_rect()
self._build_canvas_path(cr, rect)
gline = gradient_line(rect, alpha)
# fill
if self.show_face:
if fill_gradient and lod:
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)
cr.set_source (pat)
else: # take gradient from color scheme (not implemented)
cr.set_source_rgba(*fill)
if self.show_border:
cr.fill_preserve()
else:
cr.fill()
# stroke
if self.show_border:
if stroke_gradient:
if lod:
stroke = fill
pat = cairo.LinearGradient (*gline)
rgba = brighten(+stroke_gradient*.5, *stroke)
pat.add_color_stop_rgba(0, *rgba)
rgba = brighten(-stroke_gradient*.5, *stroke)
pat.add_color_stop_rgba(1, *rgba)
cr.set_source (pat)
else:
cr.set_source_rgba(*fill)
else:
cr.set_source_rgba(*self.get_stroke_color())
cr.set_line_width(line_width)
cr.stroke()
def draw_dish_key(self, cr, fill, line_width, lod):
canvas_rect = self.get_canvas_rect()
if self.geometry:
geometry = self.geometry
else:
geometry = KeyGeometry.from_rect(self.get_border_rect())
size_scale_x, size_scale_y = geometry.scale_log_to_size((1.0, 1.0))
# compensate for smaller size due to missing stroke
canvas_rect = canvas_rect.inflate(1.0)
# parameters for the base path
base_rgba = brighten(-0.200, *fill)
stroke_gradient = self.get_stroke_gradient()
light_dir = self.get_light_direction() - pi * 0.5 # 0 = light from top
lightx = cos(light_dir)
lighty = sin(light_dir)
key_offset_x, key_offset_y, key_size_x, key_size_y = \
self.get_key_offset_size(geometry)
radius_pct = max(config.theme_settings.roundrect_radius, 2)
radius_pct = max(radius_pct, 2) # too much +-1 fudging for square corners
chamfer_size = self.get_chamfer_size()
chamfer_size = (self.context.scale_log_to_canvas_x(chamfer_size) +
self.context.scale_log_to_canvas_y(chamfer_size)) * 0.5
# parameters for the top path, key face
stroke_width = self.get_stroke_width()
key_offset_top_y = key_offset_y - \
config.DISH_KEY_Y_OFFSET * stroke_width
border = config.DISH_KEY_BORDER
scale_top_x = 1.0 - (border[0] * stroke_width * size_scale_x * 2.0)
scale_top_y = 1.0 - (border[1] * stroke_width * size_scale_y * 2.0)
key_size_top_x = key_size_x * scale_top_x
key_size_top_y = key_size_y * scale_top_y
chamfer_size_top = chamfer_size * (scale_top_x + scale_top_y) * 0.5
# realize all paths we're going to use
polygons, polygon_paths = \
self.get_canvas_polygons(geometry,
key_offset_x, key_offset_y,
key_size_x, key_size_y,
radius_pct, chamfer_size)
polygons_top, polygon_paths_top = \
self.get_canvas_polygons(geometry,
key_offset_x, key_offset_top_y,
key_size_top_x - size_scale_x,
key_size_top_y - size_scale_y,
radius_pct, chamfer_size_top)
polygons_top1, polygon_paths_top1 = \
self.get_canvas_polygons(geometry,
key_offset_x, key_offset_top_y,
key_size_top_x, key_size_top_y,
radius_pct, chamfer_size_top)
# draw key border
if self.show_border:
if not lod:
cr.set_source_rgba(*base_rgba)
for path in polygon_paths:
rounded_polygon_path_to_cairo_path(cr, path)
cr.fill()
else:
for ipg, polygon in enumerate(polygons):
polygon_top = polygons_top[ipg]
path = polygon_paths[ipg]
path_top = polygon_paths_top[ipg]
self._draw_dish_key_border(cr, path, path_top,
polygon, polygon_top,
base_rgba, stroke_gradient,
lightx, lighty)
# Draw the key face, the smaller top rectangle.
if self.show_face:
if not lod:
cr.set_source_rgba(*fill)
else:
# Simulate the concave key dish with a gradient that has
# a sligthly brighter middle section.
if self.id == "SPCE":
angle = pi / 2.0 # space has a convex top
else:
angle = 0.0 # all others are concave
fill_gradient = config.theme_settings.key_fill_gradient / 100.0
dark_rgba = brighten(-fill_gradient*.5, *fill)
bright_rgba = brighten(+fill_gradient*.5, *fill)
gline = gradient_line(canvas_rect, angle)
pat = cairo.LinearGradient (*gline)
pat.add_color_stop_rgba(0.0, *dark_rgba)
pat.add_color_stop_rgba(0.5, *bright_rgba)
pat.add_color_stop_rgba(1.0, *dark_rgba)
cr.set_source (pat)
for path in polygon_paths_top1:
rounded_polygon_path_to_cairo_path(cr, path)
cr.fill()
def _draw_dish_key_border(self, cr, path, path_top,
polygon, polygon_top,
base_rgba, stroke_gradient, lightx, lighty):
n = len(polygon)
m = len(path)
# Lambert lighting
edge_colors = []
for i in range(0, n, 2):
x0 = polygon[i]
y0 = polygon[i+1]
if i < n-2:
x1 = polygon[i+2]
y1 = polygon[i+3]
else:
x1 = polygon[0]
y1 = polygon[1]
nx = y1 - y0
ny = -(x1 - x0)
ln = sqrt(nx*nx + ny*ny)
I = (nx * lightx + ny * lighty) / ln \
* stroke_gradient * 0.8 \
if ln else 0.0
edge_colors.append(brighten(I, *base_rgba))
# draw border sections
edge = 0
for i in range(0, m-2, 2):
# get path points
i1 = i + 1
i2 = i + 2
if i2 >= m:
i2 -= m
i3 = i + 3
if i3 >= m:
i3 = 1
p0 = path[i]
p0x = p0[0]
p0y = p0[1]
p1 = path[i1]
p1x = p1[0]
p1y = p1[1]
p2 = path[i2]
p2x = p2[0]
p2y = p2[1]
p3 = path[i3]
p3x = p3[0]
p3y = p3[1]
p = path_top[i]
ptop0x = p[0]
ptop0y = p[1]
p = path_top[i1]
ptop1x = p[0]
ptop1y = p[1]
p = path_top[i2]
ptop2x = p[0]
ptop2y = p[1]
# get polygon points, only to
# fill in gaps at concave corners.
j0 = edge*2
j1 = j0 + 2
if j1 >= n:
j1 -= n
j2 = j0 + 4
if j2 >= n:
j2 -= n
ptopax = polygon_top[j0]
ptopay = polygon_top[j0 + 1]
ptopbx = polygon_top[j1]
ptopby = polygon_top[j1 + 1]
ptopcx = polygon_top[j2]
ptopcy = polygon_top[j2 + 1]
vax = ptopbx - ptopax
vay = ptopby - ptopay
nbx = ptopcy - ptopby
nby = -(ptopcx - ptopbx)
concave = vax*nbx + vay*nby < 0.0
# Fake Gouraud shading: draw a gradient between mid points
# of the lines connecting the base with the top path.
pat = cairo.LinearGradient((p1x + ptop1x) * 0.5,
(p1y + ptop1y) * 0.5,
(p2x + ptop2x) * 0.5,
(p2y + ptop2y) * 0.5)
edge1 = (edge + 1) % len(edge_colors)
pat.add_color_stop_rgba(0.0, *edge_colors[edge])
pat.add_color_stop_rgba(1.0, *edge_colors[edge1])
cr.set_source (pat)
# Draw corners and edges with enough overlap to avoid
# artefacts at touching line boundaries.
cr.move_to(p0x, p0y)
cr.line_to(p1x, p1y)
cr.curve_to(p2[2], p2[3], p2[4], p2[5], p2[0], p2[1])
cr.line_to(p3x, p3y)
cr.line_to(ptop2x, ptop2y)
if concave:
cr.line_to(ptopbx, ptopby)
cr.line_to(ptop1x, ptop1y)
cr.line_to(ptop0x, ptop0y)
cr.close_path()
cr.fill()
edge += 1
def get_label_runs(self):
runs = []
log_rect = self.get_label_rect()
canvas_rect = self.context.log_to_canvas_rect(log_rect)
# secondary label
label = self.get_secondary_label()
if label and \
len(label) == 1 and \
config.keyboard.show_secondary_labels:
font_size = self.font_size * 0.5
layout = self.get_pango_layout(label, font_size, 1)
src_size = layout.get_size()
src_size = (src_size[0] * PangoUnscale, src_size[1] * PangoUnscale)
xalign, yalign = self.align_secondary_label(src_size,
(canvas_rect.w, canvas_rect.h))
x = int(canvas_rect.x + xalign)
y = int(canvas_rect.y + yalign)
rgba = self.get_secondary_label_color()
runs.append((layout, x, y, rgba))
# popup indicator
if not self.popup_id is None and \
not config.xid_mode:
label = self._get_popup_indicator()
font_size = self.font_size
layout = self.get_pango_layout(label, font_size, 2)
src_size = layout.get_size()
src_size = (src_size[0] * PangoUnscale, src_size[1] * PangoUnscale)
xalign, yalign = self.align_popup_indicator(src_size,
(canvas_rect.w, canvas_rect.h))
x = int(canvas_rect.x + xalign)
y = int(canvas_rect.y + yalign)
rgba = self.get_secondary_label_color()
runs.append((layout, x, y, rgba))
# main label
label = self.get_label()
if label:
font_size = self.font_size
layout = self.get_pango_layout(label, font_size, 0)
src_size = layout.get_size()
src_size = (src_size[0] * PangoUnscale, src_size[1] * PangoUnscale)
xalign, yalign = self.align_label(src_size,
(canvas_rect.w, canvas_rect.h))
x = int(canvas_rect.x + xalign)
y = int(canvas_rect.y + yalign)
rgba = self.get_label_color()
runs.append((layout, x, y, rgba))
return runs
def _get_popup_indicator(self):
"""
Find the shortest ellipsis possible with the current font.
The font is assumed to never change during the livetime of the key.
"""
result = self._popup_indicator
if not result:
labels = ("", "...") # label candidates
BASE_FONTDESCRIPTION_SIZE = 10000000
wmin = None
result = ""
for label in labels:
layout = self.get_pango_layout(label, BASE_FONTDESCRIPTION_SIZE, 2)
w = layout.get_size()[0]
if wmin is None or w < wmin:
wmin = w
result = label
self._popup_indicator = result
return result
def draw_label(self, context, lod):
# Skip cairo errors when drawing labels with font size 0
# This may happen for hidden keys and keys with bad size groups.
if self.font_size == 0 or not self.show_label:
return
runs = self.get_label_runs()
if not runs:
return
fill = self.get_fill_color()
for dx, dy, lum, last in self._label_iterations(lod):
# draw dwell progress after fake emboss, before final image
if last and self.is_dwelling():
DwellProgress.draw(self, context,
self.get_dwell_progress_canvas_rect(),
self.get_dwell_progress_color())
for layout, x, y, rgba in runs:
if lum:
rgba = brighten(lum, *fill) # darker
context.move_to(x + dx, y + dy)
context.set_source_rgba(*rgba)
PangoCairo.show_layout(context, layout)
def draw_image(self, context, lod):
"""
Draws the key's optional image.
Fixme: merge with draw_label, can't do this for 0.99 because
the Gdk.flush() workaround on the nexus 7 might fail.
"""
if not self.image_filenames or not self.show_image:
return
log_rect = self.get_label_rect()
rect = self.context.log_to_canvas_rect(log_rect)
if rect.w < 1 or rect.h < 1:
return
pixbuf = self.get_image(rect.w, rect.h)
if not pixbuf:
return
src_size = (pixbuf.get_width(), pixbuf.get_height())
xalign, yalign = self.align_label(src_size, (rect.w, rect.h))
label_rgba = self.get_label_color()
fill = self.get_fill_color()
for dx, dy, lum, last in self._label_iterations(lod):
# draw dwell progress after fake emboss, before final image
if last and self.is_dwelling():
DwellProgress.draw(self, context,
self.get_dwell_progress_canvas_rect(),
self.get_dwell_progress_color())
if lum:
rgba = brighten(lum, *fill) # darker
else:
rgba = label_rgba
pixbuf.draw(context, rect.offset(xalign + dx, yalign + dy), rgba)
def draw_shadow_cached(self, context):
entry = self._shadow_surface
if entry is None:
if config.theme_settings.key_shadow_strength:
entry = self.create_shadow_surface(context,
self._shadow_steps,
self._shadow_alpha)
self._shadow_surface = entry
if entry:
surface, rect = entry
context.set_source_rgba(0.0, 0.0, 0.0, 1.0)
context.mask_surface(surface, rect.x, rect.y)
def create_shadow_surface(self, base_context, shadow_steps, shadow_alpha):
"""
Draw shadow and shaded halo.
Somewhat slow, make sure to cache the result.
Glitchy, if the clip-rect covers only a single button (Precise),
therefore, draw only with unrestricted clipping rect.
"""
rect = self.get_canvas_rect()
root = self.get_layout_root()
if rect.is_empty():
return None
extent = min(root.context.scale_log_to_canvas((1.0, 1.0)))
alpha = pi / 2 + self.get_light_direction()
shadow_opacity = config.theme_settings.key_shadow_strength * \
shadow_alpha
shadow_scale = config.theme_settings.key_shadow_size / 20.0
shadow_radius = max(extent * shadow_scale, 1.0)
shadow_displacement = max(extent * shadow_scale * 0.26, 1.0)
shadow_offset = (shadow_displacement * cos(alpha),
shadow_displacement * sin(alpha))
has_halo = shadow_steps > 1 and not config.window.transparent_background
halo_opacity = shadow_opacity * 0.11
halo_radius = max(extent * 8.0, 1.0)
clip_rect = rect.offset(shadow_offset[0]+1, shadow_offset[1]+1)
if has_halo:
clip_rect = clip_rect.inflate(halo_radius * 1.5)
else:
clip_rect = clip_rect.inflate(shadow_radius * 1.3)
clip_rect = clip_rect.int()
# create caching surface
target = base_context.get_target()
surface = target.create_similar(cairo.CONTENT_ALPHA,
clip_rect.w, clip_rect.h)
context = cairo.Context(surface)
# paint the surface
context.save()
context.translate(-clip_rect.x, -clip_rect.y)
context.rectangle(*clip_rect)
context.clip()
context.push_group_with_content(cairo.CONTENT_ALPHA)
self._build_canvas_path(context, rect)
context.set_source_rgba(0.0, 0.0, 0.0, 1.0)
context.fill()
shape = context.pop_group()
# shadow
drop_shadow(context, shape, rect,
shadow_radius, shadow_offset, shadow_opacity, shadow_steps)
# halo
if has_halo:
drop_shadow(context, shape, rect,
halo_radius, shadow_offset, halo_opacity, shadow_steps)
# cut out the key area, the key may be transparent
context.set_operator(cairo.OPERATOR_CLEAR)
context.set_source_rgba(0.0, 0.0, 0.0, 1.0)
self._build_canvas_path(context, rect)
context.fill()
context.restore()
return surface, clip_rect
def _build_canvas_path(self, cr, rect = None, path = None):
""" Build cairo path of the key geometry. """
if self.geometry:
if not path:
path = self.get_canvas_path()
self._build_complex_path(cr, path)
else:
if not rect:
rect = self.get_canvas_rect()
self._build_rect_path(cr, rect)
def _build_complex_path(self, cr, path):
roundness = config.theme_settings.roundrect_radius
chamfer_size = self.get_chamfer_size()
chamfer_size = self.context.scale_log_to_canvas_y(chamfer_size)
rounded_path(cr, path, roundness, chamfer_size)
def _build_rect_path(self, context, rect):
roundness = config.theme_settings.roundrect_radius
if roundness:
roundrect_curve(context, rect, roundness)
else:
context.rectangle(*rect)
def get_gradient_angle(self):
return -pi/2.0 + self.get_light_direction()
def get_best_font_size(self, mod_mask):
"""
Get the maximum font size that would not cause the label to
overflow the boundaries of the key.
"""
# Base this on the unpressed rect, so fake physical key action
# doesn't influence the font_size and doesn't cause surface cache
# misses for that minor wiggle.
rect = self.get_label_rect(self.get_unpressed_rect())
label_width, label_height = \
self.get_label_base_extents(mod_mask)
size_for_maximum_width = self.context.scale_log_to_canvas_x(
(rect.w - self.label_margin[0]*2)) \
/ label_width
size_for_maximum_height = self.context.scale_log_to_canvas_y(
(rect.h - self.label_margin[1]*2)) \
/ label_height
if size_for_maximum_width < size_for_maximum_height:
return int(size_for_maximum_width)
else:
return int(size_for_maximum_height)
def get_label_base_extents(self, mod_mask):
"""
Update resolution independent extents of the label layout.
"""
extents = self._label_extents.get(mod_mask)
if not extents:
extents = self.calc_label_base_extents(self.get_label())
self._label_extents[mod_mask] = extents
return extents
def calc_label_base_extents(self, label):
""" Calculate font-size independent extents. """
cr = Gdk.pango_context_get()
layout = Pango.Layout(cr)
BASE_FONTDESCRIPTION_SIZE = 10000000
self.prepare_pango_layout(layout, label, BASE_FONTDESCRIPTION_SIZE)
w, h = layout.get_size() # In Pango units
w = w or 1.0
h = h or 1.0
return w / (Pango.SCALE * BASE_FONTDESCRIPTION_SIZE), \
h / (Pango.SCALE * BASE_FONTDESCRIPTION_SIZE)
def invalidate_label_extents(self):
"""
Cached label extents are resolution independent. Calling this
is only necessary when the system font dpi change.
"""
self._label_extents = {}
def get_image(self, width, height):
"""
Get the cached image pixbuf object, load image
and create it if necessary.
Width and height in canvas coordinates.
"""
if not self.image_filenames:
return None
if self.active and ImageSlot.ACTIVE in self.image_filenames:
slot = ImageSlot.ACTIVE
else:
slot = ImageSlot.NORMAL
image_filename = self.image_filenames.get(slot)
if not image_filename:
return
if not self._image_pixbuf:
self._image_pixbuf = {}
self._requested_image_size = {}
pixbuf = self._image_pixbuf.get(slot)
size = self._requested_image_size.get(slot)
if not pixbuf or \
size[0] != int(width) or size[1] != int(height):
pixbuf = None
filename = config.get_image_filename(image_filename)
if filename:
_logger.debug("loading image '{}'".format(filename))
try:
pixbuf = PixBufScaled. \
from_file_and_size(filename, width, height)
except Exception as ex: # private exception gi._glib.GError when
# librsvg2-common wasn't installed
_logger.error("get_image(): " + unicode_str(ex))
if pixbuf:
self._requested_image_size[slot] = (int(width), int(height))
self._image_pixbuf[slot] = pixbuf
return pixbuf
def _label_iterations(self, lod):
stroke_gradient = self.get_stroke_gradient()
if lod == LOD.FULL and \
self.get_style() != "flat" and stroke_gradient:
root = self.get_layout_root()
d = 0.4 # fake-emboss distance
#d = max(src_size[1] * 0.02, 0.0)
max_offset = 2
alpha = self.get_gradient_angle()
xo = root.context.scale_log_to_canvas_x(d * cos(alpha))
yo = root.context.scale_log_to_canvas_y(d * sin(alpha))
xo = min(int(round(xo)), max_offset)
yo = min(int(round(yo)), max_offset)
luminosity_factor = stroke_gradient * 0.25
# shadow
yield xo, yo, -luminosity_factor, False
# highlight
yield -xo, -yo, luminosity_factor, False
# normal
yield 0, 0, 0, True
class FixedFontMixin:
""" Font size independent of text length """
def get_best_font_size(self, mod_mask):
"""
Get the maximum font size that would not cause the label to
overflow the height of the key.
"""
return self.calc_font_size(self.context,
self.get_fullsize_rect().get_size(),
True)
def calc_font_size(self, context, size, use_width = False):
""" Calculate font size based on the height of the key """
# Base this on the unpressed rect, so fake physical key action
# doesn't influence the font_size and doesn't cause surface cache
# misses for that minor wiggle.
label_width, label_height = self.get_label_base_extents(0)
size_for_maximum_width = context.scale_log_to_canvas_x(
(size[0] - self.label_margin[0]*2)) \
/ label_width
size_for_maximum_height = context.scale_log_to_canvas_y(
(size[1] - self.label_margin[1]*2)) \
/ label_height
font_size = size_for_maximum_height
if use_width and size_for_maximum_width < font_size:
font_size = size_for_maximum_width
return int(font_size * 0.9)
def get_label_base_extents(self, mod_mask):
"""
Update resolution independent extents of the label layout.
"""
extents = self._label_extents.get(mod_mask)
if not extents:
extents = self.calc_label_base_extents("Mg")
self._label_extents[mod_mask] = extents
return extents
class WordlistKey(RectKey):
def get_style(self):
style = super(WordlistKey, self).get_style()
if style == "dish":
style = "gradient"
return style
def get_stroke_width(self):
# Turn down stroke width -> Only subtly bevel the wordlist bar.
value = super(WordlistKey, self).get_stroke_width()
return min(value, 0.6)
def get_stroke_gradient(self):
# Turn down stroke gradient -> Only subtly bevel the wordlist bar.
value = super(WordlistKey, self).get_stroke_gradient()
return min(value, 0.3)
def get_light_direction(self):
return -0.3 * pi / 180
def draw_shadow_cached(self, context):
# no shadow
pass
class FullSizeKey(WordlistKey):
def __init__(self, id = "", border_rect = None):
super(FullSizeKey, self).__init__(id, border_rect)
def get_rect(self):
""" Get bounding box in logical coordinates """
# Disable key_size, let wordlist creation have complete size control.
return self.get_fullsize_rect()
class BarKey(FullSizeKey):
def __init__(self, id = "", border_rect = None):
super(BarKey, self).__init__(id, border_rect)
def draw(self, context, lod = LOD.FULL):
# draw only when pressed, to blend in with the word list bar
if self.pressed or self.active or self.scanned:
self.draw_geometry(context, lod)
self.draw_image(context, lod)
self.draw_label(context, lod)
def can_show_label_popup(self):
return False
def get_stroke_width(self):
# Turn down stroke width -> no annoying banding at
# what should be flat key edges.
return 0.0
class WordKey(FixedFontMixin, BarKey):
def __init__(self, id="", border_rect = None):
super(WordKey, self).__init__(id, border_rect)
class InputlineKey(FixedFontMixin, RectKey, InputlineKeyCommon):
cursor = 0
def __init__(self, id="", border_rect = None):
RectKey.__init__(self, id, border_rect)
self.word_infos = []
self._xscroll = 0.0
def set_content(self, line, word_infos, cursor):
self.line = line
self.word_infos = word_infos
self.cursor = cursor
self.invalidate_key()
# determine text direction
dir = Pango.find_base_dir(line, -1)
self.ltr = dir != Pango.Direction.RTL
def draw_label(self, context, lod):
layout, rect, cursor_rect, layout_pos = self._calc_layout_params()
cursor_width = cursor_rect.h * 0.075
cursor_width = max(cursor_width, 1.0)
label_rgba = self.get_label_color()
context.save()
context.rectangle(*rect)
context.clip()
# draw text
context.set_source_rgba(*label_rgba)
context.move_to(*layout_pos)
PangoCairo.show_layout(context, layout)
context.restore() # don't clip the caret
# draw caret
context.move_to(cursor_rect.x, cursor_rect.y)
context.rel_line_to(0, cursor_rect.h)
context.set_source_rgba(*label_rgba)
context.set_line_width(cursor_width)
context.stroke()
# reset attributes; layout is reused by all keys due to memory leak
layout.set_attributes(Pango.AttrList())
def get_layout(self):
text, attrs = self._build_layout_contents()
layout = self.get_pango_layout(text, self.font_size)
layout.set_attributes(attrs)
layout.set_auto_dir(True)
return layout
def get_canvas_label_rect(self):
rect = super(InputlineKey, self).get_canvas_label_rect()
return rect.int() # else clipping glitches
def _build_layout_contents(self):
# Add one char to avoid having to handle RTL corner cases at line end.
text = self.line + " "
attrs = None
# prepare colors
color_ignored = '#00FFFF'
color_partial_match = '#00AA00'
color_no_match = '#00FF00'
color_error = '#FF0000'
# set text colors, highlight unknown words
# AttrForeground/pango_attr_foreground_new are still inaccassible
# -> use parse_markup instead.
# https://bugzilla.gnome.org/show_bug.cgi?id=646788
markup = ""
wis = self.word_infos
for i, wi in enumerate(wis):
cursor_at_word_end = self.cursor == wi.end
# select colors
predict_color = None
spell_color = None
if 0: # no more bold, keep it simple
if wi.ignored:
#color = color_ignored
pass
elif not wi.exact_match:
if wi.partial_match and cursor_at_word_end:
predict_color = color_partial_match
else:
predict_color = color_no_match
if wi.spelling_errors:
spell_color = color_error
# highlight the word as needed
word = text[wi.start : wi.end]
word = GLib.markup_escape_text(word)
if predict_color or spell_color:
span = ""
if predict_color:
span += "<b>"
if spell_color:
span += "<span underline_color='" + spell_color + "' " + \
"underline='error'>"
span += word
if spell_color:
span += "</span>"
if predict_color:
span += "</b>"
t = span
else:
span = word
# assemble the escaped pieces
if i == 0:
# add text up to the first word
intro = text[:wi.start]
markup += GLib.markup_escape_text(intro)
else:
# add gap between words
wiprev = wis[i-1]
gap = text[wiprev.end : wi.start]
markup += GLib.markup_escape_text(gap)
# add the word
markup += span
if i == len(wis) - 1:
# add remaining text after the last word
remainder = text[wi.end:]
markup += GLib.markup_escape_text(remainder)
result = Pango.parse_markup(markup, -1, "\0")
if len(result) == 4:
ok, attrs, text, error = result
return text, attrs
def _calc_layout_params(self):
layout = self.get_layout()
# get label rect and aligned drawing origin
rect = self.get_canvas_label_rect()
text_size = layout.get_pixel_size()
xalign, yalign = self.align_label(text_size, (rect.w, rect.h), self.ltr)
# get cursor position
cursor_index = self.cursor_to_layout_index(layout, self.cursor, self.ltr)
strong_pos, weak_pos = layout.get_cursor_pos(cursor_index)
pos = strong_pos
cursor_rect = Rect(pos.x, pos.y, pos.width, pos.height).scale(1.0 / Pango.SCALE)
# scroll to cursor
self._update_scroll_position(rect, text_size, cursor_rect, xalign)
xlayout = rect.x + xalign + self._xscroll
ylayout = rect.y + yalign
cursor_rect.x += xlayout
cursor_rect.y += ylayout
return layout, rect, cursor_rect, (xlayout, ylayout)
def _update_scroll_position(self, label_rect, text_size,
cursor_rect, xalign):
xscroll = self._xscroll
# scroll line into view
gap_begin = xalign + xscroll
gap_end = label_rect.w - (xalign + xscroll + text_size[0])
if gap_begin > 0 or gap_end > 0:
xscroll = 0
# scroll cursor into view
over_begin = -(xalign + cursor_rect.x)
over_end = xalign + cursor_rect.x - label_rect.w
if over_begin - xscroll > 0.0:
xscroll = over_begin
if over_end + xscroll > 0.0:
xscroll = -over_end
self._xscroll = xscroll
@staticmethod
def cursor_to_layout_index(layout, cursor, ltr = False):
""" Translate unicode character position to pango byte index. """
indexes = []
i = 0
iter = layout.get_iter()
while True:
indexes.append(iter.get_index())
if not iter.next_char():
break
if ltr:
if len(indexes) == 0:
cursor_index = 0
elif cursor < 0:
cursor_index = 0
elif cursor >= len(indexes):
cursor_index = indexes[-1]
else:
cursor_index = indexes[cursor]
else:
if len(indexes) == 0:
cursor_index = 0
elif cursor < 0:
cursor_index = indexes[-1]
elif cursor >= len(indexes):
cursor_index = 0
else:
cursor_index = indexes[-(cursor+1)]
return cursor_index
class PixBufScaled:
"""
Workaround for blurry images when window_scaling_factor >1
"""
_pixbuf = None
_width = 0
_height = 0
_real_width = 0
_real_height = 0
@staticmethod
def from_file_and_size(filename, width, height):
pixbuf = PixBufScaled()
pixbuf._load(filename, width, height)
return pixbuf
def get_width(self):
return self._width
def get_height(self):
return self._height
def _load(self, filename, width, height):
scale = config.window_scaling_factor
load_width = width * scale
load_height = height * scale
self._pixbuf = GdkPixbuf.Pixbuf. \
new_from_file_at_size(filename, load_width, load_height)
self._real_width = self._pixbuf.get_width()
self._real_height = self._pixbuf.get_height()
self._width = self._real_width / scale
self._height = self._real_height / scale
def draw(self, context, rect, rgba):
"""
Draw the image in the theme's label color.
Only the alpha channel of the image is used.
"""
context.save()
context.translate(rect.x, rect.y)
scale = config.window_scaling_factor
if scale and scale != 1.0:
context.scale(1.0 / scale, 1.0 / scale)
Gdk.cairo_set_source_pixbuf(context, self._pixbuf, 0, 0)
pattern = context.get_source()
context.set_source_rgba(*rgba)
context.mask(pattern)
context.restore()