Forum Archive

UI label with italics?

peiriannydd

I'm trying to create a label that has one of the words italicized for vocabulary lists, eg:

लाल laal red
नीला neela blue
...

Is this possible? I only see Label.font and Label.text_color which seem to be for the whole label. Is it possible maybe to concatenate several labels with different attributes?

mikael

@peiriannydd, probably the easiest way about this would be to use ui.WebView and just put divs with whatever fonts and colors you want there. You would not even have to manage layout much.

Next easiest is to use separate labels, as you say. You can use for example ”ArialMT” and ”Arial-ItalicMT” fonts, or use the list here to find other combos. If you go this route, I would recommend anchors to make the ”label stacking” easy.

Most difficult option would be to use ObjC AttributedText to get the different styles within one label. Search the forum for ”AttributedText” for examples. @JonB and @cvp are probably the best experts around for this option.

cvp

@peiriannydd try this @JonB 's marvelous example here

peiriannydd

@mikael @cvp Thank you both, I will certainly be able to figure this out based on your suggestions. I appreciate your help.

cvp

@peiriannydd Quick and dirty

import ui
from objc_util import *

# main view
v = ui.View()
v.frame = (0,0,300,300)
v.background_color = 'white'
v.name = 'ui.Label with italic sub-part'

# label
l = ui.Label()
l.frame = (10,10,200,32)
t = 'लाल laal red'
l.text = t
l.font = ('ArialMT',20)
v.add_subview(l)

# italic sub-part
UIFont = ObjCClass('UIFont').fontWithName_size_('Arial-ItalicMT',20)
mystr = ObjCClass('NSMutableAttributedString').alloc().initWithString_(t)
tt = 'red'
st = t.find(tt)
ll = len(tt)
mystr.addAttribute_value_range_(ns('NSFont'),UIFont,NSRange(st,ll))

# red sub-part
UIColor=ObjCClass('UIColor').redColor()
mystr.addAttribute_value_range_(ns('NSColor'),UIColor,NSRange(st,ll))

lobj = ObjCInstance(l._objc_ptr)
lobj.setAttributedText_(mystr)

v.present('sheet') 

JonB

You might also consider a table view , which is natural for this application...you don’t need to muck around with locations of all the elements, or deal with scrolling.

Tableviewcells have a few different cell styles to show a title (your word) and a subtitle (definition/translation) either below, or left or right aligned after the title.

For example using Pythonista, See https://forum.omz-software.com/topic/941/tableviewcell-detail_text_label/3

mikael

@cvp, I took your code and put an HTML-ish syntax around it. Now using this code:

from richlabel import RichLabel

r = RichLabel(
    font=('Arial', 24),
    background_color='white',
    alignment=ui.ALIGN_CENTER,
    number_of_lines=0,
)

r.set_rich_text("\n".join([
    "Plain",
    "<f Zapfino><c red>Color</c></f>",
    "<b>Bold <i>italic</i></b>",
    "and <i><f system 32>just</f> italic</i>",
]))

r.present('fullscreen')

... you get this:

Result

The c or color tag takes any Pythonista color definition (name, hex or tuple). f or font tag expects a font name or a size or both.

Code is here.

Would be good to hear ideas/alternatives on the markup syntax.

cvp

@mikael Nice, as usual...

peiriannydd

@mikael very nice!

cvp

@mikael you could add an outline tag

# red sub-part
UIColor=ObjCClass('UIColor').redColor()
mystr.addAttribute_value_range_(ns('NSColor'),UIColor,NSRange(st,ll))

outlineWidth = -3   # width in percentage of font size
                                        # >0 => stroke only
                                        # <0 => stroke and fill color
outlineColor = ObjCClass('UIColor').colorWithRed_green_blue_alpha_(0,0,0,1)
mystr.addAttribute_value_range_(ns('NSStrokeColor'), outlineColor, NSRange(st,ll))
mystr.addAttribute_value_range_(ns('NSStrokeWidth'), outlineWidth, NSRange(st,ll))              

mikael

@cvp, fancy!

Ok, so now there is an o or outline tag that you can use as-is or with an optional color and/or width. Results with this string:

