Artifact [1647517a84]
Artifact 1647517a8473e03aded2c1b40792038760f26c73:
- File
uikit.py
— part of check-in
[d90db23c73]
at
2016-10-28 18:11:30
on branch trunk
— Move liststore_edit() and app_bin_check() from configwin. to uikit.
Allow ListStore for config_play/_record/_specbuttons without `editable` row [2], which is now a property of the CellRenderers (instead of a cell-attribute).
Specialized uikit.config_treeview() builds a custom two-column TreeView now. (user: mario, size: 28574) [annotate] [blame] [check-ins using]
# encoding: UTF-8 # api: python # type: functions # title: uikit helper functions # description: simplify usage of some gtk widgets # version: 1.9 # author: mario # license: public domain # # Wrappers around gtk methods. The TreeView method .columns() allows # to fill a treeview. It adds columns and data rows with a mapping # dictionary (which specifies many options and data positions). # # The .tree() method is a trimmed-down variant of that, creates a # single column, but has threaded entries. # # With the methodes .app_state() and .app_restore() named gtk widgets # can be queried for attributes. The methods return a saveable dict, # which contain current layout options for a few Widget types. Saving # and restoring must be handled elsewhere. # debug from config import * # system import os.path import copy import sys import re import base64 import inspect from compat2and3 import unicode, xrange, PY3, gzip_decode # gtk version (2=gtk2, 3=gtk3, 7=tk;) ver = 2 # if running on Python3 or with commandline flag if PY3 or conf.args.gtk3: ver = 3 # load gtk modules if ver==3: from gi import pygtkcompat as pygtk pygtk.enable() pygtk.enable_gtk(version='3.0') from gi.repository import Gtk as gtk from gi.repository import GObject as gobject from gi.repository import GdkPixbuf log.STAT(gtk) log.STAT(gobject) else: import pygtk import gtk import gobject GdkPixbuf = gtk.gdk # prepare gtkbuilder data ui_xml = get_data("gtk3.xml.gz", decode=True, gz=True) #or get_data("gtk3.xml", decode=True) if ver == 2: ui_xml = ui_xml.replace('version="3.0"', 'version="2.16"') # simplified gtk constructors --------------------------------------------- class uikit: #-- fill a treeview # # Adds treeviewcolumns/cellrenderers and liststore from a data dictionary. # Its datamap and the table contents can be supplied in one or two steps. # When new data gets applied, the columns aren't recreated. # # The columns are created according to the datamap, which describes cell # mapping and layout. Columns can have multiple cellrenderers, but usually # there is a direct mapping to a data source key from entries. # # datamap = [ # title width dict-key type, renderer, attrs # ["Name", 150, ["titlerow", str, "text", {} ] ], # [False, 0, ["interndat", int, None, {} ] ], # ["Desc", 200, ["descriptn", str, "text", {} ], ["icon",str,"pixbuf",{}] ], # # An according entries list then would contain a dictionary for each row: # entries = [ {"titlerow":"first", "interndat":123}, {"titlerow":"..."}, ] # Keys not mentioned in the datamap get ignored, and defaults are applied # for missing cols. All values must already be in the correct type however. # @staticmethod def columns(widget, datamap=[], entries=None, show_favicons=True, pix_entry=False, fixed_size=24): # create treeviewcolumns? if not widget.get_column(0): # loop through titles datapos = 0 for n_col, desc in enumerate(datamap): # check for title if not isinstance(desc[0], str): datapos += 1 # if there is none, this is just an undisplayed data column continue # new tvcolumn col = gtk.TreeViewColumn(desc[0]) # title col.set_resizable(True) # width if (desc[1] > 0): col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) col.set_fixed_width(desc[1]) # loop through cells for var in range(2, len(desc)): cell = desc[var] # cell renderer if (cell[2] == "pixbuf"): rend = gtk.CellRendererPixbuf() # img cell # stock icons if cell[1] == str: # only match for literal `str` cell[3]["stock_id"] = datapos expand = False # pixbufs else: pix_entry = datapos cell[3]["pixbuf"] = datapos if fixed_size: if not isinstance(fixed_size, list): fixed_size = [fixed_size, fixed_size] rend.set_fixed_size(*fixed_size) else: rend = gtk.CellRendererText() # text cell cell[3]["text"] = datapos #col.set_sort_column_id(datapos) # only on textual cells # attach cell to column col.pack_end(rend, expand=cell[3].get("expand",True)) # apply attributes for attr,val in list(cell[3].items()): col.add_attribute(rend, attr, val) # next datapos += 1 #log.INFO(cell, len(cell)) # add column to treeview #log.E(col) widget.append_column(col) # add data? if (entries is not None): #- expand datamap vartypes = [] #(str, str, bool, str, int, int, gtk.gdk.Pixbuf, str, int) rowmap = [] #["title", "desc", "bookmarked", "name", "count", "max", "img", ...] for desc in datamap: for var in xrange(2, len(desc)): vartypes.append(desc[var][1]) # content types rowmap.append(desc[var][0]) # dict{} column keys in entries[] list # create gtk array storage #log.UI(vartypes, len(vartypes)) ls = gtk.ListStore(*vartypes) # could be a TreeStore, too #log.DATA(rowmap, len(rowmap)) # prepare for missing values, and special variable types defaults = { str: "", unicode: "", bool: False, int: 0, gtk.gdk.Pixbuf: empty_pixbuf } if gtk.gdk.Pixbuf in vartypes: pix_entry = vartypes.index(gtk.gdk.Pixbuf) # sort data into gtk liststore array for row in entries: # preset some values if absent row.setdefault("deleted", False) row.setdefault("search_col", "#ffffff") row.setdefault("search_set", False) # generate ordered list from dictionary, using rowmap association row = [ row.get( skey , defaults[vartypes[i]] ) for i,skey in enumerate(rowmap) ] # map Python2 unicode to str row = [ str(value) if type(value) is unicode else value for value in row ] # autotransform string -> gtk image object if (pix_entry and type(row[pix_entry]) == str): pix = None try: if show_favicons and os.path.exists(row[pix_entry]): pix = gtk.gdk.pixbuf_new_from_file(row[pix_entry]) except Exception as e: log.ERR("uikik.columns: Pixbuf fail,", e) row[pix_entry] = pix or defaults[gtk.gdk.Pixbuf] try: # add ls.append(row) # had to be adapted for real TreeStore (would require additional input for grouping/level/parents) except: # brute-force typecast ls.append( [va if ty==gtk.gdk.Pixbuf else ty(va) for va,ty in zip(row,vartypes)] ) #if entries: # log.ROWS(row, len(row)) # apply array to widget widget.set_model(ls) return ls, rowmap, pix_entry pass #-- treeview for categories # # simple two-level treeview display in one column # with entries = [main,[sub,sub], title,[...],...] # @staticmethod def tree(widget, entries, title="category", icon=gtk.STOCK_DIRECTORY): # list types ls = gtk.TreeStore(str, str) #log.DATA(".tree", entries) # add entries main = None for entry in entries: if isinstance(entry, (str,unicode)): main = ls.append(None, [str(entry), icon]) else: for sub_title in entry: ls.append(main, [str(sub_title), icon]) # finalize widget.set_model(ls) widget.set_search_column(0) #tvcolumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) #tvcolumn.set_fixed_width(125]) #widget.expand_all() #widget.expand_row("3", False) #print(widget.row_expanded("3")) return ls @staticmethod def tree_column(widget, title="Category"): # just one column tvcolumn = gtk.TreeViewColumn(title); widget.append_column(tvcolumn) # inner display: icon & string pix = gtk.CellRendererPixbuf() txt = gtk.CellRendererText() # position tvcolumn.pack_start(pix, expand=False) tvcolumn.pack_end(txt, expand=True) # select array content source in treestore tvcolumn.add_attribute(pix, "stock_id", 1) tvcolumn.add_attribute(txt, "text", 0) tvcolumn.set_sort_column_id(0) #-- save window size and widget properties # # needs a list of widgetnames # e.g. pickle.dump(uikit.app_state(...), open(os.environ["HOME"]+"/.config/app_winstate", "w")) # @staticmethod def app_state(wTree, widgetnames=["window1", "treeview2", "vbox17"]): r = {} # restore array for wn in widgetnames: r[wn] = {} w = wTree.get_widget(wn) t = type(w) # print(wn, w, t) # extract different information from individual widget types if t == gtk.Window: r[wn]["size"] = list(w.get_size()) #print("WINDOW SIZE", list(w.get_size()), r[wn]) if t == gtk.Widget: r[wn]["name"] = w.get_name() # gtk.TreeView if t == gtk.TreeView: r[wn]["columns:width"] = [] for col in w.get_columns(): r[wn]["columns:width"].append( col.get_width() ) # - Rows r[wn]["rows:expanded"] = [] for i in xrange(0,50): if w.row_expanded(treepath(i)): # Gtk.TreePath for Gtk3 r[wn]["rows:expanded"].append(i) # - selected (model, paths) = w.get_selection().get_selected_rows() if paths: r[wn]["row:selected"] = treepath_to_str(paths[0]) # gtk.Toolbar if t == gtk.Toolbar: r[wn]["icon_size"] = int(w.get_icon_size()) r[wn]["style"] = int(w.get_style()) # gtk.Notebook if t == gtk.Notebook: r[wn]["tab_pos"] = int(w.get_tab_pos()) r[wn]["tab_order"] = [w.get_menu_label_text(w.get_nth_page(i)) for i in xrange(0, w.get_n_pages())] r[wn]["tab_current"] = w.get_menu_label_text(w.get_nth_page(w.get_current_page())) #print(r) return r gtk_position_type_enum = [gtk.POS_LEFT, gtk.POS_RIGHT, gtk.POS_TOP, gtk.POS_BOTTOM] #-- restore window and widget properties # # requires only the previously saved widget state dict # @staticmethod def app_restore(wTree, r=None): for wn in r.keys(): # widgetnames w = wTree.get_widget(wn) if (not w): continue t = type(w) for method,args in r[wn].items(): # gtk.Window if method == "size": w.resize(args[0], args[1]) # gtk.TreeView if method == "columns:width": for i,col in enumerate(w.get_columns()): if (i < len(args)): col.set_fixed_width(args[i]) # - Rows if method == "rows:expanded": w.collapse_all() for i in args: w.expand_row(treepath(i), False) # - selected # if method == "row:selected": # w.get_selection().select_path(treepath(args)) # gtk.Toolbar if method == "icon_size": w.set_icon_size(args) if method == "style": w.set_style(args) # gtk.Notebook if method == "tab_pos": w.set_tab_pos(r[wn]["tab_pos"]) if method == "tab_order": tab_current = r[wn].get("tab_current") for pos,ord_tabname in enumerate(args): # compare current label list on each reordering round for i in range(0, w.get_n_pages()): w_tab = w.get_nth_page(i) w_label = w.get_menu_label_text(w_tab) if w_label == ord_tabname: w.reorder_child(w_tab, pos) if tab_current == ord_tabname: w.set_current_page(pos) pass #-- Save-As dialog # @staticmethod def save_file(title="Save As", parent=None, fn="", formats=[("*.pls", "*.pls"), ("*.xspf", "*.xpsf"), ("*.m3u", "*.m3u"), ("*.jspf", "*.jspf"), ("*.asx", "*.asx"), ("*.json", "*.json"), ("*.smil", "*.smil"), ("*.desktop", "*.desktop"), ("*","*")]): # With overwrite confirmation c = gtk.FileChooserDialog(title, parent, action=gtk.FILE_CHOOSER_ACTION_SAVE, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)) c.set_do_overwrite_confirmation(True) # Params if fn: c.set_current_name(fn) fn = "" for fname,ftype in formats: f = gtk.FileFilter() f.set_name(fname) f.add_pattern(ftype) c.add_filter(f) # Yes, that's how to retrieve signals for changed filter selections try: filterbox = c.get_children()[0].get_children()[0] filterbox.connect("notify::filter", lambda *w: uikit.save_file_filterchange(c)) except: pass # Filter handlers don't work either. # Display and wait if c.run(): fn = c.get_filename() # return filaname c.destroy() return fn # Callback for changed FileFilter, updates current filename extension @staticmethod def save_file_filterchange(c): fn, ext = c.get_filename(), c.get_filter().get_name() if fn and ext: fn = os.path.basename(fn) c.set_current_name(re.sub(r"\.(m3u|pls|xspf|jspf|asx|json|smil|desktop|url|wpl)8?$", ext.strip("*"), fn)) # Spool gtk update calls from non-main threads (optional immediate=1 flag to run task next, not last) @staticmethod def do(callback, *args, **kwargs): name = inspect.getsource(callback).strip() if callback.__name__=='<lambda>' else str(callback) if kwargs.get("immediate"): del kwargs["immediate"] pos = 0 else: pos = len(uikit.idle_tasks) # Run callback right away if uikit.in_idle or conf.nothreads: log.UIKIT_RUN_NOW(name) callback(*args, **kwargs) # Spool them for Gtk idle handling else: #log.UIKIT_SPOOL(name) uikit.idle_tasks.insert(pos, [lambda: callback(*args, **kwargs), name]) gobject.idle_add(uikit.idle_do) # Collect tasks to perform in gtk.main loop idle_tasks = [] in_idle = False # Execute UI updating tasks in order @staticmethod def idle_do(): uikit.in_idle = True if uikit.idle_tasks: task, name = uikit.idle_tasks.pop(0) log.UIKIT_EXEC(name) task() uikit.in_idle = False return len(uikit.idle_tasks) > 0 # Adds background color to widget, # eventually wraps it into a gtk.EventBox, if it needs a container @staticmethod def bg(w, color="", where=["bg"], wrap=1): """ this method should be called after widget creation, and before .add()ing it to container """ if color: # wrap unstylable widgets into EventBox if wrap and not isinstance(w, (gtk.Window, gtk.EventBox)): wrap = gtk.EventBox() wrap.add(w) w = wrap # copy style object, modify settings try: # Gtk2 c = w.get_colormap().alloc_color(color) except: # Gtk3 p, c = gtk.gdk.Color.parse(color) for state in (gtk.STATE_NORMAL, gtk.STATE_SELECTED, gtk.STATE_ACTIVE): w.modify_bg(state, c) # return modified or wrapped widget return w # Create GtkLabel @staticmethod def label(text, size=305, markup=0): label = gtk.Label(text) if markup: label.set_markup(text) #######label.set_property("visible", True) label.set_line_wrap(True) label.set_size_request(size, -1) return label # Wrap two widgets in horizontal box @staticmethod def hbox(w1, w2, exr=True): b = gtk.HBox(homogeneous=False, spacing=5) ######b.set_property("visible", True) b.pack_start(w1, expand=not exr, fill=not exr) b.pack_start(w2, expand=exr, fill=exr) return b # Wrap entries/checkboxes with extra label, background, images, etc. @staticmethod def wrap(widgetstore=None, id=None, w=None, label=None, color=None, image=None, align=1, label_size=305, label_markup=0): if id: widgetstore[id] = w if label: if type(w) is gtk.Entry: w.set_width_chars(11) w = uikit.hbox(w, uikit.label(label, size=label_size, markup=label_markup)) if image: pix = gtk.image_new_from_pixbuf(uikit.pixbuf(image)) if pix: w = uikit.hbox(w, pix, exr=False) if color: w = uikit.bg(w, color) if align: a = gtk.Alignment() a.set_padding(0, 0, align, 0) a.add(w) w = a w.show_all() return w # Config win table (editable dictionary, two columns w/ executable indicator pixbuf) @staticmethod def config_treeview(opt, columns=["Icon", "Command"]): liststore = gtk.ListStore(str, str, str) w = gtk.TreeView(liststore) lno = len(columns) # two text columns and renderers for i in range(0, lno): c = gtk.TreeViewColumn(columns[i]) c.set_resizable(True) c.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) c.set_fixed_width(150 + 75*i) r = gtk.CellRendererText() c.pack_end(r, expand=True) r.set_property("editable", True) r.connect("edited", uikit.liststore_edit, (liststore, i)) c.add_attribute(r, "text", i) #c.add_attribute(r, "editable", 2) w.append_column(c) # add pixbuf holder to last column if lno < 3: r = gtk.CellRendererPixbuf() c.pack_start(r, expand=False) c.add_attribute(r, "stock_id", 2) w.set_property("width_request", 450) w.set_property("height_request", 115) return w, liststore # Generic Gtk callback to update ListStore when entries get edited. # where user_data = (liststore, column #id) @staticmethod def liststore_edit(cell, row, text, user_data): #log.EDIT(cell, row, text, user_data) liststore, column = user_data liststore[row][column] = text # update executable-indicator pixbuf if column == 1 and len(liststore) == 3 and liststore[row][2].startswith("gtk."): liststore[row][2] = uikit.app_bin_check(text) # add new row if editing last if row == len(liststore) -1: liststore.append(*["" for c in liststore[column]]) # return OK or CANCEL depending on availability of app @staticmethod def app_bin_check(v): bin = re.findall(r"(?<![$(`%-;/$])\b(\w+(?:-\w+)*)", v) if bin: bin = [find_executable(bin) for bin in bin] if not None in bin: return gtk.STOCK_MEDIA_PLAY else: return gtk.STOCK_CANCEL else: return gtk.STOCK_NEW # Attach textual menu entry and callback @staticmethod def add_menu(menuwidget, label, action, insert=None): for where in list(menuwidget): m = gtk.MenuItem(label) m.connect("activate", action) m.show() if insert: where.insert(m, insert) else: where.add(m) # gtk.messagebox @staticmethod def msg(text, style=gtk.MESSAGE_INFO, buttons=gtk.BUTTONS_CLOSE, yes=None): m = gtk.MessageDialog(None, 0, style, buttons, message_format=text) m.show() if yes: response = m.run() m.destroy() return response in (gtk.RESPONSE_OK, gtk.RESPONSE_ACCEPT, gtk.RESPONSE_YES) else: m.connect("response", lambda *w: m.destroy()) pass # manual signal binding with a dict of { (widget, signal): callback } @staticmethod def add_signals(builder, map): for (widget,signal),func in map.items(): builder.get_widget(widget).connect(signal, func) # Pixbug loader (from inline string, as in `logo.png`, automatic base64 decoding, and gunzipping of raw data) @staticmethod def pixbuf(buf, fmt="png", decode=True, gzip=False): if not buf or len(buf) < 16: return None if fmt and ver==3: p = GdkPixbuf.PixbufLoader.new_with_type(fmt) elif fmt: p = GdkPixbuf.PixbufLoader(fmt) else: p = GdkPixbuf.PixbufLoader() if decode and re.match("^[\w+/=\s]+$", str(buf)): buf = base64.b64decode(buf) # inline encoding if gzip: buf = gzip_decode(buf) if buf: p.write(buf) pix = p.get_pixbuf() p.close() return pix # Transparent png empty_pixbuf = uikit.pixbuf( """iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAA B6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAAB3RJTUU H3wQLEAE6zgxuGAAAABFJREFUOBFjGAWjYBSMAigAAAQQAAFWMn04AAAAAElFTkSuQmCC""" ) #-- Gtk3 wrappers # Use a str of "1:2:3" as treepath, # literally in Gtk2, TreePath-wrapped for Gtk3 def treepath(ls): if isinstance(ls, (list,tuple)): ls = ":".join(map(str, ls)) if ver==2: return ls else: return gtk.TreePath.new_from_string(str(ls)) # def treepath_to_str(tp): if ver==2: return tp else: return tp.to_string() # Text-only dropdown list. # # Necessary because gtk.ComboBoxText binding is absent in debian packages # https://bugzilla.gnome.org/show_bug.cgi?id=660659 # # This one implements a convenience method `.set_default()` to define the active # selection by value, rather than by index. # # Can use a list[] of entries or a key->value dict{}, where the value becomes # display text, and the key the internal value. # class ComboBoxText(gtk.ComboBox): ls = None def __init__(self, entries, no_scroll=1): # prepare widget gtk.ComboBox.__init__(self) ########self.set_property("visible", True) cell = gtk.CellRendererText() self.pack_start(cell, True) self.add_attribute(cell, "text", 1) if no_scroll: self.connect("scroll_event", self.no_scroll) # collect entries self.ls = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) self.set_model(self.ls) if type(entries[0]) is not tuple: entries = zip(entries, entries) for key,value in entries: self.ls.append([key, value]) # activate dropdown of given value def set_default(self, value): for index,row in enumerate(self.ls): if value in row: return self.set_active(index) # add as custom entry self.ls.append([value, value]) self.set_active(index + 1) # fetch currently selected text entry def get_active_text(self): index = self.get_active() if index >= 0: return self.ls[index][0] # Signal/Event callback to prevent hover scrolling of ComboBox widgets def no_scroll(self, widget, event, data=None): return True # Expand A=a|B=b|C=c option list into (key,value) tuple list, or A|B|C just into a list. @staticmethod def parse_options(opts, sep="|", assoc="="): if opts.find(assoc) >= 0: return [ (k,v) for k,v in (x.split(assoc, 1) for x in opts.split(sep)) ] else: return opts.split(sep) #dict( (v,v) for v in opts.split(sep) ) #-- startup progress bar progresswin, progressbar = None, None def gui_startup(p=0/100.0, msg="streamtuner2 is starting"): global progresswin, progressbar if not progresswin: # GtkWindow "progresswin" progresswin = gtk.Window() progresswin.set_property("title", "streamtuner2") progresswin.set_property("default_width", 300) progresswin.set_property("width_request", 300) progresswin.set_property("default_height", 30) progresswin.set_property("height_request", 30) #progresswin.set_property("window_position", "center") progresswin.set_property("decorated", False) #######progresswin.set_property("visible", True) # GtkProgressBar "progressbar" progressbar = gtk.ProgressBar() #########progressbar.set_property("visible", True) progressbar.set_property("show_text", True) progressbar.set_property("text", msg) progresswin.add(progressbar) progresswin.show_all() try: if p <= 0.0: pass elif p < 1.0: progressbar.set_fraction(p) progressbar.set_property("text", msg) while gtk.events_pending(): gtk.main_iteration() else: progresswin.hide() except Exception as e: log.ERR("gui_startup()", e) # Encapsulates references to gtk objects AND properties in main window, # which allows to use self. and main. almost interchangably. # # This is a kludge to keep gtkBuilder widgets accessible; so just one # instance has to be built. Also ties main.channels{} or .features{} # dicts together for feature windows. Used by search, config, streamedit. # class AuxiliaryWindow(object): def __init__(self, parent): self.main = parent self.module = self.__class__.__name__ self.meta = plugin_meta(None, inspect.getcomments(inspect.getmodule(self))) def __getattr__(self, name): if name in self.main.__dict__: return self.main.__dict__[name] elif name in self.main.__class__.__dict__: return self.main.__class__.__dict__[name] else: return self.main.get_widget(name) # Auxiliary window: about dialog # class AboutStreamtuner2(AuxiliaryWindow): def __init__(self, parent): a = gtk.AboutDialog() a.set_name(parent.meta["id"]) a.set_version(parent.meta["version"]) a.set_license(parent.meta["license"]) a.set_authors((get_data("CREDITS") or parent.meta["author"]).split("\n")) a.set_website(parent.meta["url"]) a.connect("response", lambda a, ok: ( a.hide(), a.destroy() ) ) a.show_all()