127 lines
4.0 KiB
Python
127 lines
4.0 KiB
Python
#
|
|
# Python GUI - Models - Generic
|
|
#
|
|
|
|
import weakref
|
|
from GUI import export
|
|
from GUI.Properties import Properties, overridable_property
|
|
|
|
# List of views for a model is kept separately so that models
|
|
# can be pickled without fear of accidentally trying to pickle
|
|
# the user interface.
|
|
|
|
_model_views = weakref.WeakKeyDictionary() # {Model: [object]}
|
|
|
|
class Model(Properties):
|
|
"""A Model represents an application object which can appear in a View.
|
|
Each Model can have any number of Views attached to it. When a Model is
|
|
changed, it should notify all of its Views so that they can update
|
|
themselves.
|
|
|
|
The 'parent' attribute of a Model is treated specially when pickling.
|
|
If it refers to an object having a 'pickle_as_parent_model' attribute
|
|
whose value is false, the 'parent' attribute is not pickled. This allows
|
|
a Model to have a Document as a parent without the Document being pickled
|
|
along with the Model.
|
|
"""
|
|
|
|
views = overridable_property('views',
|
|
"List of objects observing this model. Do not modify directly.")
|
|
|
|
parent = None # Model
|
|
|
|
def __init__(self, parent = None, **kwds):
|
|
Properties.__init__(self, **kwds)
|
|
if parent:
|
|
self.parent = parent
|
|
|
|
def destroy(self):
|
|
"""All views currently observing this model are removed, and their
|
|
'model_destroyed' methods, if any, are called with the model as
|
|
an argument."""
|
|
for view in self.views[:]:
|
|
self.remove_view(view)
|
|
self._call_if_present(view, 'model_destroyed', self)
|
|
|
|
#
|
|
# Properties
|
|
#
|
|
|
|
def get_views(self):
|
|
views = _model_views.get(self)
|
|
if views is None:
|
|
views = []
|
|
_model_views[self] = views
|
|
return views
|
|
|
|
#
|
|
# Pickling behaviour
|
|
#
|
|
|
|
def __getstate__(self):
|
|
state = self.__dict__
|
|
parent = self.parent
|
|
if not getattr(parent, 'pickle_as_parent_model', True):
|
|
state = state.copy()
|
|
del state['parent']
|
|
return state
|
|
|
|
def __setstate__(self, state):
|
|
self.__dict__.update(state)
|
|
|
|
#
|
|
# Adding and removing views
|
|
#
|
|
|
|
def add_view(self, view):
|
|
"""Add the given object as an observer of this model. The view will
|
|
typically be a View subclass, but need not be. If the view is not
|
|
already an observer of this model and defines an 'add_model' method,
|
|
this method is called with the model as an argument."""
|
|
views = self.views
|
|
if view not in views:
|
|
views.append(view)
|
|
self._call_if_present(view, 'add_model', self)
|
|
|
|
def remove_view(self, view):
|
|
"""If the given object is currently an observer of this model, it
|
|
is removed, and if it defines a 'remove_model' method, this method
|
|
is called with the model as an argument."""
|
|
views = self.views
|
|
if view in views:
|
|
views.remove(view)
|
|
self._call_if_present(view, 'remove_model', self)
|
|
|
|
#
|
|
# View notification
|
|
#
|
|
|
|
def notify_views(self, message = 'model_changed', *args, **kwds):
|
|
"""For each observer, if the observer defines a method with the name of the
|
|
message, call it with the given arguments. Otherwise, if it defines a
|
|
method called 'model_changed', call it with no arguments. Otherwise,
|
|
do nothing for that observer."""
|
|
for view in self.views:
|
|
if not self._call_if_present(view, message, self, *args, **kwds):
|
|
self._call_if_present(view, 'model_changed', self)
|
|
|
|
def _call_if_present(self, obj, method_name, *args, **kwds):
|
|
method = getattr(obj, method_name, None)
|
|
if method:
|
|
method(*args, **kwds)
|
|
return 1
|
|
else:
|
|
return 0
|
|
|
|
#
|
|
# Marking as changed
|
|
#
|
|
|
|
def changed(self):
|
|
"Mark the containing Document as changed."
|
|
parent = self.parent
|
|
if parent:
|
|
parent.changed()
|
|
|
|
export(Model)
|