May 25
Pygtk star rating cellrenderer for a treeview
This code is a complete hack. I’ve probably done it wrong in someones eyes out there, and if by chance you are better at this sort of thing, and see room for improvements please pass it along. I can only stand so much insanity building this thing. It works for what I need.
This code comes from lots of different sources:
http://www.pygtk.org/articles/writing-a-custom-widget-using-pygtk/writing-a-custom-widget-using-pygtk.htm
http://faq.pygtk.org/index.py?req=show&file=faq13.045.htp # Create a custom gtk.CellRenderer
http://faq.pygtk.org/index.py?req=show&file=faq13.041.htp # How to add images/animations
http://nullege.com/codes/show/src%40t%40r%40translate-HEAD%40src%40trunk%40virtaal%40virtaal%40views%40widgets%40cellrendererwidget.py/147/gtk.CellEditable/python # Add a widget to a cell
Now that I’ve given credit where credit is due … time to get into the code.
Here’s the complete package, images and everything.
#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright 2009 Zuza Software Foundation # Copyright 2012 Eugene R. Miller # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, see <http: //www.gnu.org/licenses>. # # This code comes from lots of different sources: # http://faq.pygtk.org/index.py?req=show&file=faq13.045.htp # Create a custom gtk.CellRenderer # http://faq.pygtk.org/index.py?req=show&file=faq13.041.htp # How to add images/animations # http://nullege.com/codes/show/src%40t%40r%40translate-HEAD%40src%40trunk%40virtaal%40virtaal%40views%40widgets%40cellrendererwidget.py/147/gtk.CellEditable/python # how to add a widget. import gtk, random import pango import gtk, pygtk, gobject from gobject import idle_add, PARAM_READWRITE, SIGNAL_RUN_FIRST, TYPE_PYOBJECT star_grey = gtk.gdk.pixbuf_new_from_file("star.grey.png") star_hover = gtk.gdk.pixbuf_new_from_file("star.selected.png") star_selected = gtk.gdk.pixbuf_new_from_file("star.hover.png") no_star = gtk.gdk.pixbuf_new_from_file("no.star.png") no_star_selected = gtk.gdk.pixbuf_new_from_file("no.star.selected.png") no_star_grey = gtk.gdk.pixbuf_new_from_file("no.star.grey.png") class CellRendererStar(gtk.GenericCellRenderer): __gproperties__ = { "rating": (gobject.TYPE_INT, "Rating", "Rating", 0, 10, 0, gobject.PARAM_READWRITE), } def __init__(self, size=30, data_col=1): self.__gobject_init__() self.props.mode = gtk.CELL_RENDERER_MODE_EDITABLE self.image = None self.rating = None self.size = size self.star_grey = star_grey.scale_simple(self.size, self.size, gtk.gdk.INTERP_BILINEAR) self.star_selected = star_selected.scale_simple(self.size, self.size, gtk.gdk.INTERP_BILINEAR) self.star_hover = star_hover.scale_simple(self.size, self.size, gtk.gdk.INTERP_BILINEAR) self.no_star = no_star.scale_simple(self.size, self.size, gtk.gdk.INTERP_BILINEAR) self.no_star_grey = no_star_grey.scale_simple(self.size, self.size, gtk.gdk.INTERP_BILINEAR) self.no_star_selected = no_star_selected.scale_simple(self.size, self.size, gtk.gdk.INTERP_BILINEAR) self.rating = -1 self.editablemap = {} self.stars_hover = {} self.stars = {} self.data_col = data_col self._starting_edit = False def do_set_property(self, pspec, value): # print "PSPEC:",pspec.name,"=",value if pspec.name == 'rating': if value != self.rating: # print "REMADE!" self.rating = value self.make_stars() setattr(self, pspec.name, value) def make_stars(self): rating = self.rating if self.stars.has_key(rating): self.image = self.stars[rating] self.image.show() return self.image = gtk.Image() self.image.show() # self.widget.value = rating num_of_stars = 6 target_width = self.size * num_of_stars target_height = self.size # gtk.gdk.Pixbuf(colorspace, has_alpha, bits_per_sample, width, height) self.pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, target_width, target_height) for i in range(0,num_of_stars,1): if i == 0: if rating == 0: self.no_star.copy_area( 0, # src_x 0, # src_y self.size, # width self.size, # height self.pb, # dest_pixbuf (i*self.size), # dest_x 0, # dest_y ) elif rating >= i: self.no_star_selected.copy_area( 0, # src_x 0, # src_y self.size, # width self.size, # height self.pb, # dest_pixbuf (i*self.size), # dest_x 0, # dest_y ) else: self.no_star_grey.copy_area( 0, # src_x 0, # src_y self.size, # width self.size, # height self.pb, # dest_pixbuf (i*self.size), # dest_x 0, # dest_y ) continue if rating >= i: # copy_area(src_x, src_y, width, height, dest_pixbuf, dest_x, dest_y) self.star_selected.copy_area( 0, # src_x 0, # src_y self.size, # width self.size, # height self.pb, # dest_pixbuf (i*self.size), # dest_x 0, # dest_y ) else: self.star_grey.copy_area( 0, # src_x 0, # src_y self.size, # width self.size, # height self.pb, # dest_pixbuf (i*self.size), # dest_x 0, # dest_y ) self.image.set_from_pixbuf(self.pb) self.image.show() self.stars[rating] = self.image def make_stars_hover(self, rating): self.image = gtk.Image() self.image.show() if self.stars_hover.has_key(rating): self.image = self.stars_hover[rating] self.image.show() return num_of_stars = 6 target_width = self.size * num_of_stars target_height = self.size self.pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, target_width, target_height) for i in range(0,num_of_stars,1): if i == 0: if rating == 0: self.no_star.copy_area( 0, # src_x 0, # src_y self.size, # width self.size, # height self.pb, # dest_pixbuf (i*self.size), # dest_x 0, # dest_y ) elif rating >= i: self.no_star_selected.copy_area( 0, # src_x 0, # src_y self.size, # width self.size, # height self.pb, # dest_pixbuf (i*self.size), # dest_x 0, # dest_y ) else: self.no_star_grey.copy_area( 0, # src_x 0, # src_y self.size, # width self.size, # height self.pb, # dest_pixbuf (i*self.size), # dest_x 0, # dest_y ) continue if rating >= i: # copy_area(src_x, src_y, width, height, dest_pixbuf, dest_x, dest_y) self.star_hover.copy_area( 0, # src_x 0, # src_y self.size, # width self.size, # height self.pb, # dest_pixbuf (i*self.size), # dest_x 0, # dest_y ) else: self.star_grey.copy_area( 0, # src_x 0, # src_y self.size, # width self.size, # height self.pb, # dest_pixbuf (i*self.size), # dest_x 0, # dest_y ) self.image.set_from_pixbuf(self.pb) self.image.show() self.stars_hover[rating] = self.image def do_get_property(self, pspec): return getattr(self, pspec.name) def func(self, model, path, iter, (image, tree)): # print "FUNC:",model,path,iter, (image, tree) if model.get_value(iter, 0) == image: self.redraw = 1 cell_area = tree.get_cell_area(path, tree.get_column(0)) tree.queue_draw_area(cell_area.x, cell_area.y, cell_area.width, cell_area.height) def animation_timeout(self, tree, image): if image.get_storage_type() == gtk.IMAGE_ANIMATION: self.redraw = 0 image.get_data('iter').advance() model = tree.get_model() model.foreach(self.func, (image, tree)) if self.redraw: gobject.timeout_add(image.get_data('iter').get_delay_time(), self.animation_timeout, tree, image) else: image.set_data('iter', None) def on_activate(event, widget, path, background_area, cell_area, flags): print "ON_ACTIVATE:",event, widget, path, background_area, cell_area, flags def on_button_press(self, tree, event): # "on_button_press:", tree, event try: path, col, x, y = tree.get_path_at_pos(int(event.x), int(event.y)) except TypeError: return rends = col.get_cell_renderers() if rends[0] != self: return cell_area = tree.get_cell_area(path, tree.get_column(0)) rating = x / self.size # print "x, y:", x, y model = tree.get_model() model[path][self.data_col] = rating expose_area = tree.get_background_area(path, col) flags = gtk.CELL_RENDERER_SELECTED self.make_stars_hover(rating) self.on_render(tree.get_bin_window(), tree, tree.get_background_area(path, col), tree.get_cell_area(path, col), expose_area, flags) def on_motion_notify(self, tree, event): try: path, col, x, y = tree.get_path_at_pos(int(event.x), int(event.y)) except TypeError: return rends = col.get_cell_renderers() flags = 0 if rends[0] != self: return expose_area = background_area = tree.get_background_area(path, col) cell_area = tree.get_cell_area(path, tree.get_column(0)) x = event.x y = event.y window = tree.get_bin_window() x, y, mask = window.get_pointer() if x > (expose_area.x+5) and int(x) < (expose_area.x + (expose_area.width-5)) and int(y) > expose_area.y and int(y) < (expose_area.y + cell_area.height): # print "IN!" flags = gtk.CELL_RENDERER_PRELIT # self.make_stars_hover(rating) self.on_render(window, tree, background_area , tree.get_cell_area(path, col), expose_area, flags) def on_render(self, window, widget, background_area, cell_area, expose_area, flags): self.props.mode = gtk.CELL_RENDERER_MODE_INERT if not self.image: return # self.make_stars() if flags & gtk.CELL_RENDERER_PRELIT: x, y, mask = window.get_pointer() try: # print "background_area:",background_area path, col, _x, _y = widget.get_path_at_pos(int(x), int(y)) if x > background_area.x and x < (background_area.x + background_area.width) and y > background_area.y and y < (background_area.y + background_area.height): # print "x:(%s) > xa:(%s) and x:(%s) < xa+w:(%s)" % (_x, background_area.x, _x, (background_area.x + background_area.width) ) rating = _x / self.size self.make_stars_hover(rating) else: # print "OUT!" self.make_stars() # self.make_stars_hover(rating) except TypeError: # print "TYPEERROR!" self.make_stars() else: self.make_stars() pix_rect = gtk.gdk.Rectangle() pix_rect.x, pix_rect.y, pix_rect.width, pix_rect.height = self.on_get_size(widget, cell_area) pix_rect.x += cell_area.x pix_rect.y += cell_area.y pix_rect.width -= 2 * self.get_property("xpad") pix_rect.height -= 2 * self.get_property("ypad") draw_rect = cell_area.intersect(pix_rect) draw_rect = expose_area.intersect(draw_rect) pix = self.image.get_pixbuf() window.draw_pixbuf(widget.style.black_gc, pix, draw_rect.x-pix_rect.x, draw_rect.y-pix_rect.y, draw_rect.x, draw_rect.y, draw_rect.width, draw_rect.height, gtk.gdk.RGB_DITHER_NONE, 0, 0) def on_get_size(self, widget, cell_area): if not self.image: return 0, 0, 0, 0 if self.image.get_storage_type() == gtk.IMAGE_ANIMATION: animation = self.image.get_animation() pix_rect = animation.get_iter().get_pixbuf() elif self.image.get_storage_type() == gtk.IMAGE_PIXBUF: pix = self.image.get_pixbuf() else: return 0, 0, 0, 0 pixbuf_width = pix.get_width() pixbuf_height = pix.get_height() calc_width = self.get_property("xpad") * 2 + pixbuf_width calc_height = self.get_property("ypad") * 2 + pixbuf_height x_offset = 0 y_offset = 0 if cell_area and pixbuf_width > 0 and pixbuf_height > 0: x_offset = self.get_property("xalign") * (cell_area.width - calc_width - self.get_property("xpad")) y_offset = self.get_property("yalign") * (cell_area.height - calc_height - self.get_property("ypad")) # print "on_get_size:",x_offset, y_offset, calc_width, calc_height return x_offset, y_offset, calc_width, calc_height gobject.type_register(CellRendererStar) if __name__ == "__main__": class Tree(gtk.TreeView): def __init__(self): self.store = gtk.ListStore(str, int, int) gtk.TreeView.__init__(self) self.set_model(self.store) self.set_headers_visible(True) self.append_column(gtk.TreeViewColumn('First', gtk.CellRendererText(), text=0)) # self.append_column(gtk.TreeViewColumn('Second', CellRendererWidget(lambda widget:widget.get_label()), widget=1)) cell = CellRendererStar(30,1) cell.set_property('xalign',0.0) self.append_column(gtk.TreeViewColumn('Erm', cell, rating=1)) self.connect("motion-notify-event", cell.on_motion_notify) self.connect("button-press-event", cell.on_button_press) cell = CellRendererStar(30,2) cell.set_property('xalign',0.0) self.append_column(gtk.TreeViewColumn('Steph', cell, rating=2)) self.connect("motion-notify-event", cell.on_motion_notify) self.connect("button-press-event", cell.on_button_press) self.connect("key-release-event", self.on_key_release) self.set_property('reorderable',True) self.set_property('rubber-banding', True) select = self.get_selection() select.set_mode(gtk.SELECTION_MULTIPLE) def insert(self, name): for r in range(1,20): self.store.append(["%s.%s" % (r, name), random.randint(0, 7), random.randint(0, 7)]) def on_key_release(self, widget, event): print "on_key_release:",event.keyval,'string:', event.string, 'keycode:', event.hardware_keycode, 'state:',event.state if event.hardware_keycode == 119 and event.state == 0: selection = self.get_selection() selected_rows = selection.get_selected_rows() print "selected_rows:",selected_rows liststore, rows = selected_rows rows.reverse() for r in rows: del liststore[r] def on_change(*args): print "CHANGED!", args w = gtk.Window() w.set_position(gtk.WIN_POS_CENTER) w.connect('delete-event', gtk.main_quit) t = Tree() m = t.get_model() m.connect("row-changed", on_change) t.insert('foo') w.add(t) w.show_all() gtk.main()