# -*- coding: UTF-8 -*- # Copyright © 2007 Martin Böhme # Copyright © 2009 Chris Jones # Copyright © 2010 Francesco Fumanti # Copyright © 2011 Alan Bell # Copyright © 2012 Gerd Kohlberger # Copyright © 2009-2014, 2016 marmuta # # This file is part of Onboard. # # Onboard is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # Onboard is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from __future__ import division, print_function, unicode_literals 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 += "" if spell_color: span += "" span += word if spell_color: span += "" if predict_color: span += "" 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()