r.set_rich_text("\n".join([
    "Plain",
    "<f Zapfino><c red>Color</c></f>",
    "<b>Bold <i>italic</i></b>",
    "and <i><f system 32>just</f> italic</i>",
    "Outlines:",
    "<o>DEFAULT</o>",
    "<o 3>THICK</o>",
    "<o 3 blue>COLORED</o>",
    "<o -2><c orange>FILLED</c></o>",
]))

... look like this:

Result

cvp

@mikael sincerely, you're a champion

mikael

@cvp, now that you got me started, how could we say anything important without underlining it? (Strikethrough thrown in because it was just 4 extra lines of code.)

Underline tag is u or underline, strikethrough is s or strike. Both take a suitable combination of style qualifiers: thick, double, dot, dash, dashdot, dashdotdot, byword.

So now with this:

    "Plain",
    "<f Zapfino><c red>Color</c></f>",
    "<b>Bold <i>italic</i></b>",
    "and <i><f system 32>just</f> italic</i>\n",
    "<u>Outlines:</u>\n",
    "<o>DEFAULT</o>",
    "<o 3>THICK</o>",
    "<o 3 blue>COLORED</o>",
    "<o -2><c orange>FILLED</c></o>\n",
    "<s double byword>really not cool</s>"

... we get this:

Result

cvp

@mikael one more time, whaaaa

cvp

@mikael and a last one for today

UIColor=ObjCClass('UIColor').redColor()
mystr.addAttribute_value_range_(ns('NSColor'),UIColor,NSRange(st,ll))
shadow = ObjCClass('NSShadow').alloc().init()
shadow.setShadowOffset_(CGSize(2,2))
shadowColor = ObjCClass('UIColor').colorWithRed_green_blue_alpha_(0,0,0,1)
shadow.setShadowColor_(shadowColor)
shadow.setShadowBlurRadius_(3)
mystr.addAttribute_value_range_(ns('NSShadow'), shadow, NSRange(st,ll))          

mikael

@cvp, you ruthless taskmaster!

So, no more s tag, you now need to use the longer strike or shadow.

shadow can be customized with color (default 'grey'), offset (default (2,2)) and blur (default 3).

Now with:

    "Plain",
    "<b>Bold <i>italic</i></b>",
    "and <i><f system 32>just</f> italic</i>",
    "",
    "<f Zapfino><c red>Color</c>",
    "<shadow>Shadow</shadow></f>",
    "<u>Outlines:</u>",
    "<b>",
    "<o>DEFAULT</o>",
    "<o blue>COLORED</o>",
    "<o -3><c orange>FILLED</c></o>",
    "</b>",
    "<strike double byword>really not cool</s>"

We get:

Result

cvp

@mikael said:

you ruthless taskmaster!

Sorry, but I'm sure you like that 😀

And bravo, as usual

cvp

@mikael Finally, it is perhaps better to define the text in html and use standard functions

import ui
from objc_util import *

# main view
v = ui.View()
v.frame = (0,0,300,200)
v.background_color = 'white'
v.name = 'ui.Label with html'

# label
l = ui.Label()
l.frame = (10,0,200,200)
l.number_of_lines = 0
v.add_subview(l)

html = """
<html>
  <head>
    <title>Title of the document</title>
    <style>
      .shadow {
        text-shadow: 2px 2px #1c87c9;
      }
    </style>
  </head>
  <body>
    <h2>Text-shadow property example</h2>
    <p>Some paragraph for example.</p>
    <p class="shadow">Some paragraph with the text-shadow property.</p>
  </body>
</html> 
"""
data = ns(html.encode())
mystr = ObjCClass('NSMutableAttributedString').alloc().initWithHTML_documentAttributes_(data,None)

lobj = ObjCInstance(l._objc_ptr)
lobj.setAttributedText_(mystr)

v.present('sheet')  

mikael

@cvp, ooo, cool. The solution that ends all solutions.

... Although, there are several things I do not like about HTML styling and layouts, so I might still use the other option if I need rich text in my labels.

One thing I meant to ask: what does the objc_ptr do? Code seems to work without it.

cvp

@mikael said:

what does the objc_ptr do

