Ajout du GUI
This commit is contained in:
879
kivy/core/text/markup.py
Normal file
879
kivy/core/text/markup.py
Normal file
@@ -0,0 +1,879 @@
|
||||
'''
|
||||
Text Markup
|
||||
===========
|
||||
|
||||
.. versionadded:: 1.1.0
|
||||
|
||||
.. versionchanged:: 1.10.1
|
||||
|
||||
Added `font_context`, `font_features` and `text_language` (Pango only)
|
||||
|
||||
We provide a simple text-markup for inline text styling. The syntax look the
|
||||
same as the `BBCode <http://en.wikipedia.org/wiki/BBCode>`_.
|
||||
|
||||
A tag is defined as ``[tag]``, and should have a corresponding
|
||||
``[/tag]`` closing tag. For example::
|
||||
|
||||
[b]Hello [color=ff0000]world[/color][/b]
|
||||
|
||||
The following tags are available:
|
||||
|
||||
``[b][/b]``
|
||||
Activate bold text
|
||||
``[i][/i]``
|
||||
Activate italic text
|
||||
``[u][/u]``
|
||||
Underlined text
|
||||
``[s][/s]``
|
||||
Strikethrough text
|
||||
``[font=<str>][/font]``
|
||||
Change the font (note: this refers to a TTF file or registered alias)
|
||||
``[font_context=<str>][/font_context]``
|
||||
Change context for the font, use string value "none" for isolated context.
|
||||
``[font_family=<str>][/font_family]``
|
||||
Font family to request for drawing. This is only valid when using a
|
||||
font context, and takes precedence over `[font]`. See
|
||||
:class:`kivy.uix.label.Label` for details.
|
||||
``[font_features=<str>][/font_features]``
|
||||
OpenType font features, in CSS format, this is passed straight
|
||||
through to Pango. The effects of requesting a feature depends on loaded
|
||||
fonts, library versions, etc. Pango only, requires v1.38 or later.
|
||||
``[size=<size>][/size]``
|
||||
Change the font size. <size> should be an integer, optionally with a
|
||||
unit (i.e. ``16sp``)
|
||||
``[color=#<color>][/color]``
|
||||
Change the text color
|
||||
``[ref=<str>][/ref]``
|
||||
Add an interactive zone. The reference + all the word box inside the
|
||||
reference will be available in :attr:`MarkupLabel.refs`
|
||||
``[anchor=<str>]``
|
||||
Put an anchor in the text. You can get the position of your anchor within
|
||||
the text with :attr:`MarkupLabel.anchors`
|
||||
``[sub][/sub]``
|
||||
Display the text at a subscript position relative to the text before it.
|
||||
``[sup][/sup]``
|
||||
Display the text at a superscript position relative to the text before it.
|
||||
``[text_language=<str>][/text_language]``
|
||||
Language of the text, this is an RFC-3066 format language tag (as string),
|
||||
for example "en_US", "zh_CN", "fr" or "ja". This can impact font selection,
|
||||
metrics and rendering. For example, the same bytes of text can look
|
||||
different for `ur` and `ar` languages, though both use Arabic script.
|
||||
Use the string `'none'` to revert to locale detection. Pango only.
|
||||
|
||||
If you need to escape the markup from the current text, use
|
||||
:func:`kivy.utils.escape_markup`.
|
||||
'''
|
||||
|
||||
__all__ = ('MarkupLabel', )
|
||||
|
||||
import re
|
||||
from kivy.properties import dpi2px
|
||||
from kivy.parser import parse_color
|
||||
from kivy.logger import Logger
|
||||
from kivy.core.text import Label, LabelBase
|
||||
from kivy.core.text.text_layout import layout_text, LayoutWord, LayoutLine
|
||||
from copy import copy
|
||||
from functools import partial
|
||||
|
||||
# We need to do this trick when documentation is generated
|
||||
MarkupLabelBase = Label
|
||||
if Label is None:
|
||||
MarkupLabelBase = LabelBase
|
||||
|
||||
|
||||
class MarkupLabel(MarkupLabelBase):
|
||||
'''Markup text label.
|
||||
|
||||
See module documentation for more information.
|
||||
'''
|
||||
|
||||
def __init__(self, *largs, **kwargs):
|
||||
self._style_stack = {}
|
||||
self._refs = {}
|
||||
self._anchors = {}
|
||||
super(MarkupLabel, self).__init__(*largs, **kwargs)
|
||||
self._internal_size = 0, 0
|
||||
self._cached_lines = []
|
||||
|
||||
@property
|
||||
def refs(self):
|
||||
'''Get the bounding box of all the ``[ref=...]``::
|
||||
|
||||
{ 'refA': ((x1, y1, x2, y2), (x1, y1, x2, y2)), ... }
|
||||
'''
|
||||
return self._refs
|
||||
|
||||
@property
|
||||
def anchors(self):
|
||||
'''Get the position of all the ``[anchor=...]``::
|
||||
|
||||
{ 'anchorA': (x, y), 'anchorB': (x, y), ... }
|
||||
'''
|
||||
return self._anchors
|
||||
|
||||
@property
|
||||
def markup(self):
|
||||
'''Return the text with all the markup split::
|
||||
|
||||
>>> MarkupLabel('[b]Hello world[/b]').markup
|
||||
>>> ('[b]', 'Hello world', '[/b]')
|
||||
|
||||
'''
|
||||
s = re.split(r'(\[.*?\])', self.label)
|
||||
s = [x for x in s if x != '']
|
||||
return s
|
||||
|
||||
def _push_style(self, k):
|
||||
if k not in self._style_stack:
|
||||
self._style_stack[k] = []
|
||||
self._style_stack[k].append(self.options[k])
|
||||
|
||||
def _pop_style(self, k):
|
||||
if k not in self._style_stack or len(self._style_stack[k]) == 0:
|
||||
Logger.warning('Label: pop style stack without push')
|
||||
return
|
||||
v = self._style_stack[k].pop()
|
||||
self.options[k] = v
|
||||
|
||||
def render(self, real=False):
|
||||
options = copy(self.options)
|
||||
if not real:
|
||||
ret = self._pre_render()
|
||||
else:
|
||||
ret = self._render_real()
|
||||
self.options = options
|
||||
return ret
|
||||
|
||||
def _pre_render(self):
|
||||
# split markup, words, and lines
|
||||
# result: list of word with position and width/height
|
||||
# during the first pass, we don't care about h/valign
|
||||
self._cached_lines = lines = []
|
||||
self._refs = {}
|
||||
self._anchors = {}
|
||||
clipped = False
|
||||
w = h = 0
|
||||
uw, uh = self.text_size
|
||||
spush = self._push_style
|
||||
spop = self._pop_style
|
||||
options = self.options
|
||||
options['_ref'] = None
|
||||
options['_anchor'] = None
|
||||
options['script'] = 'normal'
|
||||
shorten = options['shorten']
|
||||
# if shorten, then don't split lines to fit uw, because it will be
|
||||
# flattened later when shortening and broken up lines if broken
|
||||
# mid-word will have space mid-word when lines are joined
|
||||
uw_temp = None if shorten else uw
|
||||
xpad = options['padding_x']
|
||||
uhh = (None if uh is not None and options['valign'] != 'top' or
|
||||
options['shorten'] else uh)
|
||||
options['strip'] = options['strip'] or options['halign'] == 'justify'
|
||||
find_base_dir = Label.find_base_direction
|
||||
base_dir = options['base_direction']
|
||||
self._resolved_base_dir = None
|
||||
for item in self.markup:
|
||||
if item == '[b]':
|
||||
spush('bold')
|
||||
options['bold'] = True
|
||||
self.resolve_font_name()
|
||||
elif item == '[/b]':
|
||||
spop('bold')
|
||||
self.resolve_font_name()
|
||||
elif item == '[i]':
|
||||
spush('italic')
|
||||
options['italic'] = True
|
||||
self.resolve_font_name()
|
||||
elif item == '[/i]':
|
||||
spop('italic')
|
||||
self.resolve_font_name()
|
||||
elif item == '[u]':
|
||||
spush('underline')
|
||||
options['underline'] = True
|
||||
self.resolve_font_name()
|
||||
elif item == '[/u]':
|
||||
spop('underline')
|
||||
self.resolve_font_name()
|
||||
elif item == '[s]':
|
||||
spush('strikethrough')
|
||||
options['strikethrough'] = True
|
||||
self.resolve_font_name()
|
||||
elif item == '[/s]':
|
||||
spop('strikethrough')
|
||||
self.resolve_font_name()
|
||||
elif item[:6] == '[size=':
|
||||
item = item[6:-1]
|
||||
try:
|
||||
if item[-2:] in ('px', 'pt', 'in', 'cm', 'mm', 'dp', 'sp'):
|
||||
size = dpi2px(item[:-2], item[-2:])
|
||||
else:
|
||||
size = int(item)
|
||||
except ValueError:
|
||||
raise
|
||||
size = options['font_size']
|
||||
spush('font_size')
|
||||
options['font_size'] = size
|
||||
elif item == '[/size]':
|
||||
spop('font_size')
|
||||
elif item[:7] == '[color=':
|
||||
color = parse_color(item[7:-1])
|
||||
spush('color')
|
||||
options['color'] = color
|
||||
elif item == '[/color]':
|
||||
spop('color')
|
||||
elif item[:6] == '[font=':
|
||||
fontname = item[6:-1]
|
||||
spush('font_name')
|
||||
options['font_name'] = fontname
|
||||
self.resolve_font_name()
|
||||
elif item == '[/font]':
|
||||
spop('font_name')
|
||||
self.resolve_font_name()
|
||||
elif item[:13] == '[font_family=':
|
||||
spush('font_family')
|
||||
options['font_family'] = item[13:-1]
|
||||
elif item == '[/font_family]':
|
||||
spop('font_family')
|
||||
elif item[:14] == '[font_context=':
|
||||
fctx = item[14:-1]
|
||||
if not fctx or fctx.lower() == 'none':
|
||||
fctx = None
|
||||
spush('font_context')
|
||||
options['font_context'] = fctx
|
||||
elif item == '[/font_context]':
|
||||
spop('font_context')
|
||||
elif item[:15] == '[font_features=':
|
||||
spush('font_features')
|
||||
options['font_features'] = item[15:-1]
|
||||
elif item == '[/font_features]':
|
||||
spop('font_features')
|
||||
elif item[:15] == '[text_language=':
|
||||
lang = item[15:-1]
|
||||
if not lang or lang.lower() == 'none':
|
||||
lang = None
|
||||
spush('text_language')
|
||||
options['text_language'] = lang
|
||||
elif item == '[/text_language]':
|
||||
spop('text_language')
|
||||
elif item[:5] == '[sub]':
|
||||
spush('font_size')
|
||||
spush('script')
|
||||
options['font_size'] = options['font_size'] * .5
|
||||
options['script'] = 'subscript'
|
||||
elif item == '[/sub]':
|
||||
spop('font_size')
|
||||
spop('script')
|
||||
elif item[:5] == '[sup]':
|
||||
spush('font_size')
|
||||
spush('script')
|
||||
options['font_size'] = options['font_size'] * .5
|
||||
options['script'] = 'superscript'
|
||||
elif item == '[/sup]':
|
||||
spop('font_size')
|
||||
spop('script')
|
||||
elif item[:5] == '[ref=':
|
||||
ref = item[5:-1]
|
||||
spush('_ref')
|
||||
options['_ref'] = ref
|
||||
elif item == '[/ref]':
|
||||
spop('_ref')
|
||||
elif not clipped and item[:8] == '[anchor=':
|
||||
options['_anchor'] = item[8:-1]
|
||||
elif not clipped:
|
||||
item = item.replace('&bl;', '[').replace(
|
||||
'&br;', ']').replace('&', '&')
|
||||
if not base_dir:
|
||||
base_dir = self._resolved_base_dir = find_base_dir(item)
|
||||
opts = copy(options)
|
||||
extents = self.get_cached_extents()
|
||||
opts['space_width'] = extents(' ')[0]
|
||||
w, h, clipped = layout_text(
|
||||
item, lines, (w, h), (uw_temp, uhh),
|
||||
opts, extents,
|
||||
append_down=True,
|
||||
complete=False
|
||||
)
|
||||
|
||||
if len(lines): # remove any trailing spaces from the last line
|
||||
old_opts = self.options
|
||||
self.options = copy(opts)
|
||||
w, h, clipped = layout_text(
|
||||
'', lines, (w, h), (uw_temp, uhh),
|
||||
self.options, self.get_cached_extents(),
|
||||
append_down=True,
|
||||
complete=True
|
||||
)
|
||||
self.options = old_opts
|
||||
|
||||
self.is_shortened = False
|
||||
if shorten:
|
||||
options['_ref'] = None # no refs for you!
|
||||
options['_anchor'] = None
|
||||
w, h, lines = self.shorten_post(lines, w, h)
|
||||
self._cached_lines = lines
|
||||
# when valign is not top, for markup we layout everything (text_size[1]
|
||||
# is temporarily set to None) and after layout cut to size if too tall
|
||||
elif uh != uhh and h > uh and len(lines) > 1:
|
||||
if options['valign'] == 'bottom':
|
||||
i = 0
|
||||
while i < len(lines) - 1 and h > uh:
|
||||
h -= lines[i].h
|
||||
i += 1
|
||||
del lines[:i]
|
||||
else: # middle
|
||||
i = 0
|
||||
top = int(h / 2. + uh / 2.) # remove extra top portion
|
||||
while i < len(lines) - 1 and h > top:
|
||||
h -= lines[i].h
|
||||
i += 1
|
||||
del lines[:i]
|
||||
i = len(lines) - 1 # remove remaining bottom portion
|
||||
while i and h > uh:
|
||||
h -= lines[i].h
|
||||
i -= 1
|
||||
del lines[i + 1:]
|
||||
|
||||
# now justify the text
|
||||
if options['halign'] == 'justify' and uw is not None:
|
||||
# XXX: update refs to justified pos
|
||||
# when justify, each line should've been stripped already
|
||||
split = partial(re.split, re.compile('( +)'))
|
||||
uww = uw - 2 * xpad
|
||||
chr = type(self.text)
|
||||
space = chr(' ')
|
||||
empty = chr('')
|
||||
|
||||
for i in range(len(lines)):
|
||||
line = lines[i]
|
||||
words = line.words
|
||||
# if there's nothing to justify, we're done
|
||||
if (not line.w or int(uww - line.w) <= 0 or not len(words) or
|
||||
line.is_last_line):
|
||||
continue
|
||||
|
||||
done = False
|
||||
parts = [None, ] * len(words) # contains words split by space
|
||||
idxs = [None, ] * len(words) # indices of the space in parts
|
||||
# break each word into spaces and add spaces until it's full
|
||||
# do first round of split in case we don't need to split all
|
||||
for w in range(len(words)):
|
||||
word = words[w]
|
||||
sw = word.options['space_width']
|
||||
p = parts[w] = split(word.text)
|
||||
idxs[w] = [v for v in range(len(p)) if
|
||||
p[v].startswith(' ')]
|
||||
# now we have the indices of the spaces in split list
|
||||
for k in idxs[w]:
|
||||
# try to add single space at each space
|
||||
if line.w + sw > uww:
|
||||
done = True
|
||||
break
|
||||
line.w += sw
|
||||
word.lw += sw
|
||||
p[k] += space
|
||||
if done:
|
||||
break
|
||||
|
||||
# there's not a single space in the line?
|
||||
if not any(idxs):
|
||||
continue
|
||||
|
||||
# now keep adding spaces to already split words until done
|
||||
while not done:
|
||||
for w in range(len(words)):
|
||||
if not idxs[w]:
|
||||
continue
|
||||
word = words[w]
|
||||
sw = word.options['space_width']
|
||||
p = parts[w]
|
||||
for k in idxs[w]:
|
||||
# try to add single space at each space
|
||||
if line.w + sw > uww:
|
||||
done = True
|
||||
break
|
||||
line.w += sw
|
||||
word.lw += sw
|
||||
p[k] += space
|
||||
if done:
|
||||
break
|
||||
|
||||
# if not completely full, push last words to right edge
|
||||
diff = int(uww - line.w)
|
||||
if diff > 0:
|
||||
# find the last word that had a space
|
||||
for w in range(len(words) - 1, -1, -1):
|
||||
if not idxs[w]:
|
||||
continue
|
||||
break
|
||||
old_opts = self.options
|
||||
self.options = word.options
|
||||
word = words[w]
|
||||
# split that word into left/right and push right till uww
|
||||
l_text = empty.join(parts[w][:idxs[w][-1]])
|
||||
r_text = empty.join(parts[w][idxs[w][-1]:])
|
||||
left = LayoutWord(
|
||||
word.options,
|
||||
self.get_extents(l_text)[0],
|
||||
word.lh,
|
||||
l_text
|
||||
)
|
||||
right = LayoutWord(
|
||||
word.options,
|
||||
self.get_extents(r_text)[0],
|
||||
word.lh,
|
||||
r_text
|
||||
)
|
||||
left.lw = max(left.lw, word.lw + diff - right.lw)
|
||||
self.options = old_opts
|
||||
|
||||
# now put words back together with right/left inserted
|
||||
for k in range(len(words)):
|
||||
if idxs[k]:
|
||||
words[k].text = empty.join(parts[k])
|
||||
words[w] = right
|
||||
words.insert(w, left)
|
||||
else:
|
||||
for k in range(len(words)):
|
||||
if idxs[k]:
|
||||
words[k].text = empty.join(parts[k])
|
||||
line.w = uww
|
||||
w = max(w, uww)
|
||||
|
||||
self._internal_size = w, h
|
||||
if uw:
|
||||
w = uw
|
||||
if uh:
|
||||
h = uh
|
||||
if h > 1 and w < 2:
|
||||
w = 2
|
||||
if w < 1:
|
||||
w = 1
|
||||
if h < 1:
|
||||
h = 1
|
||||
return int(w), int(h)
|
||||
|
||||
def render_lines(self, lines, options, render_text, y, size):
|
||||
xpad = options['padding_x']
|
||||
w = size[0]
|
||||
halign = options['halign']
|
||||
refs = self._refs
|
||||
anchors = self._anchors
|
||||
base_dir = options['base_direction'] or self._resolved_base_dir
|
||||
auto_halign_r = halign == 'auto' and base_dir and 'rtl' in base_dir
|
||||
|
||||
for layout_line in lines: # for plain label each line has only one str
|
||||
lw, lh = layout_line.w, layout_line.h
|
||||
x = xpad
|
||||
if halign == 'center':
|
||||
x = int((w - lw) / 2.)
|
||||
elif halign == 'right' or auto_halign_r:
|
||||
x = max(0, int(w - lw - xpad))
|
||||
layout_line.x = x
|
||||
layout_line.y = y
|
||||
psp = pph = 0
|
||||
for word in layout_line.words:
|
||||
options = self.options = word.options
|
||||
# the word height is not scaled by line_height, only lh was
|
||||
wh = options['line_height'] * word.lh
|
||||
# calculate sub/super script pos
|
||||
if options['script'] == 'superscript':
|
||||
script_pos = max(0, psp if psp else self.get_descent())
|
||||
psp = script_pos
|
||||
pph = wh
|
||||
elif options['script'] == 'subscript':
|
||||
script_pos = min(lh - wh, ((psp + pph) - wh)
|
||||
if pph else (lh - wh))
|
||||
pph = wh
|
||||
psp = script_pos
|
||||
else:
|
||||
script_pos = (lh - wh) / 1.25
|
||||
psp = pph = 0
|
||||
if len(word.text):
|
||||
render_text(word.text, x, y + script_pos)
|
||||
|
||||
# should we record refs ?
|
||||
ref = options['_ref']
|
||||
if ref is not None:
|
||||
if ref not in refs:
|
||||
refs[ref] = []
|
||||
refs[ref].append((x, y, x + word.lw, y + wh))
|
||||
|
||||
# Should we record anchors?
|
||||
anchor = options['_anchor']
|
||||
if anchor is not None:
|
||||
if anchor not in anchors:
|
||||
anchors[anchor] = (x, y)
|
||||
x += word.lw
|
||||
y += lh
|
||||
return y
|
||||
|
||||
def shorten_post(self, lines, w, h, margin=2):
|
||||
''' Shortens the text to a single line according to the label options.
|
||||
|
||||
This function operates on a text that has already been laid out because
|
||||
for markup, parts of text can have different size and options.
|
||||
|
||||
If :attr:`text_size` [0] is None, the lines are returned unchanged.
|
||||
Otherwise, the lines are converted to a single line fitting within the
|
||||
constrained width, :attr:`text_size` [0].
|
||||
|
||||
:params:
|
||||
|
||||
`lines`: list of `LayoutLine` instances describing the text.
|
||||
`w`: int, the width of the text in lines, including padding.
|
||||
`h`: int, the height of the text in lines, including padding.
|
||||
`margin` int, the additional space left on the sides. This is in
|
||||
addition to :attr:`padding_x`.
|
||||
|
||||
:returns:
|
||||
3-tuple of (xw, h, lines), where w, and h is similar to the input
|
||||
and contains the resulting width / height of the text, including
|
||||
padding. lines, is a list containing a single `LayoutLine`, which
|
||||
contains the words for the line.
|
||||
'''
|
||||
|
||||
def n(line, c):
|
||||
''' A function similar to text.find, except it's an iterator that
|
||||
returns successive occurrences of string c in list line. line is
|
||||
not a string, but a list of LayoutWord instances that we walk
|
||||
from left to right returning the indices of c in the words as we
|
||||
encounter them. Note that the options can be different among the
|
||||
words.
|
||||
|
||||
:returns:
|
||||
3-tuple: the index of the word in line, the index of the
|
||||
occurrence in word, and the extents (width) of the combined
|
||||
words until this occurrence, not including the occurrence char.
|
||||
If no more are found it returns (-1, -1, total_w) where total_w
|
||||
is the full width of all the words.
|
||||
'''
|
||||
total_w = 0
|
||||
for w in range(len(line)):
|
||||
word = line[w]
|
||||
if not word.lw:
|
||||
continue
|
||||
f = partial(word.text.find, c)
|
||||
i = f()
|
||||
while i != -1:
|
||||
self.options = word.options
|
||||
yield w, i, total_w + self.get_extents(word.text[:i])[0]
|
||||
i = f(i + 1)
|
||||
self.options = word.options
|
||||
total_w += self.get_extents(word.text)[0]
|
||||
yield -1, -1, total_w # this should never be reached, really
|
||||
|
||||
def p(line, c):
|
||||
''' Similar to the `n` function, except it returns occurrences of c
|
||||
from right to left in the list, line, similar to rfind.
|
||||
'''
|
||||
total_w = 0
|
||||
offset = 0 if len(c) else 1
|
||||
for w in range(len(line) - 1, -1, -1):
|
||||
word = line[w]
|
||||
if not word.lw:
|
||||
continue
|
||||
f = partial(word.text.rfind, c)
|
||||
i = f()
|
||||
while i != -1:
|
||||
self.options = word.options
|
||||
yield (w, i, total_w +
|
||||
self.get_extents(word.text[i + 1:])[0])
|
||||
if i:
|
||||
i = f(0, i - offset)
|
||||
else:
|
||||
if not c:
|
||||
self.options = word.options
|
||||
yield (w, -1, total_w +
|
||||
self.get_extents(word.text)[0])
|
||||
break
|
||||
self.options = word.options
|
||||
total_w += self.get_extents(word.text)[0]
|
||||
yield -1, -1, total_w # this should never be reached, really
|
||||
|
||||
def n_restricted(line, uw, c):
|
||||
''' Similar to the function `n`, except it only returns the first
|
||||
occurrence and it's not an iterator. Furthermore, if the first
|
||||
occurrence doesn't fit within width uw, it returns the index of
|
||||
whatever amount of text will still fit in uw.
|
||||
|
||||
:returns:
|
||||
similar to the function `n`, except it's a 4-tuple, with the
|
||||
last element a boolean, indicating if we had to clip the text
|
||||
to fit in uw (True) or if the whole text until the first
|
||||
occurrence fitted in uw (False).
|
||||
'''
|
||||
total_w = 0
|
||||
if not len(line):
|
||||
return 0, 0, 0
|
||||
for w in range(len(line)):
|
||||
word = line[w]
|
||||
f = partial(word.text.find, c)
|
||||
self.options = word.options
|
||||
extents = self.get_cached_extents()
|
||||
i = f()
|
||||
if i != -1:
|
||||
ww = extents(word.text[:i])[0]
|
||||
|
||||
if i != -1 and total_w + ww <= uw: # found and it fits
|
||||
return w, i, total_w + ww, False
|
||||
elif i == -1:
|
||||
ww = extents(word.text)[0]
|
||||
if total_w + ww <= uw: # wasn't found and all fits
|
||||
total_w += ww
|
||||
continue
|
||||
i = len(word.text)
|
||||
|
||||
# now just find whatever amount of the word does fit
|
||||
e = 0
|
||||
while e != i and total_w + extents(word.text[:e])[0] <= uw:
|
||||
e += 1
|
||||
e = max(0, e - 1)
|
||||
return w, e, total_w + extents(word.text[:e])[0], True
|
||||
|
||||
return -1, -1, total_w, False
|
||||
|
||||
def p_restricted(line, uw, c):
|
||||
''' Similar to `n_restricted`, except it returns the first
|
||||
occurrence starting from the right, like `p`.
|
||||
'''
|
||||
total_w = 0
|
||||
if not len(line):
|
||||
return 0, 0, 0
|
||||
for w in range(len(line) - 1, -1, -1):
|
||||
word = line[w]
|
||||
f = partial(word.text.rfind, c)
|
||||
self.options = word.options
|
||||
extents = self.get_cached_extents()
|
||||
i = f()
|
||||
if i != -1:
|
||||
ww = extents(word.text[i + 1:])[0]
|
||||
|
||||
if i != -1 and total_w + ww <= uw: # found and it fits
|
||||
return w, i, total_w + ww, False
|
||||
elif i == -1:
|
||||
ww = extents(word.text)[0]
|
||||
if total_w + ww <= uw: # wasn't found and all fits
|
||||
total_w += ww
|
||||
continue
|
||||
|
||||
# now just find whatever amount of the word does fit
|
||||
s = len(word.text) - 1
|
||||
while s >= 0 and total_w + extents(word.text[s:])[0] <= uw:
|
||||
s -= 1
|
||||
return w, s, total_w + extents(word.text[s + 1:])[0], True
|
||||
|
||||
return -1, -1, total_w, False
|
||||
|
||||
textwidth = self.get_cached_extents()
|
||||
uw = self.text_size[0]
|
||||
if uw is None:
|
||||
return w, h, lines
|
||||
old_opts = copy(self.options)
|
||||
uw = max(0, int(uw - old_opts['padding_x'] * 2 - margin))
|
||||
chr = type(self.text)
|
||||
ssize = textwidth(' ')
|
||||
c = old_opts['split_str']
|
||||
line_height = old_opts['line_height']
|
||||
xpad, ypad = old_opts['padding_x'], old_opts['padding_y']
|
||||
dir = old_opts['shorten_from'][0]
|
||||
|
||||
# flatten lines into single line
|
||||
line = []
|
||||
last_w = 0
|
||||
for l in range(len(lines)):
|
||||
# concatenate (non-empty) inside lines with a space
|
||||
this_line = lines[l]
|
||||
if last_w and this_line.w and not this_line.line_wrap:
|
||||
line.append(LayoutWord(old_opts, ssize[0], ssize[1], chr(' ')))
|
||||
last_w = this_line.w or last_w
|
||||
for word in this_line.words:
|
||||
if word.lw:
|
||||
line.append(word)
|
||||
|
||||
# if that fits, just return the flattened line
|
||||
lw = sum([word.lw for word in line])
|
||||
if lw <= uw:
|
||||
lh = max([word.lh for word in line] + [0]) * line_height
|
||||
self.is_shortened = False
|
||||
return (
|
||||
lw + 2 * xpad,
|
||||
lh + 2 * ypad,
|
||||
[LayoutLine(0, 0, lw, lh, 1, 0, line)]
|
||||
)
|
||||
|
||||
elps_opts = copy(old_opts)
|
||||
if 'ellipsis_options' in old_opts:
|
||||
elps_opts.update(old_opts['ellipsis_options'])
|
||||
|
||||
# Set new opts for ellipsis
|
||||
self.options = elps_opts
|
||||
# find the size of ellipsis that'll fit
|
||||
elps_s = textwidth('...')
|
||||
if elps_s[0] > uw: # even ellipsis didn't fit...
|
||||
self.is_shortened = True
|
||||
s = textwidth('..')
|
||||
if s[0] <= uw:
|
||||
return (
|
||||
s[0] + 2 * xpad,
|
||||
s[1] * line_height + 2 * ypad,
|
||||
[LayoutLine(
|
||||
0, 0, s[0], s[1], 1, 0,
|
||||
[LayoutWord(old_opts, s[0], s[1], '..')])]
|
||||
)
|
||||
|
||||
else:
|
||||
s = textwidth('.')
|
||||
return (
|
||||
s[0] + 2 * xpad,
|
||||
s[1] * line_height + 2 * ypad,
|
||||
[LayoutLine(
|
||||
0, 0, s[0], s[1], 1, 0,
|
||||
[LayoutWord(old_opts, s[0], s[1], '.')])]
|
||||
)
|
||||
|
||||
elps = LayoutWord(elps_opts, elps_s[0], elps_s[1], '...')
|
||||
uw -= elps_s[0]
|
||||
# Restore old opts
|
||||
self.options = old_opts
|
||||
|
||||
# now find the first left and right words that fit
|
||||
w1, e1, l1, clipped1 = n_restricted(line, uw, c)
|
||||
w2, s2, l2, clipped2 = p_restricted(line, uw, c)
|
||||
|
||||
if dir != 'l': # center or right
|
||||
line1 = None
|
||||
if clipped1 or clipped2 or l1 + l2 > uw:
|
||||
# if either was clipped or both don't fit, just take first
|
||||
if len(c):
|
||||
self.options = old_opts
|
||||
old_opts['split_str'] = ''
|
||||
res = self.shorten_post(lines, w, h, margin)
|
||||
self.options['split_str'] = c
|
||||
self.is_shortened = True
|
||||
return res
|
||||
line1 = line[:w1]
|
||||
last_word = line[w1]
|
||||
last_text = last_word.text[:e1]
|
||||
self.options = last_word.options
|
||||
s = self.get_extents(last_text)
|
||||
line1.append(LayoutWord(last_word.options, s[0], s[1],
|
||||
last_text))
|
||||
elif (w1, e1) == (-1, -1): # this shouldn't occur
|
||||
line1 = line
|
||||
if line1:
|
||||
line1.append(elps)
|
||||
lw = sum([word.lw for word in line1])
|
||||
lh = max([word.lh for word in line1]) * line_height
|
||||
self.options = old_opts
|
||||
self.is_shortened = True
|
||||
return (
|
||||
lw + 2 * xpad,
|
||||
lh + 2 * ypad,
|
||||
[LayoutLine(0, 0, lw, lh, 1, 0, line1)]
|
||||
)
|
||||
|
||||
# now we know that both the first and last word fit, and that
|
||||
# there's at least one instances of the split_str in the line
|
||||
if (w1, e1) != (w2, s2): # more than one split_str
|
||||
if dir == 'r':
|
||||
f = n(line, c) # iterator
|
||||
assert next(f)[:-1] == (w1, e1) # first word should match
|
||||
ww1, ee1, l1 = next(f)
|
||||
while l2 + l1 <= uw:
|
||||
w1, e1 = ww1, ee1
|
||||
ww1, ee1, l1 = next(f)
|
||||
if (w1, e1) == (w2, s2):
|
||||
break
|
||||
else: # center
|
||||
f = n(line, c) # iterator
|
||||
f_inv = p(line, c) # iterator
|
||||
assert next(f)[:-1] == (w1, e1)
|
||||
assert next(f_inv)[:-1] == (w2, s2)
|
||||
while True:
|
||||
if l1 <= l2:
|
||||
ww1, ee1, l1 = next(f) # hypothesize that next fit
|
||||
if l2 + l1 > uw:
|
||||
break
|
||||
w1, e1 = ww1, ee1
|
||||
if (w1, e1) == (w2, s2):
|
||||
break
|
||||
else:
|
||||
ww2, ss2, l2 = next(f_inv)
|
||||
if l2 + l1 > uw:
|
||||
break
|
||||
w2, s2 = ww2, ss2
|
||||
if (w1, e1) == (w2, s2):
|
||||
break
|
||||
else: # left
|
||||
line1 = [elps]
|
||||
if clipped1 or clipped2 or l1 + l2 > uw:
|
||||
# if either was clipped or both don't fit, just take last
|
||||
if len(c):
|
||||
self.options = old_opts
|
||||
old_opts['split_str'] = ''
|
||||
res = self.shorten_post(lines, w, h, margin)
|
||||
self.options['split_str'] = c
|
||||
self.is_shortened = True
|
||||
return res
|
||||
first_word = line[w2]
|
||||
first_text = first_word.text[s2 + 1:]
|
||||
self.options = first_word.options
|
||||
s = self.get_extents(first_text)
|
||||
line1.append(LayoutWord(first_word.options, s[0], s[1],
|
||||
first_text))
|
||||
line1.extend(line[w2 + 1:])
|
||||
elif (w1, e1) == (-1, -1): # this shouldn't occur
|
||||
line1 = line
|
||||
if len(line1) != 1:
|
||||
lw = sum([word.lw for word in line1])
|
||||
lh = max([word.lh for word in line1]) * line_height
|
||||
self.options = old_opts
|
||||
self.is_shortened = True
|
||||
return (
|
||||
lw + 2 * xpad,
|
||||
lh + 2 * ypad,
|
||||
[LayoutLine(0, 0, lw, lh, 1, 0, line1)]
|
||||
)
|
||||
|
||||
# now we know that both the first and last word fit, and that
|
||||
# there's at least one instances of the split_str in the line
|
||||
if (w1, e1) != (w2, s2): # more than one split_str
|
||||
f_inv = p(line, c) # iterator
|
||||
assert next(f_inv)[:-1] == (w2, s2) # last word should match
|
||||
ww2, ss2, l2 = next(f_inv)
|
||||
while l2 + l1 <= uw:
|
||||
w2, s2 = ww2, ss2
|
||||
ww2, ss2, l2 = next(f_inv)
|
||||
if (w1, e1) == (w2, s2):
|
||||
break
|
||||
|
||||
# now add back the left half
|
||||
line1 = line[:w1]
|
||||
last_word = line[w1]
|
||||
last_text = last_word.text[:e1]
|
||||
self.options = last_word.options
|
||||
s = self.get_extents(last_text)
|
||||
if len(last_text):
|
||||
line1.append(LayoutWord(last_word.options, s[0], s[1], last_text))
|
||||
line1.append(elps)
|
||||
|
||||
# now add back the right half
|
||||
first_word = line[w2]
|
||||
first_text = first_word.text[s2 + 1:]
|
||||
self.options = first_word.options
|
||||
s = self.get_extents(first_text)
|
||||
if len(first_text):
|
||||
line1.append(LayoutWord(first_word.options, s[0], s[1],
|
||||
first_text))
|
||||
line1.extend(line[w2 + 1:])
|
||||
|
||||
lw = sum([word.lw for word in line1])
|
||||
lh = max([word.lh for word in line1]) * line_height
|
||||
self.options = old_opts
|
||||
if uw < lw:
|
||||
self.is_shortened = True
|
||||
return (
|
||||
lw + 2 * xpad,
|
||||
lh + 2 * ypad,
|
||||
[LayoutLine(0, 0, lw, lh, 1, 0, line1)]
|
||||
)
|
||||
Reference in New Issue
Block a user