class AdditionListSlave(SearchSlave): """A slave that offers a simple list and its management. This slave also has the option to display a small message right next to the buttons """ domain = 'stoq' toplevel_name = gladefile = 'AdditionListSlave' widgets = ('add_button', 'delete_button', 'klist', 'list_vbox', 'edit_button') gsignal('before-edit-item', object, retval=object) gsignal('on-edit-item', object) gsignal('on-add-item', object) gsignal('before-delete-items', object) gsignal('after-delete-items') def __init__(self, store, columns=None, editor_class=None, klist_objects=None, visual_mode=False, restore_name=None, tree=False): """ Creates a new AdditionListSlave object :param store: a store :param columns: column definitions :type columns: sequence of :class:`kiwi.ui.objectlist.Columns` :param editor_class: the window that is going to be open when user clicks on add_button or edit_button. :type: editor_class: a :class:`stoqlib.gui.editors.BaseEditor` subclass :param klist_objects: initial objects to insert into the list :param visual_mode: if we are working on visual mode, that means, not possible to edit the model on this object type visual_mode: bool :param restore_name: the name used to save and restore the columns on a cache system (e.g. pickle) :type restore_name: basestring :param tree: Indication of which kind of list we are adding. If `True` ObjectTree otherwise ObjectList will be added """ columns = columns or self.get_columns() SearchSlave.__init__(self, columns=columns, restore_name=restore_name, store=store) self.tree = tree self.klist = ObjectTree() if tree else ObjectList() self.list_vbox.add(self.klist) self.list_vbox.show_all() if not self.columns: raise StoqlibError("columns must be specified") self.visual_mode = visual_mode self.store = store self.set_editor(editor_class) self._can_edit = True self._callback_id = None if self.visual_mode: self.hide_add_button() self.hide_edit_button() self.hide_del_button() items = klist_objects or self.get_items() self._setup_klist(items) self._update_sensitivity() def _setup_klist(self, klist_objects): self.klist.set_columns(self.columns) self.klist.set_selection_mode(gtk.SELECTION_MULTIPLE) if self.tree: (self.klist.append(obj.parent_item, obj) for obj in klist_objects) else: self.klist.add_list(klist_objects) if self.visual_mode: self.klist.set_sensitive(False) def _update_sensitivity(self, *args): if self.visual_mode: return can_delete = _can_edit = True objs = self.get_selection() if not objs: _can_edit = can_delete = False elif len(objs) > 1: _can_edit = False self.add_button.set_sensitive(True) self.edit_button.set_sensitive(_can_edit) self.delete_button.set_sensitive(can_delete) def _edit_model(self, model=None, parent=None): edit_mode = model result = self.emit('before-edit-item', model) if result is None: result = self.run_editor(model) if not result: return if edit_mode: self.emit('on-edit-item', result) self.klist.update(result) else: if self.tree: self.klist.append(parent, result) else: self.klist.append(result) # Emit the signal after we added the item to the list to be able to # check the length of the list in our validation callbacks. self.emit('on-add-item', result) # As we have a selection extended mode for kiwi list, we # need to unselect everything before select the new instance. self.klist.unselect_all() self.klist.select(result) self._update_sensitivity() def _edit(self): if not self._can_edit: return objs = self.get_selection() qty = len(objs) if qty != 1: raise SelectionError( ("Please select only one item before choosing Edit." "\nThere are currently %d items selected") % qty) self._edit_model(objs[0]) def _clear(self): objs = self.get_selection() qty = len(objs) if qty < 1: raise SelectionError('There are no objects selected') msg = stoqlib_ngettext( _('Delete this item?'), _('Delete these %d items?') % qty, qty) delete_label = stoqlib_ngettext( _("Delete item"), _("Delete items"), qty) keep_label = stoqlib_ngettext( _("Keep it"), _("Keep them"), qty) if not yesno(msg, gtk.RESPONSE_NO, delete_label, keep_label): return self.emit('before-delete-items', objs) if qty == len(self.klist): self.klist.clear() else: for obj in objs: self.klist.remove(obj) self.klist.unselect_all() self._update_sensitivity() self.emit('after-delete-items') # # Hooks # def get_items(self): return [] def get_columns(self): raise NotImplementedError("get_columns must be implemented in " "subclasses") def run_editor(self, model): """This can be overriden to provide a custom run_dialog line, or a conversion function for the model """ if self._editor_class is None: raise TypeError( "%s cannot create or edit items without the editor_class " "argument set" % (self.__class__.__name__)) self.store.savepoint('before_run_editor_addition') retval = run_dialog(self._editor_class, None, store=self.store, model=model) if not retval: self.store.rollback_to_savepoint('before_run_editor_addition') return retval def delete_model(self, model): """Deletes a model, can be overridden in subclass :param model: model to delete """ model.__class__.delete(model.id, store=self.store) # # Public API # def add_extra_button(self, label=None, stock=None): """Add an extra button on the this slave The extra button will be appended at the end of the button box, the one containing the add/edit/delete buttons :param label: label of the button, can be ``None`` if stock is passed :param stock: stock label of the button, can be ``None`` if label is passed :param returns: the button added :rtype: gtk.Button """ if label is None and stock is None: raise TypeError("You need to provide a label or a stock argument") button = gtk.Button(label=label, stock=stock) button.set_property('can_focus', True) self.button_box.pack_end(button, False, False) button.show() return button def set_message(self, message, details_callback=None): """Display a simple message on a label, next to the add, edit, delete buttons :param message: a message with properly escaped markup """ self.message_hbox.set_visible(True) self.message_details_button.set_visible(bool(details_callback)) if details_callback: if self._callback_id: self.message_details_button.disconnect(self._callback_id) self._callback_id = self.message_details_button.connect( 'clicked', details_callback) self.message_label.set_markup(message) def clear_message(self): self.message_hbox.set_visible(False) def get_selection(self): # XXX: add get_selected_rows and raise exceptions if not in the # right mode if self.klist.get_selection_mode() == gtk.SELECTION_MULTIPLE: return self.klist.get_selected_rows() selection = self.klist.get_selected() if not selection: return [] return [selection] def hide_add_button(self): self.add_button.hide() def hide_edit_button(self): self._can_edit = False self.edit_button.hide() def hide_del_button(self): self.delete_button.hide() def set_editor(self, editor_class): if editor_class and not issubclass(editor_class, (BaseEditor, BaseWizard)): raise TypeError("editor_class must be a BaseEditor subclass") self._editor_class = editor_class # # Signal handlers # def on_klist__row_activated(self, *args): self._edit() def on_klist__selection_changed(self, *args): self._update_sensitivity() def on_add_button__clicked(self, button): self._edit_model() def on_edit_button__clicked(self, button): self._edit() def on_delete_button__clicked(self, button): self._clear()
class AdditionListSlave(SearchSlave): """A slave that offers a simple list and its management. This slave also has the option to display a small message right next to the buttons """ domain = 'stoq' toplevel_name = gladefile = 'AdditionListSlave' widgets = ('add_button', 'delete_button', 'klist', 'list_vbox', 'edit_button') gsignal('on-edit-item', object) gsignal('on-add-item', object) gsignal('before-delete-items', object) gsignal('after-delete-items') def __init__(self, store, columns=None, editor_class=None, klist_objects=None, visual_mode=False, restore_name=None, tree=False): """ Creates a new AdditionListSlave object :param store: a store :param columns: column definitions :type columns: sequence of :class:`kiwi.ui.objectlist.Columns` :param editor_class: the window that is going to be open when user clicks on add_button or edit_button. :type: editor_class: a :class:`stoqlib.gui.editors.BaseEditor` subclass :param klist_objects: initial objects to insert into the list :param visual_mode: if we are working on visual mode, that means, not possible to edit the model on this object type visual_mode: bool :param restore_name: the name used to save and restore the columns on a cache system (e.g. pickle) :type restore_name: basestring :param tree: Indication of which kind of list we are adding. If `True` ObjectTree otherwise ObjectList will be added """ columns = columns or self.get_columns() SearchSlave.__init__(self, columns=columns, restore_name=restore_name, store=store) self.tree = tree self.klist = ObjectTree() if tree else ObjectList() self.list_vbox.add(self.klist) self.list_vbox.show_all() if not self.columns: raise StoqlibError("columns must be specified") self.visual_mode = visual_mode self.store = store self.set_editor(editor_class) self._can_edit = True self._callback_id = None if self.visual_mode: self.hide_add_button() self.hide_edit_button() self.hide_del_button() items = klist_objects or self.get_items() self._setup_klist(items) self._update_sensitivity() def _setup_klist(self, klist_objects): self.klist.set_columns(self.columns) self.klist.set_selection_mode(gtk.SELECTION_MULTIPLE) if self.tree: (self.klist.append(obj.parent_item, obj) for obj in klist_objects) else: self.klist.add_list(klist_objects) if self.visual_mode: self.klist.set_sensitive(False) def _update_sensitivity(self, *args): if self.visual_mode: return can_delete = _can_edit = True objs = self.get_selection() if not objs: _can_edit = can_delete = False elif len(objs) > 1: _can_edit = False self.add_button.set_sensitive(True) self.edit_button.set_sensitive(_can_edit) self.delete_button.set_sensitive(can_delete) def _edit_model(self, model=None, parent=None): edit_mode = model result = self.run_editor(model) if not result: return if edit_mode: self.emit('on-edit-item', result) self.klist.update(result) else: if self.tree: self.klist.append(parent, result) else: self.klist.append(result) # Emit the signal after we added the item to the list to be able to # check the length of the list in our validation callbacks. self.emit('on-add-item', result) # As we have a selection extended mode for kiwi list, we # need to unselect everything before select the new instance. self.klist.unselect_all() self.klist.select(result) self._update_sensitivity() def _edit(self): if not self._can_edit: return objs = self.get_selection() qty = len(objs) if qty != 1: raise SelectionError( ("Please select only one item before choosing Edit." "\nThere are currently %d items selected") % qty) self._edit_model(objs[0]) def _clear(self): objs = self.get_selection() qty = len(objs) if qty < 1: raise SelectionError('There are no objects selected') msg = stoqlib_ngettext(_('Delete this item?'), _('Delete these %d items?') % qty, qty) delete_label = stoqlib_ngettext(_("Delete item"), _("Delete items"), qty) keep_label = stoqlib_ngettext(_("Keep it"), _("Keep them"), qty) if not yesno(msg, gtk.RESPONSE_NO, delete_label, keep_label): return self.emit('before-delete-items', objs) if qty == len(self.klist): self.klist.clear() else: for obj in objs: self.klist.remove(obj) self.klist.unselect_all() self._update_sensitivity() self.emit('after-delete-items') # # Hooks # def get_items(self): return [] def get_columns(self): raise NotImplementedError("get_columns must be implemented in " "subclasses") def run_editor(self, model): """This can be overriden to provide a custom run_dialog line, or a conversion function for the model """ if self._editor_class is None: raise TypeError( "%s cannot create or edit items without the editor_class " "argument set" % (self.__class__.__name__)) self.store.savepoint('before_run_editor_addition') retval = run_dialog(self._editor_class, None, store=self.store, model=model) if not retval: self.store.rollback_to_savepoint('before_run_editor_addition') return retval def delete_model(self, model): """Deletes a model, can be overridden in subclass :param model: model to delete """ model.__class__.delete(model.id, store=self.store) # # Public API # def add_extra_button(self, label=None, stock=None): """Add an extra button on the this slave The extra button will be appended at the end of the button box, the one containing the add/edit/delete buttons :param label: label of the button, can be ``None`` if stock is passed :param stock: stock label of the button, can be ``None`` if label is passed :param returns: the button added :rtype: gtk.Button """ if label is None and stock is None: raise TypeError("You need to provide a label or a stock argument") button = gtk.Button(label=label, stock=stock) button.set_property('can_focus', True) self.button_box.pack_end(button, False, False) button.show() return button def set_message(self, message, details_callback=None): """Display a simple message on a label, next to the add, edit, delete buttons :param message: a message with properly escaped markup """ self.message_hbox.set_visible(True) self.message_details_button.set_visible(bool(details_callback)) if details_callback: if self._callback_id: self.message_details_button.disconnect(self._callback_id) self._callback_id = self.message_details_button.connect( 'clicked', details_callback) self.message_label.set_markup(message) def clear_message(self): self.message_hbox.set_visible(False) def get_selection(self): # XXX: add get_selected_rows and raise exceptions if not in the # right mode if self.klist.get_selection_mode() == gtk.SELECTION_MULTIPLE: return self.klist.get_selected_rows() selection = self.klist.get_selected() if not selection: return [] return [selection] def hide_add_button(self): self.add_button.hide() def hide_edit_button(self): self._can_edit = False self.edit_button.hide() def hide_del_button(self): self.delete_button.hide() def set_editor(self, editor_class): if editor_class and not issubclass(editor_class, (BaseEditor, BaseWizard)): raise TypeError("editor_class must be a BaseEditor subclass") self._editor_class = editor_class # # Signal handlers # def on_klist__row_activated(self, *args): self._edit() def on_klist__selection_changed(self, *args): self._update_sensitivity() def on_add_button__clicked(self, button): self._edit_model() def on_edit_button__clicked(self, button): self._edit() def on_delete_button__clicked(self, button): self._clear()
class EventUI(GladeSlaveDelegate): show_ranges = ['day', 'week', 'month', 'year'] def __init__(self, parent, mo): self.log = logging.getLogger('MINICAL') self.parent = parent self.mo = mo self.factory = Factory() self.__stop_auto_highlight = False # Disable automatic highlighting of events. self.__stop_auto_dayjump = False # Disable automatically jumping to the start of the event on selection. self.__stop_auto_treeview_update = False # FIXME GladeSlaveDelegate.__init__(self, gladefile='mo_tab_events', toplevel_name='window_main') # Set up the user interface eventColumns = [ Column('start', title='Start', data_type=datetime.datetime, sorted=True), Column('end', title='End', data_type=datetime.datetime), Column('summaryformat', title='Summary', use_markup=True), Column('duration', title='Duration', justify=gtk.JUSTIFY_RIGHT) ] self.treeview_event = ObjectTree(eventColumns) self.vbox_eventslist.add(self.treeview_event) self.combobox_display_range.set_active(self.show_ranges.index(self.mo.config['events.default_show'].lower())) cal_options = gtk.CALENDAR_WEEK_START_MONDAY if self.mo.config['events.cal_show_weeknr']: cal_options |= gtk.CALENDAR_SHOW_WEEK_NUMBERS self.calendar.set_display_options((self.calendar.get_display_options() | cal_options)) # Connect signals self.treeview_event.connect('selection-changed', self.treeview_event__selection_changed) self.treeview_event.connect('row-activated', self.treeview_event__row_activated) self.treeview_event.connect('key-press-event', self.treeview_event__key_press_event) self.on_toolbutton_today__clicked() def refresh(self): """ Refresh the entire events tab. This clears everything and rebuilds it. Call this when events are removed outside of this class. """ self.treeview_event.clear() self.calendar.clear_marks() self.on_calendar__month_changed(self.calendar) self.treeview_event__update() def on_toolbutton_add__clicked(self, *args): now = datetime.datetime.now() sel_day = self.calendar.get_date() start = datetime.datetime(sel_day[0], sel_day[1]+1, sel_day[2], now.hour, now.minute) end = start + datetime.timedelta(hours=+1) event = self.factory.event(start, end) event = miniorganizer.ui.EventEditUI(self.mo, event).run() if event: self.mo.cal_model.add(event) self.treeview_event.append(None, event) self.on_calendar__month_changed(self.calendar) self.on_calendar__day_selected(self.calendar) self.parent.menuitem_save.set_sensitive(True) def on_toolbutton_remove__clicked(self, *args): sel_event = self.treeview_event.get_selected() sel_real_event = getattr(sel_event, 'real_event', sel_event) # Delete real event instead of recurring event if sel_event != sel_real_event: response = dialogs.yesno('This is a recurring event. Deleting it will delete all recurrences. Are you sure you want to delete it?') if response == gtk.RESPONSE_NO: return else: sel_event = sel_real_event if sel_event: self.mo.cal_model.delete(sel_event) self.treeview_event.remove(sel_event) self.on_calendar__month_changed(self.calendar) self.on_calendar__day_selected(self.calendar) self.parent.menuitem_save.set_sensitive(True) def on_toolbutton_edit__clicked(self, *args): sel_event = self.treeview_event.get_selected() self.treeview_event__row_activated(self.treeview_event, sel_event) def on_toolbutton_today__clicked(self, *args): today_dt = datetime.date.today() self.calendar.select_month(today_dt.month - 1, today_dt.year) self.calendar.select_day(today_dt.day) def on_calendar__month_changed(self, calendar, *args): self.calendar.clear_marks() sel_date = self.calendar.get_date() month_start = datetime.datetime(sel_date[0], sel_date[1]+1, 1) month_end = month_start + relativedelta(months=+1, seconds=-1) events = self.mo.cal_model.get_events() + self.mo.cal_model.get_events_recurring(month_start, month_end) for event in events: event_start = event.get_start() event_end = event.get_end() self.log.debug('Event %s, start: %s, end %s' % (event.get_summary(), event_start, event_end)) # If the event falls in the month, mark the days the event spans in # the calendar. if (month_start >= event_start and month_start <= event_end) or \ (month_end >= event_start and month_end <= event_end) or \ (event_start >= month_start and event_end <= month_end): # Walk through the days of the event, marking them. delta_iter = datetime.datetime(*event_start.timetuple()[0:3]) while True: if delta_iter.year == month_start.year and delta_iter.month == month_start.month: self.calendar.mark_day(delta_iter.day) delta_iter = delta_iter + datetime.timedelta(days=+1) if delta_iter >= event_end: break def on_calendar__day_selected(self, calendar, *args): # Make sure the correct display range is shown. self.on_combobox_display_range__changed() # Retrieve the day the user selected. sel_day = self.calendar.get_date() day_start = datetime.datetime(sel_day[0], sel_day[1]+1, sel_day[2]) day_end = day_start + datetime.timedelta(days=+1) display_month = datetime.datetime(day_start.year, day_start.month, 1) # Highlight an event if it starts on the selected day. highlight_events = [] events = [event for event in self.treeview_event] for event in events: event_start = event.get_start() event_end = event.get_end() # If this is the first event that starts on the day the user # selected, highlight the item in the list of events. if event_start >= day_start and event_start < day_end: highlight_events.insert(0, event) # If the selected day occurs during an event, highlight it. We # append it to the list of events to be highlighted, so it'll only # be highlighted if no event actually starts on that day. elif (day_start > event_start and day_start < event_end) or \ (day_end > event_start and day_end < event_end) or \ (event_start > day_start and event_end < day_end): highlight_events.append(event) # Highlight the first event on the day the user selected, unless the # user manually selected an event. if not self.__stop_auto_highlight: if highlight_events and highlight_events[0] in self.treeview_event: self.__stop_auto_dayjump = True self.treeview_event.select(highlight_events[0], True) self.__stop_auto_dayjump = False else: self.treeview_event.unselect_all() def on_calendar__day_selected_double_click(self, *args): self.on_toolbutton_add__clicked() def on_combobox_display_range__changed(self, *args): # Get the currently selected date in the calendar. sel_date = self.calendar.get_date() sel_dt_start = datetime.datetime(sel_date[0], sel_date[1]+1, sel_date[2]) sel_dt_end = sel_dt_start + datetime.timedelta(days=+1) # Determine the start and end of the period that needs to be shown. display_range = self.combobox_display_range.get_active_text() if display_range == 'Day': display_start = sel_dt_start display_end = display_start + datetime.timedelta(days=+1, seconds=-1) text = '%s' % (display_start.strftime('%a %b %d %Y')) elif display_range == 'Week': display_start = sel_dt_start + datetime.timedelta(days=-sel_dt_start.weekday()) display_end = display_start + datetime.timedelta(weeks=+1, seconds=-1) text = '%s - %s' % (display_start.strftime('%a %b %d %Y'), display_end.strftime('%a %b %d %Y')) elif display_range == 'Month': display_start = sel_dt_start + datetime.timedelta(days=-(sel_dt_start.day - 1)) display_end = display_start + relativedelta(months=+1, seconds=-1) text = '%s' % (display_start.strftime('%b %Y')) elif display_range == 'Year': display_start = datetime.datetime(sel_dt_start.year, 1, 1) display_end = display_start + relativedelta(years=+1, seconds=-1) text = '%s' % (display_start.strftime('%Y')) else: raise Exception('No selected display range!') # Update the displayed range self.displayed_range.set_text(text) self.display_start = display_start self.display_end = display_end self.treeview_event__update() def treeview_event__update(self): if self.__stop_auto_treeview_update: return # First, remove all the recurring events, because they're generated on # the fly, so we can't know which ones in the list we need to remove. # Therefor we remove them every time. events_rm = [] for event in self.treeview_event: if hasattr(event, 'real_event'): events_rm.append(event) for event in events_rm: self.treeview_event.remove(event) # Add the events for the displayed range to the list events = self.mo.cal_model.get_events() + self.mo.cal_model.get_events_recurring(self.display_start, self.display_end) for event in events: event_start = event.get_start() event_end = event.get_end() # If the currently displayed range includes an event, add it to the list. if (self.display_start >= event_start and self.display_start < event_end) or \ (self.display_end >= event_start and self.display_end < event_end) or \ (event_start >= self.display_start and event_end < self.display_end): if not event in self.treeview_event: self.treeview_event.append(None, event) # Otherwise, we remove it from the list, if it's present. else: if event in self.treeview_event: self.treeview_event.remove(event) def treeview_event__row_activated(self, list, object): # FIXME: This might be more complicated than it needs to be. See todo.py's row_activated. sel_event = self.treeview_event.get_selected() sel_event = getattr(sel_event, 'real_event', sel_event) # Edit real event instead of recurring event event = miniorganizer.ui.EventEditUI(self.mo, sel_event).run() self.on_calendar__month_changed(self.calendar) self.on_calendar__day_selected(self.calendar) if sel_event in self.treeview_event: self.treeview_event.select(sel_event, True) self.parent.menuitem_save.set_sensitive(True) def treeview_event__selection_changed(self, list, selection): # Stop the treeview from automatically updating itself because that # will remove the recurring events and regenerate them (with different # instance IDs) which means the selection may be invalid. self.__stop_auto_treeview_update = True sel_event = self.treeview_event.get_selected() has_selection = sel_event is not None # Enable / disable toolbuttons self.toolbutton_remove.set_sensitive(has_selection) self.toolbutton_edit.set_sensitive(has_selection) # Do not jump to the day of the event. This is needed because an event # can be automatically selected even if it doesn't start on a # particular day. if self.__stop_auto_dayjump: self.__stop_auto_treeview_update = False return # Stop this selection from being overwritten. self.__stop_auto_highlight = True if has_selection: # Make the calendar jump to the day on which this event begins. sel_event_start = sel_event.get_start() self.calendar.select_month(sel_event_start.month - 1, sel_event_start.year) self.calendar.select_day(sel_event_start.day) # Enable automatic highlighting of items self.__stop_auto_highlight = False self.__stop_auto_treeview_update = False def treeview_event__key_press_event(self, treeview, event): if event.keyval == gtk.keysyms.Delete: self.on_toolbutton_remove__clicked()