Forum Archive

[Share]Simple ListView Class

Phuket2

The below is very simple. It's also been shared before by other members here. But just shows how simple it can be to make a list view or dialog list view. Not many lines of real doing something code. With the new beta, 301011 can be less lines. Anyway, has been a few questions about ui.TableView's lately.
I know I have not commented the code. I think comments can actually make it more difficult to understand. I don't mean in a professional environment, but with small snippets like this. Not really sure. But ok, in this example I am showing font family names. Wanted to do something that had some meaning other than a list of numbers. But really if you can get this far with a ui.TableView, you are 90% there. Does not take much from here to create your own data_source or create your own ui.TableViewCell's

# Pythonista Forum - @Phuket2
import ui, itertools

def get_font_list():
    # from someone on the forum, i think @omz
    UIFont = ObjCClass('UIFont')
    return list(itertools.chain(*[UIFont.fontNamesForFamilyName_(str(x))
                for x in UIFont.familyNames()]))

class SimpleListView(ui.View):
    def __init__(self, items, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.tbl = None
        self.value = None
        self.flex = 'wh'
        self.make_view(items)

    def make_view(self, items):
        tbl = ui.TableView(frame=self.bounds)
        tbl.flex = 'wh'
        tbl.data_source = tbl.delegate = ui.ListDataSource(items)
        tbl.data_source.action = self.my_action
        self.tbl = tbl
        self.add_subview(tbl)

    def my_action(self, sender):
        self.value = sender.items[sender.selected_row]
        self.close()

if __name__ == '__main__':
    w, h = 600, 800
    f = (0, 0, w, h)
    style = ''
    my_list = get_font_list()
    v = SimpleListView(my_list, frame=f, name='Font List')
    v.present(style=style)
    v.wait_modal()
    print(v.value)
JonB

You should update this to actually use the font when displaying the font name!

Phuket2

@JonB , thanks is a good idea.
Below is the script above with basically just 2 small changes that renders the font name in its script.
Btw, there are quite a few ways to do this. I don't want to complicate the example but just to beware that you will see examples that do it a little differently.

Anyway, in this case it's just a simple re wiring. I set the data_souce.tableview_cell_for_row func to my own method.
You can see methods prototype in the help file under ui.TableView.data_source.

Something to note is that I am using a ui.ListDataSource as the data_source. I could have implement the data_source class myself as shown in the help file. It's also very straight fwd. however the ui.ListDataSource is very quick and easy to use. Also, you get some functionality for free like accessory items, etc. as well as event call backs for these items.

So here taking advantage of the simplicity of the ui.ListDataSource, and rather than let the LDS create the cell, we do it as we need a little extra control. In this case we are not doing much when we create the ui.TableViewCell. But of course we can do anything we like. Remembering the data_source.items is just a list. Could be a list of any datatype you like if you are creating the cell yourself.

Hope I explained it ok. But I think a big take away from here is that ui.ListDataSource is easy to use and it's not really limiting as you might think at first glance. And it gives you quite a bit for free.

# Pythonista Forum - @Phuket2
import ui, itertools

def get_font_list():
    # from someone on the forum, i think @omz
    UIFont = ObjCClass('UIFont')
    return list(itertools.chain(*[UIFont.fontNamesForFamilyName_(str(x))
                for x in UIFont.familyNames()]))

class SimpleListView(ui.View):
    def __init__(self, items, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.tbl = None
        self.value = None
        self.flex = 'wh'
        self.make_view(items)

    def make_view(self, items):
        tbl = ui.TableView(frame=self.bounds)
        tbl.flex = 'wh'
        tbl.data_source = tbl.delegate = ui.ListDataSource(items)
        tbl.data_source.tableview_cell_for_row =\
                                    self.tableview_cell_for_row
        tbl.data_source.action = self.my_action
        self.tbl = tbl
        self.add_subview(tbl)

    def tableview_cell_for_row(self, tableview, section, row):
        # Create and return a cell for the given section/row
        cell = ui.TableViewCell()
        fnt_name = str(tableview.data_source.items[row]) 
        cell.text_label.text = fnt_name
        cell.text_label.font = (fnt_name, 16)
        return cell

    def my_action(self, sender):
        self.value = sender.items[sender.selected_row]
        self.close()

if __name__ == '__main__':
    w, h = 600, 800
    f = (0, 0, w, h)
    style = ''
    my_list = get_font_list()
    v = SimpleListView(my_list, frame=f, name='Font List')
    v.present(style=style)
    v.wait_modal()
    print(v.value)
Phuket2

I am having a problem to do a inline sort for the get_font_list func. I did the below, but guessing it's bad/inefficient. Not sure why I can't figure it out. But the below does return the fonts sorted, just in an inefficient way.

def get_font_list():
    # from someone on the forum, i think @omz
    UIFont = ObjCClass('UIFont')
    '''
    return list(itertools.chain(*[UIFont.fontNamesForFamilyName_(str(x))
                for x in UIFont.familyNames()]))
    '''
    lst = list(itertools.chain(*[UIFont.fontNamesForFamilyName_(str(x))
                for x in UIFont.familyNames()]))
    return sorted([str(item) for item in lst])
dgelessus

@Phuket2 If you want to sort a list in-place, you can use list.sort:

lst = list(...)
lst.sort()
return lst

Also, I think you can shorten the list comprehension a little, to something like this:

lst = [str(font) for family in UIFont.familyNames() for font in UIFont.fontNamesForFamilyName_(family)]
lst.sort()
return lst

Then the str conversion is done in the list comprehension directly and you don't have to do it later.

Phuket2

@dgelessus , thanks. Perfect.
The new func for get_font_list as you say above.

def get_font_list():
    # from someone on the forum, i think @omz
    UIFont = ObjCClass('UIFont')
    lst = [str(font) for family in UIFont.familyNames() for font in
                UIFont.fontNamesForFamilyName_(family)]
    lst.sort()
    return lst
Phuket2

Hmmm, one day I will get something right 100%. It's not today. I am sure some guys spotted a issue with what I say about using ui.ListDataSource and benefits with accessory items etc. that's all well and good. But if you create the cell yourself, you lose that functionality. As the ui.ListDataSource is creating that magic when it creates the ui.TableViewCell. So when you create the cell yourself that functionality disappears.
But not all is lost. It's not documented, but ui.TableViewCell takes a param. None = default, 'subtitle', 'value1', 'value2'. but ui.TableViewCell creates a slightly different cell layout depending on what Str its passed.
But if @omz creates a new ui.TableViewCell type something like 'listdatasource' , we could possibly have our cake and eat it.

@omz, not sure if this is difficult or not (a new ui.TableViewCell type). But to me it makes sense. It would make ui.ListDataSource a lot more flexible, unless I am missing something, which is very possible

ramvee

@phuket2 !! Really Nice Utility!!
I think.. This line is missing though..
'from objc_util import *'

dgelessus

@Phuket2 ui.ListDataSource is implemented in the Python part of the ui module (site-packages/ui.py in the standard library section), so you can see for yourself how it does all the "magic" with accessory items :)

To give a custom cell an accessory item, you need to set its accessory_type attribute (this is documented in the ui.TableViewCell docs), and to handle tapping on the accessory button, you need to implement the tableview_accessory_button_tapped(self, tv, section, row) method on your delegate (this part is not documented as far as I know - it's one of those omz secret features).

Phuket2

@ramvee , thanks. You are right about the import. I think because I use that import in the pythonista_startup.py it does not fail for me. Well at least I think that's why.

Phuket2

@dgelessus , lol. Thanks. You are so right. At least I predicted I was going to be wrong. I was just wrong recursively 😱
What threw me off was, if you assign a list of dicts to LDS.items as in the help file (title, image, accessory_type), they appear to be ignored if you create your own cell. I can't get it clear in my head if it should be like that or not. But what you mention works perfectly well.

Phuket2

Ok, I did a few updates to make it more useful. Not that it is really that useful unless you need it at runtime as Pythonista has the same but better built fonts picker in in the asset picker.
But this just shows how hoist the original Custom ui.View Class and place it in another ui.View Custom Class. There is not much happing to make this work, the bulk of the extra lines are creating the views.
One think I would point out though is that this example is size and orientation friendly. I am finally getting my head around the flex attr from the ui Module. I still sometimes slip up, but it's actually very easy. Shame on me, to take so long to get it. It's designed very well. I just suffer old mans syndrome sometimes. I mention that, because I don't think it's obscure, my brain was just working against myself.
Anyway, I try to write anything that is not size and orientation friendly anymore. There is no need. And the more you do it, the more natural it is.

Anyway, below is the update. Pythonista's filter seems to be the same as what I do, but @omz hilites the substring. Also, I am copying the font name to the clipboard where as the asset picker copies the font name into the editor. It's not difficult to do, just decided to copy to the clipboard.

Again, I am not saying the below is the best way to do things. I am still a beginner. Just saying it's one way.

# Pythonista Forum - @Phuket2
import ui, clipboard, console
from objc_util import *

def get_font_list():
    # updated version from @dgelessus
    UIFont = ObjCClass('UIFont')
    lst = [str(font) for family in UIFont.familyNames() for font in
                UIFont.fontNamesForFamilyName_(family)]
    lst.sort()
    return lst

class SimpleListView(ui.View):
    def __init__(self, items, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.tbl = None
        self.value = None
        self.flex = 'wh'

        self.make_view(items)

    def make_view(self, items):
        tbl = ui.TableView(frame=self.bounds)
        tbl.flex = 'wh'
        tbl.data_source = tbl.delegate = ui.ListDataSource(items)
        tbl.data_source.tableview_cell_for_row =\
            self.tableview_cell_for_row

        self.tbl = tbl
        self.add_subview(tbl)

    def tableview_cell_for_row(self, tableview, section, row):
        # Create and return a cell for the given section/row
        cell = ui.TableViewCell()
        data = tableview.data_source.items[row]
        cell.text_label.text = data
        cell.text_label.font = (data,  16)
        return cell

class FontViewer(ui.View):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.data = get_font_list()
        self.tbl = None
        self.bg_color = 'darkgray'
        self.flex = 'wh'
        self.name_str = self.name if self.name else 'Fonts'

        self.make_view(**kwargs)
        self.update_name()

    def make_view(self, **kwargs):
        # make the containing view
        margin = kwargs.pop('margin', (0, 0))
        cv = ui.View(frame=self.bounds.inset(*margin))
        cv.flex = 'wh'

        # make the search view
        sv = ui.View(frame=cv.bounds)
        sv.height = 32
        tv = ui.TextField(frame=sv.bounds)
        tv.placeholder = 'search'
        tv.clear_button_mode = 'always'
        tv.autocapitalization_type = ui.AUTOCAPITALIZE_NONE
        tv.autocorrection_type = False
        tv.delegate = self
        tv.flex = 'wh'
        sv.add_subview(tv)
        sv.flex = 'w'
        cv.add_subview(sv)

        # make the list view
        lv = ui.View(frame=sv.frame)
        lv.height = cv.height - sv.frame.max_y - 5
        lv.y = sv.frame.max_y + 5
        lv.corner_radius = 6

        lv.flex = 'wh'
        cv.add_subview(lv)

        # create the list
        slv = SimpleListView(self.data, frame=f)
        self.tbl = slv.tbl

        # redirect the action to this class.
        self.tbl.data_source.action = self.list_action
        lv.add_subview(slv)

        self.add_subview(cv)

    def textfield_did_change(self, textfield):
        self.filter_data(textfield.text)

    def filter_data(self, filter_txt):
        # real poor mans filter.  just doing an in-string search to match
        # but its case insensitive. for this data seems reasonable.
        txt = filter_txt.lower()

        # ui.ListDataSource updates itself when the items are changed
        self.tbl.data_source.items = [item for item in self.data
                                        if txt in item.lower()]
        self.update_name()

    def update_name(self):
        self.name = '{} - ({})'.format(self.name_str,
                        len(self.tbl.data_source.items))

    def list_action(self, sender):
        # when a list item is clicked
        self.value = sender.items[sender.selected_row]
        clipboard.set(self.value)
        console.hud_alert('{} - copied'.format(self.value))
        self.close()

if __name__ == '__main__':
    w, h = 400, 800
    style = 'popover'
    if style == 'popover':
        h = ui.get_screen_size()[1] * .6

    f = (0, 0, w, h)

    fv = FontViewer(name='The Fonts', frame=f, margin=(5, 5))
    fv.present(style=style)