Source code for stoqlib.gui.widgets.kanbanview

# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4

##
## Copyright (C) 2013 Async Open Source <http://www.async.com.br>
## All rights reserved
##
## 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 Lesser General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., or visit: http://www.gnu.org/.
##
## Author(s): Stoq Team <stoq-devel@async.com.br>
##
##

# Implementatiopn of the kanban process view
# http://en.wikipedia.org/wiki/Kanban

import pickle

import gobject
import gtk
from kiwi.python import Settable
from kiwi.utils import gsignal
from kiwi.ui.objectlist import Column, ObjectList


[docs]class KanbanObjectListColumn(Column):
[docs] def create_renderer(self, model): renderer = CellRendererTextBox() renderer.props.xpad = 12 renderer.props.ypad = 12 return renderer, 'markup'
[docs]class CellRendererTextBox(gtk.CellRendererText): PADDING = 3 SIZE = 6 #: the magin color of the renderer, this the part to the left of it, #: indicating a category color margin_color = gobject.property(type=str)
[docs] def do_render(self, drawable, widget, background_area, cell_area, expose_area, flags): if flags & gtk.CELL_RENDERER_SELECTED: state = gtk.STATE_SELECTED else: state = gtk.STATE_NORMAL if type(drawable) == gtk.gdk.Pixmap: cr = drawable.cairo_create() cr.set_source_color(widget.style.bg[gtk.STATE_SELECTED]) cr.paint() else: widget.style.paint_box(drawable, state, gtk.SHADOW_IN, None, widget, "frame", cell_area.x + self.PADDING, cell_area.y + self.PADDING, cell_area.width - (self.PADDING * 2), cell_area.height - (self.PADDING * 2)) color = self.props.margin_color if color is not None: cr = drawable.cairo_create() cr.rectangle(cell_area.x + self.PADDING - 1, cell_area.y + self.PADDING, 4, cell_area.height - (self.PADDING * 2)) cr.set_source_color(gtk.gdk.color_parse(color)) cr.fill() gtk.CellRendererText.do_render(self, drawable, widget, background_area, cell_area, expose_area, flags)
[docs] def on_get_size(self, widget, cell_area=None): if cell_area is None: return (0, 0, 0, 0) else: return (cell_area.x - self.SIZE, cell_area.y - self.SIZE, cell_area.width + (self.SIZE * 2), cell_area.height + (self.SIZE * 2))
gobject.type_register(CellRendererTextBox)
[docs]class KanbanViewColumn(object): """A column in a KanbanView It just has a title and can be cleared via :attr:`.clear` and you can append an item via :attr:`.append_item` """ def __init__(self, title, value): """ :param title: The title for this column :param value: A value associated with this column. Can be used to determine what should be done when an item is drag and droped into a column """ self.title = title self.value = value self.view = None self.object_list = None
[docs] def clear(self): """Clear this view, eg remove all the items""" self.object_list.clear()
[docs] def append_item(self, item): """Append an item to the view""" self.object_list.append(item)
[docs]class KanbanView(gtk.Frame): """ This is a kanban view which can be used to display a set of columns with boxes that can be rearranged. """ __gtype_name__ = 'KanbanView' TREEVIEW_DND_TARGETS = [ ('text/plain', 0, 1), ] # item activated gsignal('item-activated', object) gsignal('item-dragged', object, object, retval=bool) gsignal('item-popup-menu', object, object) gsignal('selection-changed', object) gsignal('activate-link', object) def __init__(self): super(KanbanView, self).__init__() self.hbox = gtk.HBox() self.add(self.hbox) self.hbox.show() self.set_shadow_type(gtk.SHADOW_ETCHED_IN) # column title -> column self._columns = {} # column title -> objectlist self._treeviews = {} self._selected_iter = None self._selected_treeview = None self._message_label = None # # Public API #
[docs] def clear(self): """ Clears the view and all it's columns. """ self.clear_message() for column in self._columns.values(): column.clear()
[docs] def get_column_by_title(self, column_title): """ Get a column given a title :returns: a column or ``None`` if none are found """ return self._columns.get(column_title)
[docs] def add_column(self, column): """ Adds a new column to the view :param KanbanViewColumn column: column to add """ object_list = self._create_list(column.title) self.hbox.pack_start(object_list) object_list.show() self._columns[column.title] = column self._treeviews[column.title] = object_list.get_treeview() column.view = self column.object_list = object_list
[docs] def enable_editing(self): """ Makes it possible to edit items within this treeview. You also need to return ``True`` in the ::item-dragged callback for an item to be draggable. """ for treeview in self._treeviews.values(): treeview.enable_model_drag_source( gtk.gdk.BUTTON1_MASK, self.TREEVIEW_DND_TARGETS, gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE) treeview.enable_model_drag_dest( self.TREEVIEW_DND_TARGETS, gtk.gdk.ACTION_DEFAULT) treeview.connect( "drag-data-get", self._on_drag_data_get_data) treeview.connect( "drag-data-received", self._on_drag_data_received_data)
[docs] def select(self, item): """ Select an item in the view :param item: the item to select or ``None`` to unselect all """ # FIXME: How to make this cheaper with larger lists? for treeview in self._treeviews.values(): for row in treeview.get_model(): if row[0] == item: self._maybe_selection_changed(treeview, row.iter) return self._maybe_selection_changed(None, None)
[docs] def get_selected_item(self): """ Get the currently selected item from the view :returns: the selected item or ``None`` if no items are selected """ if self._message_label: return None if self._selected_iter is not None: model = self._selected_treeview.get_model() try: return model[self._selected_iter][0] except TypeError: # Dont know why this happens return None
[docs] def render_item(self, column, renderer, item): """ Renders an item, this is an optional hook that can be implemented by a subclass. :param column: the treeview column :param renderer: the cell renderer :parma item: the item """
[docs] def get_n_items(self): return sum(len(column.object_list.get_model()) for column in self._columns.values())
[docs] def set_message(self, markup): """Adds a message on top of the treeview rows :param markup: PangoMarkup with the text to add """ if self._message_label is None: self._viewport = gtk.Viewport() self._viewport.set_shadow_type(gtk.SHADOW_NONE) self.remove(self.hbox) self.add(self._viewport) self._message_box = gtk.EventBox() self._message_box.modify_bg( gtk.STATE_NORMAL, gtk.gdk.color_parse('white')) self._viewport.add(self._message_box) self._message_box.show() self._message_label = gtk.Label() self._message_label.connect( 'activate-link', self._on_message_label__activate_link) self._message_label.set_use_markup(True) self._message_label.set_alignment(0, 0) self._message_label.set_padding(12, 12) self._message_box.add(self._message_label) self._message_label.show() self._message_label.set_label(markup) self._viewport.show()
[docs] def clear_message(self): if self._message_label is None: return children = self.get_children() if self._viewport in children: self.remove(self._viewport) if self.hbox not in children: self.add(self.hbox) self._message_label.set_label("")
# # Private # def _create_list(self, column_title): object_list = ObjectList([ KanbanObjectListColumn('markup', title=column_title, data_type=str, use_markup=True, expand=True), ]) object_list.connect('row-activated', self._on_row_activated) object_list.connect('right-click', self._on_right_click) sw = object_list.get_scrolled_window() sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) sw.set_shadow_type(gtk.SHADOW_NONE) treeview = object_list.get_treeview() treeview.set_name(column_title) treeview.connect( "button-press-event", self._on_button_press_event) treeview.set_rules_hint(False) column = object_list.get_column_by_name('markup') column.treeview_column.set_clickable(False) white = gtk.gdk.color_parse('white') treeview.modify_base(gtk.STATE_ACTIVE, white) treeview.modify_base(gtk.STATE_SELECTED, white) object_list.set_cell_data_func(self._on_results__cell_data_func) return object_list def _on_results__cell_data_func(self, column, renderer, item, text): self.render_item(column, renderer, item) return text def _maybe_selection_changed(self, treeview=None, titer=None): if titer == self._selected_iter: return for check_treeview in self._treeviews.values(): selection = check_treeview.get_selection() if check_treeview == treeview and titer is not None: selection.select_iter(titer) else: selection.unselect_all() self._selected_iter = titer self._selected_treeview = treeview if titer is not None: item = treeview.get_model()[titer][0] else: item = None self.emit('selection-changed', item) # # Callbacks # def _on_message_label__activate_link(self, label, uri): self.emit('activate-link', uri) return True def _on_row_activated(self, olist, item): self.emit('item-activated', item) def _on_right_click(self, olist, item, event): self.emit('item-popup-menu', item, event) def _on_button_press_event(self, treeview, event): retval = treeview.get_path_at_pos(int(event.x), int(event.y)) model = treeview.get_model() if retval: titer = model[retval[0]].iter else: titer = None self._maybe_selection_changed(treeview, titer) def _on_drag_data_get_data(self, treeview, context, selection, target_id, etime): treeselection = treeview.get_selection() model, titer = treeselection.get_selected() if titer is None: return selection_data = self._create_selection_data(treeview, titer) selection.set(selection.target, 8, selection_data) def _create_selection_data(self, treeview, titer): model = treeview.get_model() path = model[titer].path return pickle.dumps([treeview.get_name(), path]) def _load_selection_data(self, selection_data): column_title, path = pickle.loads(selection_data) treeview = self._treeviews[column_title] model = treeview.get_model() return model[path][0] def _on_drag_data_received_data(self, treeview, context, x, y, selection, info, etime): model = treeview.get_model() if selection.data is None: context.finish(False, False, etime) return item = self._load_selection_data(selection.data) column = self._columns[treeview.get_name()] retval = self.emit('item-dragged', column, item) if retval is False: context.finish(False, False, etime) return drop_info = treeview.get_dest_row_at_pos(x, y) if drop_info: path, position = drop_info titer = model.get_iter(path) if (position == gtk.TREE_VIEW_DROP_BEFORE or position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE): titer = model.insert_before(titer, [item]) else: titer = model.insert_after(titer, [item]) else: titer = model.append([item]) if context.action == gtk.gdk.ACTION_MOVE: context.finish(True, True, etime) treeview.grab_focus() self._maybe_selection_changed(treeview, titer)
gobject.type_register(KanbanView)
[docs]def main(): win = gtk.Window() win.set_size_request(600, 300) win.connect('destroy', gtk.main_quit) kanban = KanbanView() for title in ['Opened', 'Approved', 'Executing', 'Finished']: kanban.add_column(KanbanViewColumn(title)) kanban.connect('item-dragged', lambda *x: True) kanban.enable_editing() items = [Settable(name='Ronaldo', phone='1972-2878'), Settable(name='Gabriel', phone='1234-5678'), Settable(name='João Paulo', phone='2982-8278'), Settable(name='Bellini', phone='2982-2909'), Settable(name='Johan', phone='2929-0202')] column = kanban.get_column_by_title('Opened') for item in items: column.append_item(item) win.add(kanban) win.show_all() gtk.main()
if __name__ == '__main__': main()