aboutsummaryrefslogtreecommitdiffstats
path: root/lvc/widgets/osx/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/osx/layoutmanager.py
parenteb1896583afbbb622cadcde1a24e17173f61904f (diff)
downloadlibrevideoconverter-14738704ede6dfa6ac79f362a9c1f7f40f470cdc.tar.lz
librevideoconverter-14738704ede6dfa6ac79f362a9c1f7f40f470cdc.tar.xz
librevideoconverter-14738704ede6dfa6ac79f362a9c1f7f40f470cdc.zip
rename mvc at lvc
Diffstat (limited to 'lvc/widgets/osx/layoutmanager.py')
-rw-r--r--lvc/widgets/osx/layoutmanager.py445
1 files changed, 445 insertions, 0 deletions
diff --git a/lvc/widgets/osx/layoutmanager.py b/lvc/widgets/osx/layoutmanager.py
new file mode 100644
index 0000000..de4301b
--- /dev/null
+++ b/lvc/widgets/osx/layoutmanager.py
@@ -0,0 +1,445 @@
+# @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.
+
+"""textlayout.py -- Contains the LayoutManager class. It handles laying text,
+buttons, getting font metrics and other tasks that are required to size
+things.
+"""
+import logging
+import math
+
+from AppKit import *
+from Foundation import *
+from objc import YES, NO, nil
+
+import drawing
+
+INFINITE = 1000000 # size of an "infinite" dimension
+
+class MiroLayoutManager(NSLayoutManager):
+ """Overide NSLayoutManager to draw better underlines."""
+
+ def drawUnderlineForGlyphRange_underlineType_baselineOffset_lineFragmentRect_lineFragmentGlyphRange_containerOrigin_(self, glyph_range, typ, offset, line_rect, line_glyph_range, container_origin):
+ container, _ = self.textContainerForGlyphAtIndex_effectiveRange_(glyph_range.location, None)
+ rect = self.boundingRectForGlyphRange_inTextContainer_(glyph_range, container)
+ x = container_origin.x + rect.origin.x
+ y = (container_origin.y + rect.origin.y + rect.size.height - offset)
+ underline_height, offset = self.calc_underline_extents(glyph_range)
+ y = math.ceil(y + offset) + underline_height / 2.0
+ path = NSBezierPath.bezierPath()
+ path.setLineWidth_(underline_height)
+ path.moveToPoint_(NSPoint(x, y))
+ path.relativeLineToPoint_(NSPoint(rect.size.width, 0))
+ path.stroke()
+
+ def calc_underline_extents(self, line_glyph_range):
+ index = self.characterIndexForGlyphAtIndex_(line_glyph_range.location)
+ font, _ = self.textStorage().attribute_atIndex_effectiveRange_(NSFontAttributeName, index, None)
+ # we use a couple of magic numbers that seems to work okay. I (BDK)
+ # got it from some old mozilla code.
+ height = font.ascender() - font.descender()
+ height = max(1.0, round(0.05 * height))
+ offset = max(1.0, round(0.1 * height))
+ return height, offset
+
+class TextBoxPool(object):
+ """Handles a pool of TextBox objects. We monitor the TextBox objects and
+ when those objects die, we reclaim them for the pool.
+
+ Creating TextBoxes is fairly expensive and NSLayoutManager do a lot of
+ caching, so it's useful to keep them around rather than destroying them.
+ """
+
+ def __init__(self):
+ self.used_text_boxes = []
+ self.available_text_boxes = []
+
+ def get(self):
+ """Get a NSLayoutManager, either from the pool or by creating a new
+ one.
+ """
+ try:
+ rv = self.available_text_boxes.pop()
+ except IndexError:
+ rv = TextBox()
+ self.used_text_boxes.append(rv)
+ return rv
+
+ def reclaim_textboxes(self):
+ """Move used TextBoxes back to the available pool. This should be
+ called after the code using text boxes is done using all of them.
+ """
+ self.available_text_boxes.extend(self.used_text_boxes)
+ self.used_text_boxes[:] = []
+
+text_box_pool = TextBoxPool()
+
+class Font(object):
+ line_height_sizer = NSLayoutManager.alloc().init()
+
+ def __init__(self, nsfont):
+ self.nsfont = nsfont
+
+ def ascent(self):
+ return self.nsfont.ascender()
+
+ def descent(self):
+ return -self.nsfont.descender()
+
+ def line_height(self):
+ return Font.line_height_sizer.defaultLineHeightForFont_(self.nsfont)
+
+class FontPool(object):
+ def __init__(self):
+ self._cached_fonts = {}
+
+ def get(self, scale_factor, bold, italic, family):
+ cache_key = (scale_factor, bold, italic, family)
+ try:
+ return self._cached_fonts[cache_key]
+ except KeyError:
+ font = self._create(scale_factor, bold, italic, family)
+ self._cached_fonts[cache_key] = font
+ return font
+
+ def _create(self, scale_factor, bold, italic, family):
+ size = round(scale_factor * NSFont.systemFontSize())
+ nsfont = None
+ if family is not None:
+ if bold:
+ nsfont = NSFont.fontWithName_size_(family + " Bold", size)
+ else:
+ nsfont = NSFont.fontWithName_size_(family, size)
+ if nsfont is None:
+ logging.error('FontPool: family %s scale %s bold %s '
+ 'italic %s not found',
+ family, scale_factor, bold, italic)
+ # Att his point either we have requested a custom font that failed
+ # to load or the system font was requested.
+ if nsfont is None:
+ if bold:
+ nsfont = NSFont.boldSystemFontOfSize_(size)
+ else:
+ nsfont = NSFont.systemFontOfSize_(size)
+ return Font(nsfont)
+
+class LayoutManager(object):
+ font_pool = FontPool()
+ default_font = font_pool.get(1.0, False, False, None)
+
+ def __init__(self):
+ self.current_font = self.default_font
+ self.set_text_color((0, 0, 0))
+ self.set_text_shadow(None)
+
+ def font(self, scale_factor, bold=False, italic=False, family=None):
+ return self.font_pool.get(scale_factor, bold, italic, family)
+
+ def set_font(self, scale_factor, bold=False, italic=False, family=None):
+ self.current_font = self.font(scale_factor, bold, italic, family)
+
+ def set_text_color(self, color):
+ self.text_color = color
+
+ def set_text_shadow(self, shadow):
+ self.shadow = shadow
+
+ def textbox(self, text, underline=False):
+ text_box = text_box_pool.get()
+ color = NSColor.colorWithDeviceRed_green_blue_alpha_(self.text_color[0], self.text_color[1], self.text_color[2], 1.0)
+ text_box.reset(text, self.current_font, color, self.shadow, underline)
+ return text_box
+
+ def button(self, text, pressed=False, disabled=False, style='normal'):
+ if style == 'webby':
+ return StyledButton(text, self.current_font, pressed, disabled)
+ else:
+ return NativeButton(text, self.current_font, pressed, disabled)
+
+ def reset(self):
+ text_box_pool.reclaim_textboxes()
+ self.current_font = self.default_font
+ self.text_color = (0, 0, 0)
+ self.shadow = None
+
+class TextBox(object):
+ def __init__(self):
+ self.layout_manager = MiroLayoutManager.alloc().init()
+ container = NSTextContainer.alloc().init()
+ container.setLineFragmentPadding_(0)
+ self.layout_manager.addTextContainer_(container)
+ self.layout_manager.setUsesFontLeading_(NO)
+ self.text_storage = NSTextStorage.alloc().init()
+ self.text_storage.addLayoutManager_(self.layout_manager)
+ self.text_container = self.layout_manager.textContainers()[0]
+
+ def reset(self, text, font, color, shadow, underline):
+ """Reset the text box so it's ready to be used by a new owner."""
+ self.text_storage.deleteCharactersInRange_(NSRange(0,
+ self.text_storage.length()))
+ self.text_container.setContainerSize_(NSSize(INFINITE, INFINITE))
+ self.paragraph_style = NSMutableParagraphStyle.alloc().init()
+ self.font = font
+ self.color = color
+ self.shadow = shadow
+ self.width = None
+ self.set_text(text, underline=underline)
+
+ def make_attr_string(self, text, color, font, underline):
+ attributes = NSMutableDictionary.alloc().init()
+ if color is not None:
+ nscolor = NSColor.colorWithDeviceRed_green_blue_alpha_(color[0], color[1], color[2], 1.0)
+ attributes.setObject_forKey_(nscolor, NSForegroundColorAttributeName)
+ else:
+ attributes.setObject_forKey_(self.color, NSForegroundColorAttributeName)
+ if font is not None:
+ attributes.setObject_forKey_(font.nsfont, NSFontAttributeName)
+ else:
+ attributes.setObject_forKey_(self.font.nsfont, NSFontAttributeName)
+ if underline:
+ attributes.setObject_forKey_(NSUnderlineStyleSingle, NSUnderlineStyleAttributeName)
+ attributes.setObject_forKey_(self.paragraph_style.copy(), NSParagraphStyleAttributeName)
+ if text is None:
+ text = ""
+ return NSAttributedString.alloc().initWithString_attributes_(text, attributes)
+
+ def set_text(self, text, color=None, font=None, underline=False):
+ string = self.make_attr_string(text, color, font, underline)
+ self.text_storage.setAttributedString_(string)
+
+ def append_text(self, text, color=None, font=None, underline=False):
+ string = self.make_attr_string(text, color, font, underline)
+ self.text_storage.appendAttributedString_(string)
+
+ def set_width(self, width):
+ if width is not None:
+ self.text_container.setContainerSize_(NSSize(width, INFINITE))
+ else:
+ self.text_container.setContainerSize_(NSSize(INFINITE, INFINITE))
+ self.width = width
+
+ def update_paragraph_style(self):
+ attr = NSParagraphStyleAttributeName
+ value = self.paragraph_style.copy()
+ rnge = NSMakeRange(0, self.text_storage.length())
+ self.text_storage.addAttribute_value_range_(attr, value, rnge)
+
+ def set_wrap_style(self, wrap):
+ if wrap == 'word':
+ self.paragraph_style.setLineBreakMode_(NSLineBreakByWordWrapping)
+ elif wrap == 'char':
+ self.paragraph_style.setLineBreakMode_(NSLineBreakByCharWrapping)
+ elif wrap == 'truncated-char':
+ self.paragraph_style.setLineBreakMode_(NSLineBreakByTruncatingTail)
+ else:
+ raise ValueError("Unknown wrap value: %s" % wrap)
+ self.update_paragraph_style()
+
+ def set_alignment(self, align):
+ if align == 'left':
+ self.paragraph_style.setAlignment_(NSLeftTextAlignment)
+ elif align == 'right':
+ self.paragraph_style.setAlignment_(NSRightTextAlignment)
+ elif align == 'center':
+ self.paragraph_style.setAlignment_(NSCenterTextAlignment)
+ else:
+ raise ValueError("Unknown align value: %s" % align)
+ self.update_paragraph_style()
+
+ def get_size(self):
+ # The next line is there just to force cocoa to layout the text
+ self.layout_manager.glyphRangeForTextContainer_(self.text_container)
+ rect = self.layout_manager.usedRectForTextContainer_(self.text_container)
+ return rect.size.width, rect.size.height
+
+ def char_at(self, x, y):
+ width, height = self.get_size()
+ if 0 <= x < width and 0 <= y < height:
+ index, _ = self.layout_manager.glyphIndexForPoint_inTextContainer_fractionOfDistanceThroughGlyph_(NSPoint(x, y), self.text_container, None)
+ return index
+ else:
+ return None
+
+ def draw(self, context, x, y, width, height):
+ if self.shadow is not None:
+ context.save()
+ context.set_shadow(self.shadow.color, self.shadow.opacity, self.shadow.offset, self.shadow.blur_radius)
+ self.width = width
+ self.text_container.setContainerSize_(NSSize(width, height))
+ glyph_range = self.layout_manager.glyphRangeForTextContainer_(self.text_container)
+ self.layout_manager.drawGlyphsForGlyphRange_atPoint_(glyph_range, NSPoint(x, y))
+ if self.shadow is not None:
+ context.restore()
+ context.path.removeAllPoints()
+
+class NativeButton(object):
+
+ def __init__(self, text, font, pressed, disabled=False):
+ self.min_width = 0
+ self.cell = NSButtonCell.alloc().init()
+ self.cell.setBezelStyle_(NSRoundRectBezelStyle)
+ self.cell.setButtonType_(NSMomentaryPushInButton)
+ self.cell.setFont_(font.nsfont)
+ self.cell.setEnabled_(not disabled)
+ self.cell.setTitle_(text)
+ if pressed:
+ self.cell.setState_(NSOnState)
+ else:
+ self.cell.setState_(NSOffState)
+ self.cell.setImagePosition_(NSImageLeft)
+
+ def set_icon(self, icon):
+ image = icon.image.copy()
+ image.setFlipped_(NO)
+ self.cell.setImage_(image)
+
+ def get_size(self):
+ size = self.cell.cellSize()
+ return size.width, size.height
+
+ def draw(self, context, x, y, width, height):
+ rect = NSMakeRect(x, y, width, height)
+ NSGraphicsContext.currentContext().saveGraphicsState()
+ self.cell.drawWithFrame_inView_(rect, context.view)
+ NSGraphicsContext.currentContext().restoreGraphicsState()
+ context.path.removeAllPoints()
+
+class StyledButton(object):
+ PAD_HORIZONTAL = 11
+ BIG_PAD_VERTICAL = 4
+ SMALL_PAD_VERTICAL = 2
+ 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.19, 0.19, 0.19)
+ DISABLED_COLOR = (0.86, 0.86, 0.86)
+ DISABLED_TEXT_COLOR = (0.43, 0.43, 0.43)
+ ICON_PAD = 8
+
+ def __init__(self, text, font, pressed, disabled=False):
+ self.pressed = pressed
+ self.disabled = disabled
+ attributes = NSMutableDictionary.alloc().init()
+ attributes.setObject_forKey_(font.nsfont, NSFontAttributeName)
+ if self.disabled:
+ color = self.DISABLED_TEXT_COLOR
+ else:
+ color = self.TEXT_COLOR
+ nscolor = NSColor.colorWithDeviceRed_green_blue_alpha_(color[0], color[1], color[2], 1.0)
+ attributes.setObject_forKey_(nscolor, NSForegroundColorAttributeName)
+ self.title = NSAttributedString.alloc().initWithString_attributes_(text, attributes)
+ self.image = None
+
+ def set_icon(self, icon):
+ self.image = icon.image.copy()
+ self.image.setFlipped_(YES)
+
+ def get_size(self):
+ width, height = self.get_text_size()
+ if self.image is not None:
+ width += self.image.size().width + self.ICON_PAD
+ height = max(height, self.image.size().height)
+ height += self.BIG_PAD_VERTICAL * 2
+ else:
+ height += self.SMALL_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
+ return width, height
+
+ def get_text_size(self):
+ size = self.title.size()
+ return size.width, size.height
+
+ def draw(self, context, x, y, width, height):
+ self._draw_button(context, x, y, width, height)
+ self._draw_title(context, x, y)
+ context.path.removeAllPoints()
+
+ def _draw_button(self, context, x, y, width, height):
+ radius = height / 2
+ 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 = drawing.Gradient(x, y, x, y+height)
+ gradient.set_start_color(start_color)
+ gradient.set_end_color(end_color)
+ context.gradient_fill(gradient)
+ self._draw_border(context, x, y, width, height, radius)
+
+ 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_path_reverse(self, context, x, y, width, height, radius):
+ inner_width = width - radius * 2
+ context.move_to(x + radius, y)
+ context.arc_negative(x + radius, y+radius, radius, -math.pi/2, math.pi/2)
+ context.rel_line_to(inner_width, 0)
+ context.arc_negative(x + width - radius, y+radius, radius, math.pi/2, -math.pi/2)
+ context.rel_line_to(-inner_width, 0)
+
+ def _draw_border(self, context, x, y, width, height, radius):
+ self._draw_path(context, x, y, width, height, radius)
+ self._draw_path_reverse(context, x+1, y+1, width-2, height-2, radius-1)
+ gradient = drawing.Gradient(x, y, x, y+height)
+ gradient.set_start_color(self.LINE_COLOR_TOP)
+ gradient.set_end_color(self.LINE_COLOR_BOTTOM)
+ context.save()
+ context.clip()
+ context.rectangle(x, y, width, height)
+ context.gradient_fill(gradient)
+ context.restore()
+
+ def _draw_title(self, context, x, y):
+ c_width, c_height = self.get_size()
+ t_width, t_height = self.get_text_size()
+ x = x + self.PAD_HORIZONTAL
+ y = y + (c_height - t_height) / 2
+ if self.image is not None:
+ self.image.drawAtPoint_fromRect_operation_fraction_(
+ NSPoint(x, y+3), NSZeroRect, NSCompositeSourceOver, 1.0)
+ x += self.image.size().width + self.ICON_PAD
+ else:
+ y += 0.5
+ self.title.drawAtPoint_(NSPoint(x, y))