# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
##
## Copyright (C) 2005-2012 Async Open Source
##
## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU Lesser 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 Lesser 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>
##
""" Base classes for editors """
import logging
import gtk
from gtk import gdk
from kiwi.enums import ListType
from kiwi.ui.delegates import GladeSlaveDelegate
from kiwi.ui.listdialog import ListContainer
from kiwi.ui.widgets.label import ProxyLabel
from stoqlib.database.runtime import StoqlibStore
from stoqlib.gui.base.dialogs import RunnableView, BasicDialog, run_dialog
from stoqlib.gui.events import (EditorSlaveCreateEvent, EditorSlaveConfirmEvent,
EditorCreateEvent)
from stoqlib.lib.decorators import public
from stoqlib.lib.message import yesno
from stoqlib.lib.translation import stoqlib_gettext
log = logging.getLogger(__name__)
_ = stoqlib_gettext
def _get_slaves_recursively(editor_slave):
slaves = []
for slave in editor_slave.slaves.values():
if isinstance(slave, BaseEditorSlave):
slaves.append(slave)
slaves.extend(_get_slaves_recursively(slave))
return slaves
[docs]class BaseEditorSlave(GladeSlaveDelegate):
""" Base class for editor slaves inheritance. It offers methods for
setting up focus sequence, required attributes and validated attrs.
:cvar gladefile:
:cvar model_type:
"""
domain = 'stoq'
gladefile = None
model_type = None
fields = None
proxy_widgets = ()
def __init__(self, store, model=None, visual_mode=False, edit_mode=None):
""" A base class for editor slaves inheritance
:param store: a store
:param model: the object model tied with the proxy widgets
:param visual_mode: does this slave must be opened in visual mode?
if so, all the proxy widgets will be disable
:param edit_mode: indicate if the slave are being edited or its a new one
The editor that creates the slave should pass his
property edit_mode to the slaves.
If None self.edit_mode will be defined accordingly to
the model
"""
self.store = store
# FIXME: Need to check if we need to add edit_mode parameter for all classes
# that inherit from BaseEditor
if edit_mode is not None:
self.edit_mode = edit_mode
else:
self.edit_mode = model is not None
self.visual_mode = visual_mode
if model:
created = ""
else:
created = "created "
model = self.create_model(self.store)
if model is None:
fmt = "%s.create_model() must return a valid model, not %r"
raise ValueError(fmt % (self.__class__.__name__, model))
log.info("%s editor using a %smodel %s" % (
self.__class__.__name__, created, type(model).__name__))
if self.model_type:
if not isinstance(model, self.model_type):
fmt = '%s editor requires a model of type %s, got a %r'
raise TypeError(
fmt % (self.__class__.__name__,
self.model_type.__name__,
model))
else:
fmt = "Editor %s must define a model_type attribute"
raise ValueError(fmt % (self.__class__.__name__, ))
self.model = model
GladeSlaveDelegate.__init__(self, gladefile=self.gladefile)
if self.visual_mode:
self._setup_visual_mode()
self.setup_proxies()
self.setup_slaves()
EditorSlaveCreateEvent.emit(self, model, store, visual_mode)
#
# Private
#
def _setup_visual_mode(self):
widgets = self.__class__.proxy_widgets
for widget_name in widgets:
widget = getattr(self, widget_name)
if isinstance(widget, ProxyLabel):
pass
elif isinstance(widget, gtk.Entry):
# First, we need to be able to select text from entries
widget.set_editable(False)
# Second, make sure they don't look like they're editable,
# copy over the insentive style
style = widget.get_style()
widget.modify_text(
gtk.STATE_NORMAL, style.text[gtk.STATE_INSENSITIVE])
widget.modify_base(
gtk.STATE_NORMAL, style.base[gtk.STATE_INSENSITIVE])
else:
widget.set_sensitive(False)
if self.fields:
for field in self.fields.values():
field.set_sensitive(False)
field.can_edit = False
self.update_visual_mode()
#
# Public API
#
[docs] def confirm(self):
"""Confirms the editor
Before actually confirming, we will verify that
:meth:`.validate_confirm` (and all slaves validate_confirm)
returns ``True``.
Only if none of those calls return ``False``, we will call
:meth:`.on_confirm` on the editor and all the slaves.
:class:`BaseEditor` will call this when someone clicks it's
*confirm* button.
"""
slaves = _get_slaves_recursively(self)
slaves.append(self)
# Do this before trying to confirm any of the slaves for safety
if not all([slave.is_valid and slave.validate_confirm() for
slave in slaves]):
return False
for slave in slaves:
slave.on_confirm()
EditorSlaveConfirmEvent.emit(self, self.model, self.store, self.visual_mode)
return True
[docs] def cancel(self):
"""Cancels the editor
By calling this, the editor will get cancelled. All of the
editor's slaves :meth:`.on_cancel`, including the editor's
itself will get called.
:class:`BaseEditor` will call this when someone clicks it's
*cancel* button.
"""
slaves = _get_slaves_recursively(self)
slaves.append(self)
for slave in slaves:
slave.on_cancel()
[docs] def create_model(self, store):
"""Creates a new model for the editor.
After this method is called, the model can be accessed as self.model.
The default behavior is to raise a TypeError, which can
be overridden in a subclass.
:param store: a store
"""
raise TypeError(
"%r needs a model, got None. Perhaps you want to "
"implement create_model?" % (self.__class__.__name__))
[docs] def setup_proxies(self):
"""A subclass can override this to setup proxies
This is called at the end of the object initialization and before
:meth:`.setup_slaves`. If you need to add a proxy, it should
be done here, eg:
self.proxy = self.add_proxy(self.model, self.proxy_widgets)
"""
[docs] def setup_slaves(self):
"""A subclass can override this to setup slaves
This is called at the end of the object initialization and after
:meth:`.setup_proxies`. If you need to add slaves, it should
be done here, eg:
self.attach_slave('slave_holder', SlaveClass())
"""
[docs] def on_cancel(self):
"""Called when the dialog is about to get cancelled
When someone calls :meth:`.cancel` (normally a :class:`BaseEditor`:
when clicking on it's *cancel* button), this will be called. If you
need to do some kind of cleanup when cancelling, like removing an
object that was already commited, this is the right place to do it.
Some places (like :func:`stoqlib.gui.base.dialogs.run_dialog`) will
consider the editor's retval as :obj:`.retval`. Depending on the
case, if you want to confirm the editor even when it was cancelled
(e.g. Asking the user "Do you want to save those changes?")
you can do:
self.retval = True
Or simply do what you want in another store and commit it.
"""
[docs] def on_confirm(self):
"""Called when the dialog is about to get confirmed
When someone calls :meth:`.confirm` (normally a :class:`BaseEditor`:
when clicking on it's *confirm* button), this will be called. If you
need to do some kind of late modification before the object gets
commited, this is the place to do it.
Some places (like :func:`stoqlib.gui.base.dialogs.run_dialog`) will
consider the editor's retval as :obj:`.retval`. Depending on the
case, if you want to cancel the editor even when it was confirmed
you can do:
self.retval = False
Or simply do a rollback on the actual store.
"""
[docs] def update_visual_mode(self):
"""Called when the editor enters on visual mode
When the editor is readonly (aka visual mode), every proxy widgets
and fields will be set insensitive. You should override this if
you have any kind of extra work to do when entering visual mode.
"""
[docs] def validate_confirm(self):
"""Called to see if the editor can be confirmed
The editor won't get confirmed if this returns ``False``. It's
recommended to show a message for the user, as a feedback, to
explain why this didn't confirm. By default, this returns ``True``.
"""
return True
@public(since="1.5.0")
[docs]class BaseEditor(BaseEditorSlave, RunnableView):
""" Base class for editor dialogs. It offers methods of
BaseEditorSlave, a windows title and OK/Cancel buttons.
"""
#: the model type name of the model we are editing.
#: This value will be showed in the title of the editor and can not
#: be merely the attribute __name__ of the object for usability reasons.
#: Call sites will decide what could be the best name applicable in each
#: situation.
model_name = None
header = ''
size = ()
title = None
hide_footer = False
#: if we need to ask the user if he really wants to cancel the dialog if
#: there are any changes done that would be lost otherwise
need_cancel_confirmation = False
#: a list of widget names that when activated will confirm the dialog
confirm_widgets = ()
help_section = None
form_holder_name = 'toplevel'
def __init__(self, store, model=None, visual_mode=False):
self._confirm_disabled = False
# FIXME:
# BasicEditor should inheirt from BasicDialog and instantiate
# the slave inside here, but it requires some major surgery
BaseEditorSlave.__init__(self, store, model,
visual_mode=visual_mode)
self.main_dialog = BasicDialog(title=self.get_title(self.model),
header_text=self.header,
help_section=self.help_section,
size=self.size)
# Do not close the dialog if re return False on self.confirm
self.main_dialog.enable_confirm_validation = True
self.main_dialog.attach_slave("main", self)
self.main_dialog.connect('confirm', self._on_main_dialog__confirm)
self.main_dialog.connect('cancel', self._on_main_dialog__cancel)
dialog_toplevel = self.main_dialog.get_toplevel()
dialog_toplevel.connect('response', self._on_toplevel__response)
dialog_toplevel.connect('delete-event', self._on_toplevel__delete_event)
# This helps kiwis ui test, set the name of ourselves to
# the classname of the slave, which is much more helpful than
# just "BasicDialog"
self.main_dialog.get_toplevel().set_name(self.__class__.__name__)
if self.hide_footer or self.visual_mode:
self.main_dialog.hide_footer()
for name in self.confirm_widgets:
self.set_confirm_widget(getattr(self, name))
self.register_validate_function(self._validation_function)
self.force_validation()
# We need to use self.model instead of model, since BaseEditorSlave
# will create one if its None
EditorCreateEvent.emit(self, self.model, store, visual_mode)
if store is not None:
# This needs to be the last thing done on __init__ since we don't want
# to consider things like self.create_model as a change
self._store_pending_count = store.get_pending_count()
#
# Private
#
def _get_title_format(self):
if self.visual_mode:
return _(u"Details of %s")
if self.edit_mode:
return _(u'Edit Details of "%s"')
return _(u"Add %s")
def _need_cancel_confirmation(self):
return self.need_cancel_confirmation and self.has_changes()
#
# Public
#
[docs] def has_changes(self):
"""Check if there are changes on this editor
By default we will check if there're any pending changes on
:obj:`.store` and that information will be used by
:attr:`.need_cancel_confirmation`
"""
if self.store is None:
return False
return self.store.get_pending_count() > self._store_pending_count
[docs] def get_title(self, model):
if self.title:
return self.title
if not model:
raise ValueError("A model should be defined at this point")
title_format = self._get_title_format()
if self.model_name:
model_name = self.model_name
else:
# Fallback to the name of the class
model_name = type(self.model).__name__
return title_format % model_name
[docs] def enable_window_controls(self):
"""Enables the window controls
See :class:`kiwi.ui.views.BaseView.enable_window_controls`.
"""
self.main_dialog.enable_window_controls()
[docs] def set_description(self, description):
"""Sets the description of the model object which is used by the editor
:param description:
"""
format = self._get_title_format()
self.main_dialog.set_title(format % description)
[docs] def refresh_ok(self, validation_value):
""" Refreshes ok button sensitivity according to widget validators
status """
if self._confirm_disabled:
return
self.main_dialog.ok_button.set_sensitive(validation_value)
[docs] def cancel(self):
"""
Cancel the dialog.
"""
if (self._need_cancel_confirmation() and
not yesno(_("If you cancel this dialog all changes will be lost. "
"Are you sure?"), gtk.RESPONSE_NO,
_("Cancel"), _("Don't cancel"))):
return False
# set this before runing BaseEditorSlave.cancel so
# on_cancel can modify self.retval, if needed
self.retval = False
BaseEditorSlave.cancel(self)
self.main_dialog.close()
if isinstance(self.store, StoqlibStore):
self.store.retval = self.retval
log.info("%s: Closed (cancelled), retval=%r" % (
self.__class__.__name__, self.retval))
return True
[docs] def confirm(self):
"""
Confirm the dialog.
"""
# set this before runing BaseEditorSlave.confirm so
# on_confirm can modify self.retval, if needed
self.retval = self.model
if self._confirm_disabled:
return False
if not BaseEditorSlave.confirm(self):
return False
self.main_dialog.close()
if isinstance(self.store, StoqlibStore):
self.store.retval = self.retval
log.info("%s: Closed (confirmed), retval=%r" % (
self.__class__.__name__, self.retval))
return True
[docs] def enable_ok(self):
"""
Enable the ok button of the dialog, eg makes it possible
to close/confirm the dialog.
"""
self.main_dialog.enable_ok()
self._confirm_disabled = False
[docs] def disable_ok(self):
"""
Enable the ok button of the dialog, eg makes it possible
to close/confirm the dialog.
"""
self.main_dialog.disable_ok()
self._confirm_disabled = True
[docs] def enable_normal_window(self):
"""
Enable the dialog as a normal window.
This tells the window manager that the window
should behave as a normal window instead of a dialog.
"""
toplevel = self.main_dialog.get_toplevel()
toplevel.set_type_hint(gdk.WINDOW_TYPE_HINT_NORMAL)
[docs] def set_message(self, message, message_type=None):
"""Sets a message for this editor
:param message: message to add
:param message_type: type of message to add
"""
if message_type is None:
message_type = gtk.MESSAGE_INFO
self.main_dialog.set_message(message, message_type)
# RunnableView
# This delegate everything to self.main_dialog
[docs] def close(self):
self.main_dialog.close()
[docs] def run(self):
self.main_dialog.run()
[docs] def get_current_toplevel(self):
return self.main_dialog.get_current_toplevel()
[docs] def destroy(self):
self.main_dialog.destroy()
[docs] def set_transient_for(self, window):
self.main_dialog.set_transient_for(window)
# Callbacks
def _on_main_dialog__cancel(self, dialog, retval):
return self.cancel()
def _on_main_dialog__confirm(self, dialog, retval):
return self.confirm()
def _validation_function(self, is_valid):
self.refresh_ok(is_valid)
def _on_toplevel__delete_event(self, widget, *args, **kwargs):
# Avoid the dialog being closed when hitting 'Esc' and we would need
# confirm the cancelation.
if self._need_cancel_confirmation():
return True
def _on_toplevel__response(self, dialog, response, *args, **kwargs):
# FIXME: For the delete-event to really stops from destroying the
# dialog, we also need to stop the response event emission. See
# http://faq.pygtk.org/index.py?req=show&file=faq10.013.htp
# for more details
if (self._need_cancel_confirmation() and
response == gtk.RESPONSE_DELETE_EVENT):
dialog.emit_stop_by_name('response')
[docs]class BaseRelationshipEditorSlave(GladeSlaveDelegate):
"""An editor for relationships between objects
BaseRelationshipEditor provides an easy way to edit (add/remove) relations
between objects.
It doesn't allow creations of new objects, only linking between them.
(the linking might require new objects, though)
For example, you could edit suppliers for a product (or produts supplied
by an supplier).
Subclasses must implement get_targets, get_columns, get_relations, and
create_model.
"""
domain = 'stoq'
gladefile = 'RelationshipEditor'
target_name = None
model_type = None
editor = None
def __init__(self, store, parent=None, visual_mode=False):
self._parent = parent
self.store = store
self.visual_mode = visual_mode
GladeSlaveDelegate.__init__(self, gladefile=self.gladefile)
self._setup_widgets()
def _setup_relations_list(self):
self.relations_list = ListContainer(self.get_columns(), gtk.ORIENTATION_HORIZONTAL)
self.relations_list._vbox.padding = 0
self.model_vbox.pack_start(self.relations_list)
self.relations_list.set_list_type(ListType.UNADDABLE)
self.relations_list.connect('remove-item',
self._on_remove_item__clicked)
self.relations_list.connect('edit-item', self._on_edit_item__clicked)
self.relations_list.show()
def _setup_widgets(self):
self.model_name_label.set_label(self.target_name + ':')
targets = self.get_targets()
self.target_combo.prefill(targets)
self._setup_relations_list()
size_group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
size_group.add_widget(self.add_button)
size_group.add_widget(self.relations_list.edit_button)
size_group.add_widget(self.relations_list.remove_button)
self.add_button.set_sensitive(False)
if not self.editor:
self.relations_list.edit_button.set_sensitive(False)
self.relations_list.add_items(self.get_relations())
if self.visual_mode:
self.target_combo.set_sensitive(False)
self.add_button.set_sensitive(False)
self.relations_list.set_list_type(ListType.READONLY)
[docs] def get_targets(self):
"""Returns a list of valid taret objects.
for instance, if suppliers for a product are being edited, then this
should return a list fo suppliers.
"""
raise NotImplementedError
[docs] def get_columns(self):
"""Columns to display"""
raise NotImplementedError
[docs] def get_relations(self):
"""Returns the already existing relations.
This may be entries from a maping table or entries from the target
table itself, depending on the type of relationship
"""
raise NotImplementedError
[docs] def create_model(self):
"""This method should create the model when adding a new relationship.
If the addition is canceled. It will automatically be removed.
"""
raise NotImplementedError
[docs] def add(self):
model = self.create_model()
if not model:
return False
if not self.editor:
return model
res = run_dialog(self.editor, self._parent, self.store, model)
if not res:
self.model_type.delete(id=model.id, store=self.store)
return res
[docs] def edit(self, model):
return run_dialog(self.editor, self._parent, self.store, model)
[docs] def remove(self, model):
self.model_type.delete(model.id, store=self.store)
return True
def _run_editor(self, model=None):
"""Runs an editor for the relationship (if necessary).
An editor may be necessary only if there is an mapping table and
and extra information in this table.
"""
if model is None:
res = self.add()
else:
res = self.edit(model)
return res
[docs] def on_target_combo__content_changed(self, widget):
has_selected = self.target_combo.read() is not None
self.add_button.set_sensitive(has_selected and not self.visual_mode)
def _on_edit_item__clicked(self, list, item):
if not self.editor:
return
result = self._run_editor(item)
if result:
self.relations_list.update_item(result)
def _on_remove_item__clicked(self, list, item):
if self.remove(item):
self.relations_list.remove_item(item)