aboutsummaryrefslogtreecommitdiffstats
path: root/lvc/widgets/gtk/layoutmanager.py
diff options
context:
space:
mode:
authorJesús Eduardo <heckyel@hyperbola.info>2017-09-11 17:47:17 -0500
committerJesús Eduardo <heckyel@hyperbola.info>2017-09-11 17:47:17 -0500
commit14738704ede6dfa6ac79f362a9c1f7f40f470cdc (patch)
tree31c83bdd188ae7b64d7169974d6f066ccfe95367 /lvc/widgets/gtk/layoutmanager.py
parenteb1896583afbbb622cadcde1a24e17173f61904f (diff)
downloadlibrevideoconverter-14738704ede6dfa6ac79f362a9c1f7f40f470cdc.tar.lz
librevideoconverter-14738704ede6dfa6ac79f362a9c1f7f40f470cdc.tar.xz
librevideoconverter-14738704ede6dfa6ac79f362a9c1f7f40f470cdc.zip
rename mvc at lvc
Diffstat (limited to 'lvc/widgets/gtk/layoutmanager.py')
-rw-r--r--lvc/widgets/gtk/layoutmanager.py550
1 files changed, 550 insertions, 0 deletions
diff --git a/lvc/widgets/gtk/layoutmanager.py b/lvc/widgets/gtk/layoutmanager.py
new file mode 100644
index 0000000..8097b2e
--- /dev/null
+++ b/lvc/widgets/gtk/layoutmanager.py
@@ -0,0 +1,550 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+"""drawing.py -- Contains the LayoutManager class. LayoutManager is
+handles laying out complex objects for the custom drawing code like
+text blocks and buttons.
+"""
+
+import math
+
+import cairo
+import gtk
+import pango
+
+from lvc import utils
+
+use_native_buttons = False # not implemented in MVC
+
+class FontCache(utils.Cache):
+ def get(self, context, description, scale_factor, bold, italic):
+ key = (context, description, scale_factor, bold, italic)
+ return utils.Cache.get(self, key)
+
+ def create_new_value(self, key, invalidator=None):
+ (context, description, scale_factor, bold, italic) = key
+ return Font(context, description, scale_factor, bold, italic)
+
+_font_cache = FontCache(512)
+
+class LayoutManager(object):
+ def __init__(self, widget):
+ self.pango_context = widget.get_pango_context()
+ self.update_style(widget.style)
+ self.update_direction(widget.get_direction())
+ widget.connect('style-set', self.on_style_set)
+ widget.connect('direction-changed', self.on_direction_changed)
+ self.widget = widget
+ self.reset()
+
+ def reset(self):
+ self.current_font = self.font(1.0)
+ self.text_color = (0, 0, 0)
+ self.text_shadow = None
+
+ def on_style_set(self, widget, previous_style):
+ old_font_desc = self.style_font_desc
+ self.update_style(widget.style)
+ if self.style_font_desc != old_font_desc:
+ # bug #17423 font changed, so the widget's width might have changed
+ widget.queue_resize()
+
+ def on_direction_changed(self, widget, previous_direction):
+ self.update_direction(widget.get_direction())
+
+ def update_style(self, style):
+ self.style_font_desc = style.font_desc
+ self.style = style
+
+ def update_direction(self, direction):
+ if direction == gtk.TEXT_DIR_RTL:
+ self.pango_context.set_base_dir(pango.DIRECTION_RTL)
+ else:
+ self.pango_context.set_base_dir(pango.DIRECTION_LTR)
+
+ def font(self, scale_factor, bold=False, italic=False, family=None):
+ return _font_cache.get(self.pango_context, self.style_font_desc,
+ scale_factor, bold, italic)
+
+ def set_font(self, scale_factor, bold=False, italic=False, family=None):
+ self.current_font = self.font(scale_factor, bold, italic)
+
+ def set_text_color(self, color):
+ self.text_color = color
+
+ def set_text_shadow(self, shadow):
+ self.text_shadow = shadow
+
+ def textbox(self, text, underline=False):
+ textbox = TextBox(self.pango_context, self.current_font,
+ self.text_color, self.text_shadow)
+ textbox.set_text(text, underline=underline)
+ return textbox
+
+ def button(self, text, pressed=False, disabled=False, style='normal'):
+ if style == 'webby':
+ return StyledButton(text, self.pango_context, self.current_font,
+ pressed, disabled)
+ elif use_native_buttons:
+ return NativeButton(text, self.pango_context, self.current_font,
+ pressed, self.style, self.widget)
+ else:
+ return StyledButton(text, self.pango_context, self.current_font,
+ pressed)
+
+ def update_cairo_context(self, cairo_context):
+ cairo_context.update_context(self.pango_context)
+
+class Font(object):
+ def __init__(self, context, style_font_desc, scale, bold, italic):
+ self.context = context
+ self.description = style_font_desc.copy()
+ self.description.set_size(int(scale * style_font_desc.get_size()))
+ if bold:
+ self.description.set_weight(pango.WEIGHT_BOLD)
+ if italic:
+ self.description.set_style(pango.STYLE_ITALIC)
+ self.font_metrics = None
+
+ def get_font_metrics(self):
+ if self.font_metrics is None:
+ self.font_metrics = self.context.get_metrics(self.description)
+ return self.font_metrics
+
+ def ascent(self):
+ return pango.PIXELS(self.get_font_metrics().get_ascent())
+
+ def descent(self):
+ return pango.PIXELS(self.get_font_metrics().get_descent())
+
+ def line_height(self):
+ metrics = self.get_font_metrics()
+ # the +1: some glyphs can be slightly taller than ascent+descent
+ # (#17329)
+ return (pango.PIXELS(metrics.get_ascent()) +
+ pango.PIXELS(metrics.get_descent()) + 1)
+
+class TextBox(object):
+ def __init__(self, context, font, color, shadow):
+ self.layout = pango.Layout(context)
+ self.layout.set_wrap(pango.WRAP_WORD_CHAR)
+ self.font = font
+ self.color = color
+ self.layout.set_font_description(font.description.copy())
+ self.width = self.height = None
+ self.shadow = shadow
+
+ def set_text(self, text, font=None, color=None, underline=False):
+ self.text_chunks = []
+ self.attributes = []
+ self.text_length = 0
+ self.underlines = []
+ self.append_text(text, font, color, underline)
+
+ def append_text(self, text, font=None, color=None, underline=False):
+ if text == None:
+ text = u""
+ startpos = self.text_length
+ self.text_chunks.append(text)
+ endpos = self.text_length = self.text_length + len(text)
+ if font is not None:
+ attr = pango.AttrFontDesc(font.description, startpos, endpos)
+ self.attributes.append(attr)
+ if underline:
+ self.underlines.append((startpos, endpos))
+ if color:
+ def convert(value):
+ return int(round(value * 65535))
+ attr = pango.AttrForeground(convert(color[0]), convert(color[1]),
+ convert(color[2]), startpos, endpos)
+ self.attributes.append(attr)
+ self.text_set = False
+
+ def set_width(self, width):
+ if width is not None:
+ self.layout.set_width(int(width * pango.SCALE))
+ else:
+ self.layout.set_width(-1)
+ self.width = width
+
+ def set_height(self, height):
+ # if height is not None:
+ # # not sure why set_height isn't in the python bindings, but it
+ # # isn't
+ # pygtkhacks.set_pango_layout_height(self.layout,
+ # int(height * pango.SCALE))
+ self.height = height
+
+ def set_wrap_style(self, wrap):
+ if wrap == 'word':
+ self.layout.set_wrap(pango.WRAP_WORD_CHAR)
+ elif wrap == 'char' or wrap == 'truncated-char':
+ self.layout.set_wrap(pango.WRAP_CHAR)
+ else:
+ raise ValueError("Unknown wrap value: %s" % wrap)
+ if wrap == 'truncated-char':
+ self.layout.set_ellipsize(pango.ELLIPSIZE_END)
+ else:
+ self.layout.set_ellipsize(pango.ELLIPSIZE_NONE)
+
+ def set_alignment(self, align):
+ if align == 'left':
+ self.layout.set_alignment(pango.ALIGN_LEFT)
+ elif align == 'right':
+ self.layout.set_alignment(pango.ALIGN_RIGHT)
+ elif align == 'center':
+ self.layout.set_alignment(pango.ALIGN_CENTER)
+ else:
+ raise ValueError("Unknown align value: %s" % align)
+
+ def ensure_layout(self):
+ if not self.text_set:
+ text = ''.join(self.text_chunks)
+ if len(text) > 100:
+ text = text[:self._calc_text_cutoff()]
+ self.layout.set_text(text)
+ attr_list = pango.AttrList()
+ for attr in self.attributes:
+ attr_list.insert(attr)
+ self.layout.set_attributes(attr_list)
+ self.text_set = True
+
+ def _calc_text_cutoff(self):
+ """This method is a bit of a hack... GTK slows down if we pass too
+ much text to the layout. Even text that falls below our height has a
+ performance penalty. Try not to have too much more than is necessary.
+ """
+ if None in (self.width, self.height):
+ return -1
+
+ chars_per_line = (self.width * pango.SCALE //
+ self.font.get_font_metrics().get_approximate_char_width())
+ lines_available = self.height // self.font.line_height()
+ # overestimate these because it's better to have too many characters
+ # than too little.
+ return int(chars_per_line * lines_available * 1.2)
+
+ def line_count(self):
+ self.ensure_layout()
+ return self.layout.get_line_count()
+
+ def get_size(self):
+ self.ensure_layout()
+ return self.layout.get_pixel_size()
+
+ def char_at(self, x, y):
+ self.ensure_layout()
+ x *= pango.SCALE
+ y *= pango.SCALE
+ width, height = self.layout.get_size()
+ if 0 <= x < width and 0 <= y < height:
+ index, leading = self.layout.xy_to_index(x, y)
+ # xy_to_index returns the nearest character, but that
+ # doesn't mean the user actually clicked on it. Double
+ # check that (x, y) is actually inside that char's
+ # bounding box
+ char_x, char_y, char_w, char_h = self.layout.index_to_pos(index)
+ if char_w > 0: # the glyph is LTR
+ left = char_x
+ right = char_x + char_w
+ else: # the glyph is RTL
+ left = char_x + char_w
+ right = char_x
+ if left <= x < right:
+ return index
+ return None
+
+
+ def draw(self, context, x, y, width, height):
+ self.set_width(width)
+ self.set_height(height)
+ self.ensure_layout()
+ cairo_context = context.context
+ cairo_context.save()
+ underline_drawer = UnderlineDrawer(self.underlines)
+ if self.shadow:
+ # draw shadow first so that it's underneath the regular text
+ # FIXME: we don't use the blur_radius setting
+ cairo_context.set_source_rgba(self.shadow.color[0],
+ self.shadow.color[1], self.shadow.color[2],
+ self.shadow.opacity)
+ self._draw_layout(context, x + self.shadow.offset[0],
+ y + self.shadow.offset[1], width, height,
+ underline_drawer)
+ cairo_context.set_source_rgb(*self.color)
+ self._draw_layout(context, x, y, width, height, underline_drawer)
+ cairo_context.restore()
+ cairo_context.new_path()
+
+ def _draw_layout(self, context, x, y, width, height, underline_drawer):
+ line_height = 0
+ alignment = self.layout.get_alignment()
+ for i in xrange(self.layout.get_line_count()):
+ line = self.layout.get_line_readonly(i)
+ extents = line.get_pixel_extents()[1]
+ next_line_height = line_height + extents[3]
+ if next_line_height > height:
+ break
+ if alignment == pango.ALIGN_CENTER:
+ line_x = max(x, x + (width - extents[2]) / 2.0)
+ elif alignment == pango.ALIGN_RIGHT:
+ line_x = max(x, x + width - extents[2])
+ else:
+ line_x = x
+ baseline = y + line_height + pango.ASCENT(extents)
+ context.move_to(line_x, baseline)
+ context.context.show_layout_line(line)
+ underline_drawer.draw(context, line_x, baseline, line)
+ line_height = next_line_height
+
+class UnderlineDrawer(object):
+ """Class to draw our own underlines because cairo's don't look
+ that great at small fonts. We make sure that the underline is
+ always drawn at a pixel boundary and that there always is space
+ between the text and the baseline.
+
+ This class makes a couple assumptions that might not be that
+ great. It assumes that the correct underline size is 1 pixel and
+ that the text color doesn't change in the middle of an underline.
+ """
+ def __init__(self, underlines):
+ self.underline_iter = iter(underlines)
+ self.finished = False
+ self.next_underline()
+
+ def next_underline(self):
+ try:
+ self.startpos, self.endpos = self.underline_iter.next()
+ except StopIteration:
+ self.finished = True
+ else:
+ # endpos is the char to stop underlining at
+ self.endpos -= 1
+
+ def draw(self, context, x, baseline, line):
+ baseline = round(baseline) + 0.5
+ context.set_line_width(1)
+ while not self.finished and line.start_index <= self.startpos:
+ startpos = max(line.start_index, self.startpos)
+ endpos = min(self.endpos, line.start_index + line.length)
+ x1 = x + pango.PIXELS(line.index_to_x(startpos, 0))
+ x2 = x + pango.PIXELS(line.index_to_x(endpos, 1))
+ context.move_to(x1, baseline + 1)
+ context.line_to(x2, baseline + 1)
+ context.stroke()
+ if endpos < self.endpos:
+ break
+ else:
+ self.next_underline()
+
+class NativeButton(object):
+ ICON_PAD = 4
+
+ def __init__(self, text, context, font, pressed, style, widget):
+ self.layout = pango.Layout(context)
+ self.font = font
+ self.pressed = pressed
+ self.layout.set_font_description(font.description.copy())
+ self.layout.set_text(text)
+ self.pad_x = style.xthickness + 11
+ self.pad_y = style.ythickness + 1
+ self.style = style
+ self.widget = widget
+ # The above code assumes an "inner-border" style property of
+ # 1. PyGTK doesn't seem to support Border objects very well,
+ # so can't get it from the widget style.
+ self.min_width = 0
+ self.icon = None
+
+ def set_min_width(self, width):
+ self.min_width = width
+
+ def set_icon(self, icon):
+ self.icon = icon
+
+ def get_size(self):
+ width, height = self.layout.get_pixel_size()
+ if self.icon:
+ width += self.icon.width + self.ICON_PAD
+ height = max(height, self.icon.height)
+ width += self.pad_x * 2
+ height += self.pad_y * 2
+ return max(self.min_width, width), height
+
+ def draw(self, context, x, y, width, height):
+ text_width, text_height = self.layout.get_pixel_size()
+ if self.icon:
+ inner_width = text_width + self.icon.width + self.ICON_PAD
+ # calculate the icon position x and y are still in cairo
+ # coordinates
+ icon_x = x + (width - inner_width) / 2.0
+ icon_y = y + (height - self.icon.height) / 2.0
+ text_x = icon_x + self.icon.width + self.ICON_PAD
+ else:
+ text_x = x + (width - text_width) / 2.0
+ text_y = y + (height - text_height) / 2.0
+
+ x, y = context.context.user_to_device(x, y)
+ text_x, text_y = context.context.user_to_device(text_x, text_y)
+ # Hmm, maybe we should somehow support floating point numbers
+ # here, but I don't know how to.
+ x, y, width, height = (int(f) for f in (x, y, width, height))
+ context.context.get_target().flush()
+ self.draw_box(context.window, x, y, width, height)
+ self.draw_text(context.window, text_x, text_y)
+ if self.icon:
+ self.icon.draw(context, icon_x, icon_y, self.icon.width,
+ self.icon.height)
+
+ def draw_box(self, window, x, y, width, height):
+ if self.pressed:
+ shadow = gtk.SHADOW_IN
+ state = gtk.STATE_ACTIVE
+ else:
+ shadow = gtk.SHADOW_OUT
+ state = gtk.STATE_NORMAL
+ if 'QtCurveStyle' in str(self.style):
+ # This is a horrible hack for the libqtcurve library. See
+ # http://bugzilla.pculture.org/show_bug.cgi?id=10380 for
+ # details
+ widget = window.get_user_data()
+ else:
+ widget = self.widget
+
+ self.style.paint_box(window, state, shadow, None, widget, "button",
+ int(x), int(y), int(width), int(height))
+
+ def draw_text(self, window, x, y):
+ if self.pressed:
+ state = gtk.STATE_ACTIVE
+ else:
+ state = gtk.STATE_NORMAL
+ self.style.paint_layout(window, state, True, None, None, None,
+ int(x), int(y), self.layout)
+
+class StyledButton(object):
+ PAD_HORIZONTAL = 4
+ PAD_VERTICAL = 3
+ TOP_COLOR = (1, 1, 1)
+ BOTTOM_COLOR = (0.86, 0.86, 0.86)
+ LINE_COLOR_TOP = (0.71, 0.71, 0.71)
+ LINE_COLOR_BOTTOM = (0.45, 0.45, 0.45)
+ TEXT_COLOR = (0.184, 0.184, 0.184)
+ DISABLED_COLOR = (0.86, 0.86, 0.86)
+ DISABLED_TEXT_COLOR = (0.5, 0.5, 0.5)
+ ICON_PAD = 8
+
+ def __init__(self, text, context, font, pressed, disabled=False):
+ self.layout = pango.Layout(context)
+ self.font = font
+ self.layout.set_font_description(font.description.copy())
+ self.layout.set_text(text)
+ self.min_width = 0
+ self.pressed = pressed
+ self.disabled = disabled
+ self.icon = None
+
+ def set_icon(self, icon):
+ self.icon = icon
+
+ def set_min_width(self, width):
+ self.min_width = width
+
+ def get_size(self):
+ width, height = self.layout.get_pixel_size()
+ if self.icon:
+ width += self.icon.width + self.ICON_PAD
+ height = max(height, self.icon.height)
+ height += self.PAD_VERTICAL * 2
+ if height % 2 == 1:
+ # make height even so that the radius of our circle is
+ # whole
+ height += 1
+ width += self.PAD_HORIZONTAL * 2 + height
+ return max(self.min_width, width), height
+
+ def draw_path(self, context, x, y, width, height, radius):
+ inner_width = width - radius * 2
+ context.move_to(x + radius, y)
+ context.rel_line_to(inner_width, 0)
+ context.arc(x + width - radius, y+radius, radius, -math.pi/2,
+ math.pi/2)
+ context.rel_line_to(-inner_width, 0)
+ context.arc(x + radius, y+radius, radius, math.pi/2, -math.pi/2)
+
+ def draw_button(self, context, x, y, width, height, radius):
+ context.context.save()
+ self.draw_path(context, x, y, width, height, radius)
+ if self.disabled:
+ end_color = self.DISABLED_COLOR
+ start_color = self.DISABLED_COLOR
+ elif self.pressed:
+ end_color = self.TOP_COLOR
+ start_color = self.BOTTOM_COLOR
+ else:
+ context.set_line_width(1)
+ start_color = self.TOP_COLOR
+ end_color = self.BOTTOM_COLOR
+ gradient = cairo.LinearGradient(x, y, x, y + height)
+ gradient.add_color_stop_rgb(0, *start_color)
+ gradient.add_color_stop_rgb(1, *end_color)
+ context.context.set_source(gradient)
+ context.fill()
+ context.set_line_width(1)
+ self.draw_path(context, x+0.5, y+0.5, width, height, radius)
+ gradient = cairo.LinearGradient(x, y, x, y + height)
+ gradient.add_color_stop_rgb(0, *self.LINE_COLOR_TOP)
+ gradient.add_color_stop_rgb(1, *self.LINE_COLOR_BOTTOM)
+ context.context.set_source(gradient)
+ context.stroke()
+ context.context.restore()
+
+ def draw(self, context, x, y, width, height):
+ radius = height / 2
+ self.draw_button(context, x, y, width, height, radius)
+
+ text_width, text_height = self.layout.get_pixel_size()
+ # draw the text in the center of the button
+ text_x = x + (width - text_width) / 2
+ text_y = y + (height - text_height) / 2
+ if self.icon:
+ icon_x = text_x - (self.icon.width + self.ICON_PAD) / 2
+ text_x += (self.icon.width + self.ICON_PAD) / 2
+ icon_y = y + (height - self.icon.height) / 2
+ self.icon.draw(context, icon_x, icon_y, self.icon.width,
+ self.icon.height)
+ self.draw_text(context, text_x, text_y, width, height, radius)
+
+ def draw_text(self, context, x, y, width, height, radius):
+ if self.disabled:
+ context.set_color(self.DISABLED_TEXT_COLOR)
+ else:
+ context.set_color(self.TEXT_COLOR)
+ context.move_to(x, y)
+ context.context.show_layout(self.layout)