Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
[Share]Simple ListView Class
-
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)
-
You should update this to actually use the font when displaying the font name!
-
@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)
-
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])
-
@Phuket2 If you want to sort a
list
in-place, you can uselist.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. -
@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
-
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
-
@phuket2 !! Really Nice Utility!!
I think.. This line is missing though..
'from objc_util import *' -
@Phuket2
ui.ListDataSource
is implemented in the Python part of theui
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 theui.TableViewCell
docs), and to handle tapping on the accessory button, you need to implement thetableview_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). -
@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.
-
@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. -
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)