Repository: kivy/kivy

Issue:
Add 'is_shortened' BooleanProperty to Label
There should be a boolean property that can be checked to find out if the label was shortened or not.
One use case is to display the full text in a tooltip, on hover.



Discussion:
Maybe an event is better? So one can have access to the shortened text? Although a prop is probably just as good.

A prop might be better, with an event only, one would end up needing to store the state in a prop.
But yeah, we could have `shortened_text` instead of a boolean.

Yeah, in which case, a value of `None` means no shortened text.


Resolving patch:
diff --git a/kivy/core/text/__init__.py b/kivy/core/text/__init__.py
--- a/kivy/core/text/__init__.py
+++ b/kivy/core/text/__init__.py
@@ -217,6 +217,7 @@ def __init__(
 
         self.options = options
         self.texture = None
+        self.is_shortened = False
         self.resolve_font_name()
 
     @staticmethod
@@ -395,6 +396,7 @@ def shorten(self, text, margin=2):
         dir = opts['shorten_from'][0]
         elps = textwidth('...')[0]
         if elps > uw:
+            self.is_shortened = True
             if textwidth('..')[0] <= uw:
                 return '..'
             else:
@@ -412,6 +414,7 @@ def shorten(self, text, margin=2):
                 l1 = textwidth(text[:e1])[0]
                 l2 = textwidth(text[s2 + 1:])[0]
             if e1 == -1 or l1 + l2 > uw:
+                self.is_shortened = True
                 if len(c):
                     opts['split_str'] = ''
                     res = self.shorten(text, margin)
@@ -424,6 +427,7 @@ def shorten(self, text, margin=2):
 
             # both word fits, and there's at least on split_str
             if s2 == e1:  # there's only on split_str
+                self.is_shortened = True
                 return chr('{0}...{1}').format(text[:e1], text[s2 + 1:])
 
             # both the first and last word fits, and they start/end at diff pos
@@ -459,6 +463,7 @@ def shorten(self, text, margin=2):
                 l1 = textwidth(text[:max(0, e1)])[0]
             # if split_str
             if s2 == -1 or l2 + l1 > uw:
+                self.is_shortened = True
                 if len(c):
                     opts['split_str'] = ''
                     res = self.shorten(text, margin)
@@ -469,6 +474,7 @@ def shorten(self, text, margin=2):
 
             # both word fits, and there's at least on split_str
             if s2 == e1:  # there's only on split_str
+                self.is_shortened = True
                 return chr('{0}...{1}').format(text[:e1], text[s2 + 1:])
 
             # both the first and last word fits, and they start/end at diff pos
@@ -479,6 +485,7 @@ def shorten(self, text, margin=2):
                     break
                 ss2 = f_rev(0, s2 - offset)
 
+        self.is_shortened = True
         return chr('{0}...{1}').format(text[:e1], text[s2 + 1:])
 
     def _default_line_options(self, lines):
@@ -603,8 +610,11 @@ def render(self, real=False):
         text = self.text
         if strip:
             text = text.strip()
+
+        self.is_shortened = False
         if uw is not None and options['shorten']:
             text = self.shorten(text)
+
         self._cached_lines = lines = []
         if not text:
             return 0, 0
diff --git a/kivy/core/text/markup.py b/kivy/core/text/markup.py
--- a/kivy/core/text/markup.py
+++ b/kivy/core/text/markup.py
@@ -236,16 +236,25 @@ def _pre_render(self):
                 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, True, False)
+                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(), True, True)
+            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
@@ -351,10 +360,18 @@ def _pre_render(self):
                     # 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 = 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
 
@@ -624,8 +641,13 @@ def p_restricted(line, uw, c):
         lw = sum([word.lw for word in line])
         if lw <= uw:
             lh = max([word.lh for word in line] + [0]) * line_height
-            return lw + 2 * xpad, lh + 2 * ypad, [LayoutLine(0, 0,
-            lw, lh, 1, 0, line)]
+            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:
@@ -636,16 +658,27 @@ def p_restricted(line, uw, c):
         # 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], '..')])])
+                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], '.')])])
+                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
@@ -664,6 +697,7 @@ def p_restricted(line, uw, c):
                     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]
@@ -679,8 +713,12 @@ def p_restricted(line, uw, c):
                 lw = sum([word.lw for word in line1])
                 lh = max([word.lh for word in line1]) * line_height
                 self.options = old_opts
-                return lw + 2 * xpad, lh + 2 * ypad, [LayoutLine(0, 0,
-                    lw, lh, 1, 0, line1)]
+                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
@@ -723,6 +761,7 @@ def p_restricted(line, uw, c):
                     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:]
@@ -737,8 +776,12 @@ def p_restricted(line, uw, c):
                 lw = sum([word.lw for word in line1])
                 lh = max([word.lh for word in line1]) * line_height
                 self.options = old_opts
-                return lw + 2 * xpad, lh + 2 * ypad, [LayoutLine(0, 0,
-                    lw, lh, 1, 0, line1)]
+                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
@@ -775,5 +818,10 @@ def p_restricted(line, uw, c):
         lw = sum([word.lw for word in line1])
         lh = max([word.lh for word in line1]) * line_height
         self.options = old_opts
-        return lw + 2 * xpad, lh + 2 * ypad, [LayoutLine(0, 0,
-            lw, lh, 1, 0, line1)]
+        if uw < lw:
+            self.is_shortened = True
+        return (
+            lw + 2 * xpad,
+            lh + 2 * ypad,
+            [LayoutLine(0, 0, lw, lh, 1, 0, line1)]
+        )
diff --git a/kivy/uix/label.py b/kivy/uix/label.py
--- a/kivy/uix/label.py
+++ b/kivy/uix/label.py
@@ -345,6 +345,7 @@ def texture_update(self, *largs):
                 (self.halign == 'justify' or self.strip) and
                 not self._label.text.strip()):
             self.texture_size = (0, 0)
+            self.is_shortened = False
             if mrkup:
                 self.refs, self._label._refs = {}, {}
                 self.anchors, self._label._anchors = {}, {}
@@ -373,6 +374,7 @@ def texture_update(self, *largs):
             if texture is not None:
                 self.texture = self._label.texture
                 self.texture_size = list(self.texture.size)
+            self.is_shortened = self._label.is_shortened
 
     def on_touch_down(self, touch):
         if super(Label, self).on_touch_down(touch):
@@ -736,6 +738,16 @@ def on_ref_press(self, ref):
     defaults to `center`.
     '''
 
+    is_shortened = BooleanProperty(False)
+    '''This property indicates if :attr:`text` was rendered with or without
+    shortening when :attr:`shorten` is True.
+
+    .. versionadded:: 1.9.2
+
+    :attr:`is_shortened` is a :class:`~kivy.properties.BooleanProperty` and
+    defaults to False.
+    '''
+
     split_str = StringProperty('')
     '''The string used to split the :attr:`text` while shortening the string
     when :attr:`shorten` is True.


Test patch: