# encoding: UTF-8
# api: python
# type: functions
# title: uikit helper functions
# description: simplify usage of some gtk widgets
# version: 1.8
# 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, pix_entry=False, typecast=0):
# create treeviewcolumns?
if (not widget.get_column(0)):
# loop through titles
datapos = 0
for n_col,desc in enumerate(datamap):
# check for title
if (type(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 xrange(2, len(desc)):
cell = desc[var]
# cell renderer
if (cell[2] == "pixbuf"):
rend = gtk.CellRendererPixbuf() # img cell
#rend.set_fixed_size(24, 24)
if (cell[1] == str):
cell[3]["stock_id"] = datapos # for stock icons
expand = False
else:
pix_entry = datapos
cell[3]["pixbuf"] = datapos
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
widget.append_column(col)
# finalize widget
widget.set_search_column(5) #??
widget.set_search_column(4) #??
widget.set_search_column(3) #??
widget.set_search_column(2) #??
widget.set_search_column(1) #??
#widget.set_reorderable(True)
# 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", ...]
if (not rowmap):
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
ls = gtk.ListStore(*vartypes) # could be a TreeStore, too
#log.UI(vartypes, len(vartypes))
#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):
row[pix_entry] = ( gtk.gdk.pixbuf_new_from_file(row[pix_entry]) if os.path.exists(row[pix_entry]) else 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
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
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])
# 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)
# finalize
widget.set_model(ls)
tvcolumn.set_sort_column_id(0)
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
#-- 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(str(i)):
r[wn]["rows:expanded"].append(i)
# - selected
(model, paths) = w.get_selection().get_selected_rows()
if paths:
r[wn]["row:selected"] = 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(str(i), False)
# - selected
if method == "row:selected":
w.get_selection().select_path(tuple(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"), ("*.wpl", "*.wpl"), ("*","*")]):
# 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|wpl)$", ext.strip("*"), fn))
# pass updates from another thread, ensures that it is called just once
@staticmethod
def do(callback, *args, **kwargs):
gobject.idle_add(lambda: callback(*args, **kwargs) and False)
# adds background color to widget,
# eventually wraps it into a gtk.Window, if it needs a container
@staticmethod
def bg(w, color="", where=["bg"]):
""" this method should be called after widget creation, and before .add()ing it to container """
if color:
# wrap unstylable widgets into EventBox
if not isinstance(w, gtk.Window):
wrap = gtk.EventBox()
wrap.add(w)
##########wrap.set_property("visible", True)
w = wrap
# copy style object, modify settings
s = w.get_style().copy()
c = w.get_colormap().alloc_color(color)
for state in (gtk.STATE_NORMAL, gtk.STATE_SELECTED):
s.bg[state] = c
w.set_style(s)
# probably redundant, but better safe than sorry:
w.modify_bg(gtk.STATE_NORMAL, c)
# return modified or wrapped widget
return w
# Create GtkLabel
@staticmethod
def label(text, size=400, 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=10)
######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
# 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"""
)
# 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(False)
else:
progresswin.hide()
except: return
# 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()