I don't know 🙄, copied from old @JonB 's code (see above) and still remaining....

cvp

@mikael said:

there are several things I do not like about HTML styling and layouts, so I might still use the other option if I need rich text in my labels.

I agree

cvp

@mikael You could also add NSStrikethroughColor

I've modified your code only for testing, not Python's way

.
.
.
        def apply(self, attr_str):
            attr_str.addAttribute_value_range_(
                objc_util.ns(self.attr_key), self.line_style,
                objc_util.NSRange(self.start, self.end - self.start))
            self.through_color = (1, 0, 0, 1)
            self.objc_color = objc_util.UIColor.colorWithRed_green_blue_alpha_(
                *self.through_color)
            attr_str.addAttribute_value_range_(
                objc_util.ns('NSStrikethroughColor'), self.objc_color,
                objc_util.NSRange(self.start, self.end - self.start)) 
.
.
.

mikael

@cvp said:

@mikael said:

what does the objc_ptr do

Probably from early days of objc_util:

>>> assert (
... objc_util.ObjCInstance(v._objc_ptr) is 
... objc_util.ObjCInstance(v) is 
... v.objc_instance
... )
mikael

@cvp said:

NSStrikethroughColor

Of course lines must have colors! Now available as additional qualifier for the line tags:

    "Plain",
    "<b>Bold <i>italic</i></b>",
    "and <i><f system 32>just</f> italic</i>",
    "",
    "<f Zapfino><c red>Color</c>",
    "<shadow>Shadow</shadow></f>",
    "<c white><shadow green 0><b>BLOCK</b></shadow></c>",
    "",
    "<u lightgrey>Outlines:</u>",
    "<b>",
    "<o>DEFAULT</o>",
    "<o blue>COLORED</o>",
    "<o -3><c orange>FILLED</c></o>",
    "</b>",
    "<strike double red byword>really not cool</s>"

Result:

Result

cvp

@mikael and this one?

            self.attr_str.addAttribute_value_range_(
                objc_util.ns('NSObliqueness'), ns(-0.8), # <0: left >0: right
                objc_util.NSRange(self.start, self.end - self.start)) 

nikkinemo95

A view that displays one or more lines of informational text.
mcdvoice mybkexperience

mikael

@cvp, 😏 <— see the oblique smile

Added the oblique tag with a single float parameter. Default is 0.25, which roughly corresponds to italic on iOS.

(Not adding a picture since you already did.)

cvp

@mikael not yet in the usual github?

mikael

@cvp, you are fast! I changed the default, as described above, now committed.

cvp

@mikael whaaaa, you've added new tags....

mikael

@cvp, yes, these tags use the standard iOS styles, if you want your UI to follow Apple style guidelines:

Standard styles

cvp

@mikael If I'm allowed, you should add in your r.rich_text

        "<oblique -0.3>oblique</oblique>", 

mikael

@cvp, 😁, works for me.

cvp

@mikael Thanks, and now I 🤐 (sorry for the added work)

mikael

I used HTML-like syntax for the rich text definitions, since it is familiar and easy to parse with BeautifulSoup... But it is not especially joyful to write, especially since Pythonista has no native support for closing tags.

So, couple of things to reduce the amount of tags you need to write:

  1. If you use a specific complex style in the label string a lot, subclass RichLabel and define custom tags for it. For example:

    class MyRichLabel(RichLabel): custom = { 's': '<b><shadow green 0/></b>' }

Now, wherever you use the tag , it is replaced by the above definition.

  1. You can also define ready-to-use Label classes, where a certain format is applied by default:

    class MyRichLabel(RichLabel): custom = { 's': '<b><shadow green 0/></b>' } default = '<c white><s/></c>'

The way RichLabel class is built (it is actually a ui.Label in the end), you can set any usual ui.Label attribute defaults at the same time:

class MyRichLabel(RichLabel):
    custom = {
        's': '<b><shadow green 0/></b>'
    }
    default = '<c white><s/></c>'
    font = ('Arial', 24)
    alignment = ui.ALIGN_CENTER
    number_of_lines = 0

Now we can use it without extra tagging:

fancy = MyRichLabel(
    background_color='white',
)

fancy.rich_text('FANCY BLOCK')

With the result:

Fancy block image

  1. In the best case you do not need to write any tags, if you just need one style and are happy with the defaults. Following label classes are defined in the package:

    • BoldLabel, ItalicLabel, BoldItalicLabel, ObliqueLabel, BoldObliqueLabel
    • OutlineLabel
    • UnderlineLabel, StrikeLabel
    • ShadowLabel
    • BodyLabel, CalloutLabel, Caption1Label, Caption2Label, FootnoteLabel, HeadlineLabel, SubheadlineLabel, LargeTitleLabel, Title1Label, Title2Label, Title3Label

Due to limitations of the built-in view classes, you still need to use the rich_text method - good old text will just give you good old plain text.

mikael

@cvp, @peiriannydd and others, would appreciate your take on this.

Instead of the tag soup, it would be easy for me to implement the style formatting like this:


text = '''
Plain
b: Bold &
    i: italic
and &
    i:
        f system 32: just &
        italic

f Zapfino:
    c red: Color
    shadow: Shadow

u lightgrey: Outlines:

b:
    o: DEFAULT
    o blue: COLORED
    o -3 + c orange: FILLED

oblique: oblique

strike double red byword + oblique: really not cool
'''

White space after the colon is ignored. & at the end of the line indicates that there is no line break.

Which format would you prefer? Of course both can be supported, but one would be the default.

cvp

@mikael said:

Which format would you prefer

Even if the new format is less talkative, I prefer the html-like format, more usual, but that's only a personal feeling.

cvp

@mikael said:

@peiriannyd

double dd at end needed

mikael

@cvp, thanks, for both comments.

Also, I added your code as an html method that takes, well, some html.

mikael

For reference, here’s also @peiriannydd’s original request in both markup options:

"लाल laal <i><c red>red</c></i>"

"""नीला neela &
i + c blue: blue"""

Hmm, the latter might not be tag-verbose, i.e. more DRY, but it is ”line-verbose”.

cvp

@mikael said:

line-verbose

Yes sir

mikael

Aaand the last idea for this evening, use different tag delimiters because they act as better vertical separators, and Pythonista editor nicely matches closing pairs. Combined with the idea of ad-hoc combined tag creation.

"लाल laal {i + c red}red{i}"

Hmm, does not work nicely with f-strings, and that is a clear requirement.

So:
"लाल laal [i + c red]red[i]"

peiriannydd

@mikael @cvp I’m indifferent to the format, as you said, HTML is more usual, but any other format with at least as much flexibility would be fine. I think this a fantastic feature in any case.

peiriannydd

@mikael I have enjoyed using your richlabels (thank you!!). One thing I haven’t figured out - When I try to change the font with

I get an error that it can't find a font named times. It seems like it is looking for the first word only in the font name. I don’t know python syntax well enough - is there a way I can write my code that will combine the whole three-word string when passed to the richlabels parser to have a multiword font, or is this a limitation of the richlabels module?

Thanks again!

cvp

@peiriannydd this works (case sensitive)

    r = RichLabel(
        font=('Times New Roman', 24),
        background_color='white',
        alignment=ui.ALIGN_CENTER,
        number_of_lines=0,
    )
peiriannydd

@cvp yes, but I was hoping to change the font within the label, and doesn't work. For example, this doesn't work:

    r = RichLabel(
      font=('Chalkboard SE', 60),
      background_color='white',
      alignment=ui.ALIGN_CENTER,
      number_of_lines=0,
    )

    r.rich_text("\n".join([
      "First font",
      "<f 'Times New Roman' 60>Second font</f>, 
    ]))
mikael

@peiriannydd, needed a little change in code, but now you can do:

<f Times-New-Roman 60>

Unfortunately I am just in the process of rearranging my repo. If you want this to work now, you can make a change to the file on line 85, I think:

Current:

self.font_name = node_font or self.font_name

Change to:

self.font_name = (node_font or self.font_name).replace('-', ' ')
peiriannydd

@mikael fantastic! Thank you so much. It works great. I really appreciate your time and @cvp to help out an inexperienced python user like me.